📜 ⬆️ ⬇️

D for beginners, part 2

Good day, Habr!

Let's continue the subject of the previous article . There will be an explanation of such concepts as @ safe, @ trusted, pure, nothrow, some points related to OOP.

All code by default is @ system (with some exceptions), which means that low-level operations are allowed (work with pointers). In this mode, in D you can do absolutely everything the same as in C / C ++. It also includes many memory errors that can be made in C / C ++. There is a way to avoid a large number of errors, if you adhere to some restrictions. In fact, this is a subset of the D language, it is called SafeD and, according to the logic of working with it, is more like Java and C #. This mode is enabled by the @ safe attribute and disables all operations in the code that can cause undefined behavior.
SafeD Restrictions:

The @ trusted attribute allows using the system code inside safe. But you need to be very cautious about this possibility - check all trusted functions more carefully.

The @ nogc attribute prohibits the use of operations using the garbage collector and calling functions that are not @ nogc, more information about disabling the garbage collector can be found here .
')
The pure attribute indicates that the function will not use global or static mutable variables. This allows the compiler to use some optimizations. There is one exception to this rule — the debug block:
void func() pure { debug writeln( "print from pure" ); //      ... } 

To compile in debug mode, you must specify the dmd -debug flag (trivial, but still).
This attribute is also very useful when writing classes and structures (more on this later).

The nothrow attribute ensures that the function will not throw exceptions inherited from Exception. This does not prevent her from throwing Error exceptions. As conceived by the developers, the Error exceptions are unrecoverable, so it makes no sense to intercept them. Also, this does not prevent it from calling non-nothrow functions if they are enclosed in a try-catch block.

All functional literals and template functions, by default, have @ safe, @ nogc, pure, and nothrow attributes, if possible. To automatically assign each of the attributes to such functions, the appropriate conditions must be met.

@ Disable disables the function call. This is useful when writing structures that should not have any default functions, for example:
 struct Foo { @​disable this(); //    @​disable this(this); //  ,     this( int v ){} //        } void main() { Foo a; //    ,     auto b = Foo(3); //     auto c = b; //   ,     } 

This can be used not only with built-in functions:
 interface A { int bar(); } class B : A { int bar() { return 3; } } class C : B { @disable override int bar(); } void main() { auto bb = new B; bb.bar(); auto cc = new C; cc.bar(); //    } 

But I do not advise using this approach: it does not prohibit redefining a function in the class inherited from C, but calling it will fail during execution. There is an exception mechanism for this behavior.

The deprecated attribute displays a warning, useful for smoothly changing api, so that users everywhere can remove the call to such a function. This attribute accepts a string as a message to be displayed during compilation:
 deprecated("because it's old") void oldFunc() {} 

The attribute can be applied to the code in a different way: “just an attribute” is applied to the declaration following the attribute, when using curly brackets after the attribute, it is applied to the block of the upper levels and inside classes (declaration of blocks with attributes is forbidden and does not make sense within functions) and with a colon, in this case it is applied to the end of the file.
 module test; @​safe: ... //       pure { int somefunc1() {} //  @​safe,  pure int somefunc2() nothrow {} // @​safe, pure  nothrow } 


With simple things sorted out. Now it is worth highlighting, apparently, the most obscure topic: shared with immutable with structures and classes.

Take a simple example: we want to organize a message queue from one thread to another, using our own data structure.
Let's start with the structure. Suppose we need a timestamp and some message, and the structure will always be immutable (we don’t need other options).
 import std.stdio; import std.traits; import std.datetime; //  "  ",   ,     template isMessage(T) { enum isMessage = is( Unqual!T == _Message ); } struct _Message { ulong ts; immutable(void[]) data; //   @disable this(); //        immutable: //    immutable ,      immutable this(T)( auto ref const T val ) { static if( isMessage!T ) { //      ts = val.ts; data = val.data; } else { //     data       static if( isArray!T ) data = val.idup; else static if( is( typeof(val.array) ) ) //  ,   range data = val.array.idup; else static if( !hasUnsharedAliasing!T ) //    ,   ,      data = [val].idup; else static assert(0, "unsupported type" ); //     ts = Clock.currAppTick().length; } } //   auto as(T)() @property { static if( isArray!T ) return cast(T)(data.dup); else static if( !hasUnsharedAliasing!T ) return (cast(T[])(data.dup))[0]; else static assert(0, "unsupported type" ); } } alias Message = immutable _Message; //        ,    // ///   -  .          unittest { auto a = Message( "hello" ); auto b = Message( a ); assert( a.ts == b.ts ); assert( b.as!string == "hello" ); auto c = Message( b.data ); assert( a.ts != c.ts ); assert( c.as!string == "hello" ); auto d = a; auto e = Message( 3.14 ); assert( e.as!double == 3.14 ); } 

“Why only immutable?” You may ask? Here the question is ambiguous. Do you really need mutable messages in multi-threaded programming (about more serious types a little further)? The message is the message that it is small, "disposable." In Rust, for example, all variables are immutable by default. This further allows you to avoid unnecessary hemorrhoids with synchronization, as a result there are less errors and less code. But if you still need. First, the constructor must be pure (pure) - this will allow you to create any type of object using a single constructor (in our example, we use a function that gets time, it is not clean). Secondly, it is necessary to partially duplicate the code of access methods to the object. If the constructor cannot be clean, then you will also have to duplicate the code, clearly indicating the cases of its use. Example:
 struct CrdMessage { ulong code; float x, y; this( ulong code, float x, float y ) pure //   { this.code = code; this.x = x; this.y = y; } this( in CrdMessage msg ) pure //   { code = msg.code; x = msg.x; y = msg.y; } float sum() const @property { return x+y; } //  float sum() shared const @property { return x+y; } //  float sum() immutable @property { return x+y; } //  } 

Duplication could be removed with the help of the mixin template, but it's not just that. If we do not use shared objects of such a structure, then we can only get along with the const variant of the method (immutable objects will call it). The shared method is necessary, since we explicitly say that the object can be shared, therefore, we take responsibility for synchronization . This means that the code in the example contains an error , we did not take into account that the values ​​may change in another thread. The const and shared const methods are not enough to call a method for an immutable object, since the immutable object can be divided between threads and the type system cannot choose which method to call (const or shared const). Also, the const method may differ from immutable, since in the case of const we guarantee the immutability of the reference to the object, and in the case of immutable we guarantee the immutability of all fields of the structure throughout its lifetime, so we may need to do some actions in the const method Immutable no need to perform (additional copying for example). Such a type system makes you think about the actions performed and be more careful when writing shared code, but at first it can cause pain.
Once upon a time I myself experienced this pain, but I was hard-nosed and illiterate, I wanted everything at once
As a result, I got some code that worked as I wanted (with a few reservations) and since then it, in principle, has not changed much: here is the structure that stores untyped data, here is the message transmitted between threads, using the storage structure data.

Let's return to the creation of our multi-threaded application. We implement the queue of the simplest queue (we do not think about the optimization of memory allocation).
 synchronized class MsgQueue { Message[] data; //    //  ,       foreach bool empty() { return data.length == 0; } Message front() { return data[0]; } void popFront() { data = data[1..$]; } void put( Message msg ) { data ~= msg; } } 

Yes, everything is so simple! In fact, everything related to structures can be applied to classes (in terms of shared, immutable, etc.). The synchronized keyword means that the class is shared, but synchronized can only be used with classes and it has an important difference from shared. In order, as it might be:
 class MsgQueue { Message[] data; import core.sync.mutex; Mutex mutex; //   this() shared { mutex = cast(shared Mutex)new Mutex; } // - Mutex   shared  ... void popFront() shared // { synchronized(mutex) //  ,   { data = data[1..$]; } } ... } 

You can not mark each method with the shared attribute, but make the entire class shared. You can also use the object itself of the MsgQueue class (and any other) as a synchronization object:
 shared class MsgQueue { Message[] data; ... void popFront() { synchronized(this) { data = data[1..$]; } } ... } 

An object of any class can be a synchronization object due to the fact that from the base class (Object) each object adopts a synchronization object (__monitor) that implements the Object.Monitor interface (Mutex also implements it).

If we want to synchronize not the block inside the method, but the entire method, while we want to use the class instance itself as the synchronization object, then we can make the entire method synchronized:
 shared class MsgQueue { Message[] data; ... void popFront() synchronized { data = data[1..$]; } ... } 

If all class methods must be thread safe, we can render synchronized, like shared, to the class level, then we return to the original spelling.

I hope I managed to clarify some non-obvious points. Again, if you think it's worth paying special attention to something, write about it. I brought here only what seemed unobvious to me.

Full text messaging program
 import std.stdio; import std.traits; import std.datetime; //  "  ",   ,     template isMessage(T) { enum isMessage = is( Unqual!T == _Message ); } struct _Message { ulong ts; immutable(void[]) data; //   @disable this(); //        immutable: //    immutable ,      immutable this(T)( auto ref const T val ) { static if( isMessage!T ) { //      ts = val.ts; data = val.data; } else { //     data       static if( isArray!T ) data = val.idup; else static if( is( typeof(val.array) ) ) //  ,   range data = val.array.idup; else static if( !hasUnsharedAliasing!T ) //    ,   ,      data = [val].idup; else static assert(0, "unsupported type" ); //     ts = Clock.currAppTick().length; } } //   auto as(T)() @property { static if( isArray!T ) return cast(T)(data.dup); else static if( !hasUnsharedAliasing!T ) return (cast(T[])(data.dup))[0]; else static assert(0, "unsupported type" ); } } alias Message = immutable _Message; synchronized class MsgQueue { Message[] data; bool empty() { return data.length == 0; } Message front() { return data[0]; } void popFront() { data = data[1..$]; } void put( Message msg ) { data ~= msg; } } unittest { auto mq = new shared MsgQueue; mq.put( Message( "hello" ) ); mq.put( Message( "habr" ) ); string[] msgs; foreach( msg; mq ) msgs ~= msg.as!string; assert( msgs == ["hello", "habr"] ); } void randomsleep(uint min=1,ulong max=100) { import core.thread; import std.random; Thread.sleep( dur!"msecs"(uniform(min,max)) ); } import std.string : format; void sender( shared MsgQueue mq, string name ) { scope(exit) writefln( "sender %s finish", name ); foreach( i; 0 .. 15 ) { mq.put( Message( format( "message #%d from [%s]", i, name ) ) ); randomsleep; } } void receiver( shared MsgQueue mq ) { uint empty_mq = 0; bool start_receive = false; scope(exit) writeln( "reciver finish" ); m: while(true) { if( mq.empty ) empty_mq++; if( empty_mq > 10 && start_receive ) return; foreach( msg; mq ) { writefln( "[%012d]: %s", msg.ts, msg.as!string ); randomsleep; start_receive = true; } } } import std.concurrency; void main() { auto mq = new shared MsgQueue; spawn( &receiver, mq ); foreach( i; 0 .. 10 ) spawn( &sender, mq, format( "%d", i ) ); writeln( "main finish" ); } 



Also in the standard library D there is an implementation of "green" threads (this is me just in case), documentation on off.sayte .

Source: https://habr.com/ru/post/261079/


All Articles