Good day, Habr!
It is already quite an adult language, and there is very little material in Russian in the network. It is necessary to fill the gap. In this post I want to talk about a rather boring, but very important topic of modifiers, attributes and the like. Their abundance in D can scare away people who are just starting to get acquainted with the language. And not everyone who uses the language has a complete picture. But not so scary, not harder than others)
Variable declaration and initialization
Let's start with the simple:
int z; // z == int.init == 0 int a = 5; // auto b = 5; // int auto bl = 5_000_000_000; // long, int auto bu = 5U; // uint, u U , unsigned auto bl2 = 5L; // long, L, l ( L) 1 auto bul = 5UL; // ulong, U L const c = 5; // const(int) immutable d = 5; // immutable(int) shared e = 5; // shared(int) shared const f = 5; // shared(const(int)) shared immutable g = 5; // immutable(int) auto k; // : , import std.variant; Variant k2; // , ,
The type is explicitly indicated only for z, a, k2, in all the others it is derived from the literal, since it is always easy to calculate the type of a variable. You can read about the main data types
here . In addition to the literal, the type of the variable is automatically calculated if the result of the function is written to it.
By default, in D all variables local to the stream (
TLS ), to use a variable in another thread, it must be shared or immutable. It’s worth explaining how immutable differs from const. When we create a variable, there is not much difference, and we cannot change the one and the other after initialization. The significant difference appears when we transfer them to functions and methods, so we will return to this issue when considering the arguments of functions.
Array Types
int[] a; // int[3] b; // int[int] c; // ( ) int[][] d; // int[int[]] e; //
The last option, though possible, is not convenient, since you need to use an array of immutable values (immutable) to set the value as the key:
e[cast(immutable(int)[])[8,3]] = 42;
And here we smoothly touched the topic of array type modifiers
immutable(int)[] a = [3,4];
I did not find a way to create an immutable array of mutable data.
')
Modifiers can be combined:
const(shared(int)[]) a = [1]; // shared(const(int)[]) b = [2]; // const(shared int[]) c = [3]; // shared(const int[]) d = [4]; //
At first it may seem that there is not much difference between them.
And you can even check it out quickly. void main() { void fnc_a( const(shared(int)[]) a ) {} void fnc_b( shared(const(int)[]) a ) {} void fnc_c( const(shared int[]) a ) {} void fnc_d( shared(const int[]) a ) {} const(shared(int)[]) a = [1]; shared(const(int)[]) b = [2]; const(shared int[]) c = [3]; shared(const int[]) d = [4]; fnc_a( a ); fnc_a( b ); fnc_a( c ); fnc_a( d ); fnc_b( a ); fnc_b( b ); fnc_b( c ); fnc_b( d ); fnc_c( a ); fnc_c( b ); fnc_c( c ); fnc_c( d ); fnc_d( a ); fnc_d( b ); fnc_d( c ); fnc_d( d ); }
Between the last two definitely not (this is one type). But the rest is different. This will be shown in the section on function arguments (spoiler "passing arrays by reference").
It is worth noting that string, wstring, dstring are just alias for immutable arrays of corresponding characters.
Pointers
WARNING! there is a behavior different from C / C ++
const char * a; // const(char*), , C/C++ const(char)* b; // const(char)*, const char * C/C++ const(char*) c; // const(char*), ,
And it is worth noting that there are no constructs in the language that create a constant pointer to non-constant memory, as could be done in C / C ++.
char * const c;
In general, the declaration of pointers and the rules for the distribution of modifiers are similar to those described for arrays (* instead of [] and everything).
WARNING! there is a behavior different from C / C ++
You should always remember that * (and []) in D explicitly refer to the data type and not to the variable identifier:
int* a, b;
In D, there is no way to declare variables of different types in one declaration; if you need a number and a pointer, then there will be 2 different declarations.
int a; int* b;
Functions and Arguments
Let's start with the const and immutable arguments:
import std.stdio; class A { int val; } void func1( const A a ) { writeln( a.val ); } void func2( immutable A a ) { writeln( a.val ); } void main() { auto a = new A;
And so we see that immutable in this case is not the same as const. When we declare a const argument, we guarantee that within the function this argument will not change. In the case of immutable, we guarantee that the argument will never change after initialization. The last statement allows you to use immutable variables as shared in other threads, since they are still immutable (never under any circumstances).
There is a slippery moment: if we replace the class with the struct (and, accordingly, initialize the variable not as new A, but as A.init), then the code will work. This is explained by the fact that structures, numerical types, static arrays are passed by value, and classes, dynamic and associative arrays are passed by reference. And when passed by value, a copy is created, which implicitly can be reduced to the desired type.
Types that are passed by value can be passed by reference:
import std.stdio; struct A { int val; } void func0( ref A a ) { writeln( a.val ); } void func1( ref const A a ) { writeln( a.val ); } void func2( ref immutable A a ) { writeln( a.val ); } void main() { auto a = A.init; func0( a ); func1( a );
Passing arrays by reference void main() { void fnc_a( ref const(shared(int)[]) a ) {} void fnc_b( ref shared(const(int)[]) a ) {} void fnc_c( ref const(shared int[]) a ) {} const(shared(int)[]) a = [1]; shared(const(int)[]) b = [2]; const(shared int[]) c = [3]; fnc_a( a );
The array is a thick pointer (in D arrays store the size of the array and the pointer), and this pointer is copied to the function when passed to the function and can be converted to the desired type when copied, as with ordinary numbers. But links are no longer implicitly cited. The exception in the example is a call to a function that takes ref const (shared int []) with the argument shared (const (int) []), but everything is logical: the type of elements inside shared (const (int)), and the array itself is shared, and accepted by const. In essence, the exception is that a simple argument can be passed to a function that expects a constant reference. But with immutable it is no longer a ride. But in conjunction with shared other combinations are possible:
void main() { void fnc_a( ref immutable(shared(int)[]) a ) {} void fnc_b( ref shared(immutable(int)[]) a ) {} void fnc_c( ref immutable(shared int[]) a ) {} immutable(shared(int)[]) a = [1]; shared(immutable(int)[]) b = [2]; immutable(shared int[]) c = [3]; fnc_a( a );
Since in this case the type of the variable a and c coincides: immutable (int []). The immutable modifier "eats" all the combinations inside.
If you want to write a function that works with different links, then const will do, but if you want to return the corresponding type depending on the argument, without using metaprogramming, inout will suit you:
import std.stdio; inout(int)[] func( inout(int)[] a ) { return a[2..4]; } void main() { auto a = [ 1,2,3,4,5 ]; auto af = func(a); static assert( is( typeof(af) == int[] ) ); const(int)[] b = [ 1,2,3,4,5 ]; auto bf = func(b); static assert( is( typeof(bf) == const(int)[] ) ); immutable(int)[] c = [ 1,2,3,4,5 ]; auto cf = func(c); static assert( is( typeof(cf) == immutable(int)[] ) ); }
For cases when the argument passed by reference works for the output (to write the result to it, we do not care about the initial value) there is a special keyword out:
struct A { int val; } void func( out A a ) { }
During a call to func, the variable a is assigned the value A.init (initializing the value for the data type).
You may want to pass an argument by reference, with the guarantee that it will not be changed. At first it may seem that the keyword in for it exists, but it is not, in is an abbreviation for the const scope, so you should be verbose about what you want:
void func( ref const int v ) {}
This is useful when transferring large structures, in order to avoid the overhead of copying. But such an entry will not work with rvalue values, in this case it will not be possible to call func (5), since the literal has no address (this also applies to structures created at the time of the function exit). Unfortunately, this can be bypassed only in one way - using templates:
void func(T)( auto ref const T v ) if( is(T==int) ){}
The auto ref construct will allow the function to be instantiated as to accept the link, and if it is not possible, then to accept a copy of the argument. The if (is (T == int)) signature restriction construction allows you to instantiate a function only when the condition inside is met (in our case, this is a T type identity condition with an int), is always compile-time. In fact, 2 different functions are instantiated for links and for copying.
Not goodThe auto ref construct for the return type works with normal functions, as will be shown later. Developers are
discussing the problem, and even have a
solution . Here everything is not so simple and unambiguous, as it may seem.
In D, as in many languages there is a lazy calculation of arguments (the calculation of an argument only at the moment when it is used) of the function:
import std.stdio; void foo( bool x, lazy string str ) { writeln( "foo call" ); if( x ) writeln( str ); } string bar() { writeln( "build string" ); return "hello habr"; } void main() { writeln( "x = false" ); foo( false, bar() ); writeln( "x = true" ); foo( true, bar() ); }
will lead
x = false foo call x = true foo call build string hello habr
A complete list of storage classes of arguments:
- no - argument as mutable copy
- in - same as const scope
- out - pass by reference with initialization by default
- ref - just passing by reference
- scope - links inside such a parameter cannot be “released outside” (escaped), for example, assigned to a global variable *
- lazy - the argument is calculated only at the moment when it is used in the function body
- const - the argument is implicitly cast to const type
- immutable - the argument is implicitly cast to the immutable type
- shared - the argument is implicitly cast to the shared type.
- inout - the argument is implicitly cast to the inout type.
about scopeIn fact, I did not understand what he was doing. The documentation is written:
scope - references in the parameter cannot be escaped (eg assigned to a global variable)
What is denied by the performance of such a code:
int* glob1; int* glob2; struct A { int val; int* ptr; } void func( scope A a ) { glob1 = &(a.val); glob2 = a.ptr; } void main() { auto val = 10; auto a = A(5,&val); func( a ); assert( &val != &(a.val) );
Maybe I misunderstood something? Maybe they cut the implementation of this behavior because they want to make the scope deprecated keyword. Maybe this is just a bug.
Of course, the auto keyword can be used to calculate the return value:
auto func( int a ) { return a * 2; }
With multiple returns, the enclosing type is computed:
auto func( int a, double b ) // double { if( a > b ) return a; else if( b > a ) return b; else return 0UL; }
For the case when you want to return a reference, you can use the auto ref return type.
class A { auto ref int foo( ref int x ) { return 3; } } class B : A { override auto ref int foo( ref int x ) { return x; } } class C : A { int k; override auto ref int foo( ref int x ) { return k; } } void main() { auto a = new A; auto b = new B; auto c = new C; int val = 10;
An example with OOP is given, because I do not quite understand why using auto ref outside this context, if you have a good, simple example illustrating the need for auto ref for normal functions, I will be happy to add it.
In the second part we will talk about @ safe, pure, nothrow and some other aspects.
Here I could forget something important (implicit for newcomers to the language), so I’ll add to the commentators well.
UPD : added pro pointers