Structs, Unions

Contents
  1. Struct Layout
  2. Plain Old Data
  3. Opaque Structs and Unions
  4. Default Initialization of Structs
  5. Static Initialization of Structs
  6. Default Initialization of Unions
  7. Static Initialization of Unions
  8. Dynamic Initialization of Structs
  9. Struct Literals
  10. Struct Properties
  11. Struct Instance Properties
  12. Struct Field Properties
  13. Const, Immutable and Shared Structs
  14. Union Constructors
  15. Struct Constructors
    1. Delegating Constructors
    2. Struct Instantiation
    3. Constructor Attributes
    4. Disabling Default Struct Construction
    5. Field initialization inside a constructor
    6. Struct Copy Constructors
  16. Struct Postblits
  17. Struct Destructors
  18. Struct Invariants
  19. Identity Assignment Overload
  20. Nested Structs
  21. Unions and Special Member Functions

Whereas classes are reference types, structs are value types. Structs and unions are simple aggregations of data and their associated operations on that data.

AggregateDeclaration:
    ClassDeclaration
    InterfaceDeclaration
    StructDeclaration
    UnionDeclaration

StructDeclaration:
    struct Identifier ;
    struct Identifier AggregateBody
    StructTemplateDeclaration
    AnonStructDeclaration

AnonStructDeclaration:
    struct AggregateBody

UnionDeclaration:
    union Identifier ;
    union Identifier AggregateBody
    UnionTemplateDeclaration
    AnonUnionDeclaration

AnonUnionDeclaration:
    union AggregateBody

AggregateBody:
    { DeclDefsopt }

A struct is defined to not have an identity; that is, the implementation is free to make bit copies of the struct as convenient.

Structs and unions may not contain an instance of themselves, however, they may contain a pointer to the same type.

Best Practices:
  1. Bit fields are supported with the bitfields template.

Struct Layout

The non-static data members of a struct are called fields. Fields are laid out in lexical order. Fields are aligned according to the Align Attribute in effect. Unnamed padding is inserted between fields to align fields. There is no padding between the first field and the start of the object.

Structs with no fields of non-zero size (aka Empty Structs) have a size of one byte.

Non-static function-nested D structs, which access the context of their enclosing scope, have an extra field.

Implementation Defined:
  1. The default layout of the fields of a struct is an exact match with the associated C compiler.
  2. g++ and clang++ differ in how empty structs are handled. Both return 1 from sizeof, however, clang++ does not push them onto the parameter stack while g++ does. This is a binary incompatibility between g++ and clang++. dmd follows clang++ behavior for OSX and FreeBSD, and g++ behavior for Linux and other Posix platforms.
  3. clang and gcc both return 0 from sizeof for empty structs. Using extern "C++" in clang++ and g++ does not cause them to conform to the behavior of their respective C compilers.
Undefined Behavior:
  1. The padding data can be accessed, but its contents are undefined.
  2. Do not pass or return structs with no fields of non-zero size to extern (C) functions. According to C11 6.7.2.1p8 this is undefined behavior.
Best Practices:
  1. When laying out a struct to match an externally defined layout, use align attributes to describe an exact match. Using a Static Assert to ensure the result is as expected.
  2. Although the contents of the padding are often zero, do not rely on that.
  3. Avoid using empty structs when interfacing with C and C++ code.
  4. Avoid using empty structs as parameters or arguments to variadic functions.

Plain Old Data

A struct or union is Plain Old Data (POD) if it meets the following criteria:

  1. it is not nested
  2. it has no postblits, copy constructors, destructors, or assignment operators
  3. it has no ref fields or fields that are themselves non-POD
Best Practices: Structs or unions that interface with C code should be POD.

Opaque Structs and Unions

Opaque struct and union declarations do not have a AggregateBody:

struct S;
union U;
struct V(T);
union W(T);

The members are completely hidden to the user, and so the only operations on those types are ones that do not require any knowledge of the contents of those types. For example:

struct S;
S.sizeof; // error, size is not known
S s;      // error, cannot initialize unknown contents
S* p;     // ok, knowledge of members is not necessary
Best Practices: They can be used to implement the PIMPL idiom.

Default Initialization of Structs

Struct fields are by default initialized to whatever the Initializer for the field is, and if none is supplied, to the default initializer for the field's type.

struct S { int a = 4; int b; }
S x; // x.a is set to 4, x.b to 0

The default initializers are evaluated at compile time.

The default initializers may not contain references to mutable data.

Static Initialization of Structs

If a StructInitializer is supplied, the fields are initialized by the StructMemberInitializer syntax. StructMemberInitializers with the Identifier : NonVoidInitializer syntax may be appear in any order, where Identifier is the field identifier. StructMemberInitializers with the NonVoidInitializer syntax appear in the lexical order of the fields in the StructDeclaration.

Fields not specified in the StructInitializer are default initialized.

struct S { int a, b, c, d = 7; }
S r;                          // r.a = 0, r.b = 0, r.c = 0, r.d = 7
S s = { a:1, b:2 };           // s.a = 1, s.b = 2, s.c = 0, s.d = 7
S t = { c:4, b:5, a:2, d:5 }; // t.a = 2, t.b = 5, t.c = 4, t.d = 5
S u = { 1, 2 };               // u.a = 1, u.b = 2, u.c = 0, u.d = 7
S v = { 1, d:3 };             // v.a = 1, v.b = 0, v.c = 0, v.d = 3
S w = { b:1, 3 };             // w.a = 0, w.b = 1, w.c = 3, w.d = 7

Initializing a field more than once is an error:

S x = { 1, a:2 };  // error: duplicate initializer for field `a`

Default Initialization of Unions

Unions are by default initialized to whatever the Initializer for the first field is, and if none is supplied, to the default initializer for the first field's type.

If the union is larger than the first field, the remaining bits are set to 0.

union U { int a = 4; long b; }
U x; // x.a is set to 4, x.b to an implementation-defined value

union V { int a; long b = 4; }
V y; // y.a is set to 0, y.b to an implementation-defined value
union W { int a = 4; long b = 5; } // error: overlapping default initialization for `a` and `b`

The default initializer is evaluated at compile time.

Implementation Defined: The values the fields other than the default initialized field are set to.

Static Initialization of Unions

Unions are initialized similarly to structs, except that only one initializer is allowed.

union U { int a; double b; }
U u = { 2 };       // u.a = 2
U v = { b : 5.0 }; // v.b = 5.0
U w = { 2, 3 };    // error: overlapping initialization for field `a` and `b`

If the union is larger than the initialized field, the remaining bits are set to 0.

Implementation Defined: The values the fields other than the initialized field are set to.

Dynamic Initialization of Structs

The static initializer syntax can also be used to initialize non-static variables. The initializer need not be evaluable at compile time.

struct S { int a, b, c, d = 7; }

void test(int i)
{
    S q = { 1, b:i }; // q.a = 1, q.b = i, q.c = 0, q.d = 7
}

Structs can be dynamically initialized from another value of the same type:

struct S { int a; }
S t;      // default initialized
t.a = 3;
S s = t;  // s.a is set to 3

If opCall is overridden for the struct, and the struct is initialized with a value that is of a different type, then the opCall operator is called:

struct S
{
    int a;

    static S opCall(int v)
    {
        S s;
        s.a = v;
        return s;
    }

    static S opCall(S v)
    {
        assert(0);
    }
}

S s = 3; // sets s.a to 3 using S.opCall(int)
S t = s; // sets t.a to 3, S.opCall(S) is not called

Struct Literals

Struct literals consist of the name of the struct followed by a parenthesized argument list:

struct S { int x; float y; }

int foo(S s) { return s.x; }

foo( S(1, 2) ); // set field x to 1, field y to 2

Struct literals are syntactically like function calls. If a struct has a member function named opCall, then struct literals for that struct are not possible. See also opCall operator overloading for the issue workaround. It is an error if there are more arguments than fields of the struct. If there are fewer arguments than fields, the remaining fields are initialized with their respective default initializers. If there are anonymous unions in the struct, only the first member of the anonymous union can be initialized with a struct literal, and all subsequent non-overlapping fields are default initialized.

Struct Properties

Struct Properties
Name Description
.sizeof Size in bytes of struct
.alignof Size boundary struct needs to be aligned on

Struct Instance Properties

Struct Instance Properties
Name Description
.tupleof An expression sequence of all struct fields - see Class Properties for a class-based example.

Struct Field Properties

Struct Field Properties
Name Description
.offsetof Offset in bytes of field from beginning of struct

Const, Immutable and Shared Structs

A struct declaration can have a storage class of const, immutable or shared. It has an equivalent effect as declaring each member of the struct as const, immutable or shared.

const struct S { int a; int b = 2; }
void main()
{
    S s = S(3); // initializes s.a to 3
    S t;        // initializes t.a to 0
    t = s;      // error, t.a and t.b are const, so cannot modify them.
    t.a = 4;    // error, t.a is const
}

Union Constructors

Unions are constructed in the same way as structs.

Struct Constructors

Struct constructors are used to initialize an instance of a struct when a more complex construction is needed than is allowed by static initialization or a struct literal.

Constructors are defined with a function name of this and have no return value. The grammar is the same as for the class Constructor.

A struct constructor is called by the name of the struct followed by Parameters.

If the ParameterList is empty, the struct instance is default initialized.

struct S
{
    int x, y = 4, z = 6;
    this(int a, int b)
    {
        x = a;
        y = b;
    }
}

void main()
{
    S a = S(4, 5); // calls S.this(4, 5):  a.x = 4, a.y = 5, a.z = 6
    S b = S();  // default initialized:    b.x = 0, b.y = 4, b.z = 6
    S c = S(1); // error, matching this(int) not found
}

A default constructor (i.e. one with an empty ParameterList) is not allowed.

struct S
{
    int x;
    this() { } // error, struct default constructor not allowed
}

Delegating Constructors

A constructor can call another constructor for the same struct in order to share common initializations. This is called a delegating constructor:

struct S
{
    int j = 1;
    long k = 2;
    this(long k)
    {
        this.k = k;
    }
    this(int i)
    {
        // At this point: j=1, k=2
        this(6); // delegating constructor call
        // At this point: j=1, k=6
        j = i;
        // At this point: j=i, k=6
    }
}

The following restrictions apply:

  1. If a constructor's code contains a delegating constructor call, all possible execution paths through the constructor must make exactly one delegating constructor call:
    struct S
    {
        int a;
        this(int i) { }
    
        this(char c)
        {
            c || this(1); // error, not on all paths
        }
    
        this(wchar w)
        {
            (w) ? this(1) : this('c'); } // ok
    
        this(byte b)
        {
            foreach (i; 0 .. b)
            {
                this(1);  // error, inside loop
            }
        }
    }
    
  2. It is illegal to refer to this implicitly or explicitly prior to making a delegating constructor call.
  3. Once the delegating constructor returns, all fields are considered constructed.
  4. Delegating constructor calls cannot appear after labels.

See also: delegating class constructors.

Struct Instantiation

When an instance of a struct is created, the following steps happen:

  1. The raw data is statically initialized using the values provided in the struct definition. This operation is equivalent to doing a memory copy of a static version of the object onto the newly allocated one.
  2. If there is a constructor defined for the struct, the constructor matching the argument list is called.
  3. If struct invariant checking is turned on, the struct invariant is called at the end of the constructor.

Constructor Attributes

A constructor qualifier (const, immutable or shared) constructs the object instance with that specific qualifier.

struct S1
{
    int[] a;
    this(int n) { a = new int[](n); }
}
struct S2
{
    int[] a;
    this(int n) immutable { a = new int[](n); }
}
void main()
{
    // Mutable constructor creates mutable object.
    S1 m1 = S1(1);

    // Constructed mutable object is implicitly convertible to const.
    const S1 c1 = S1(1);

    // Constructed mutable object is not implicitly convertible to immutable.
    immutable i1 = S1(1); // error

    // Mutable constructor cannot construct immutable object.
    auto x1 = immutable S1(1); // error


    // Immutable constructor creates immutable object.
    immutable i2 = immutable S2(1);

    // Immutable constructor cannot construct mutable object.
    auto x2 = S2(1); // error

    // Constructed immutable object is not implicitly convertible to mutable.
    S2 m2 = immutable S2(1); // error

    // Constructed immutable object is implicitly convertible to const.
    const S2 c2 = immutable S2(1);
}

Constructors can be overloaded with different attributes.

struct S
{
    this(int);           // non-shared mutable constructor
    this(int) shared;    // shared mutable constructor
    this(int) immutable; // immutable constructor
}

S m = S(1);
shared s = shared S(2);
immutable i = immutable S(3);

Pure Constructors

If the constructor can create a unique object (i.e. if it is pure), the object is implicitly convertible to any qualifiers.

struct S
{
    this(int) pure;
    // Based on the definition, this creates a mutable object. But the
    // created object cannot contain any mutable global data.
    // Therefore the created object is unique.

    this(int[] arr) immutable pure;
    // Based on the definition, this creates an immutable object. But
    // the argument int[] never appears in the created object so it
    // isn't implicitly convertible to immutable. Also, it cannot store
    // any immutable global data.
    // Therefore the created object is unique.
}

immutable i = immutable S(1); // this(int) pure is called
shared s = shared S(1);       // this(int) pure is called
S m = S([1,2,3]);             // this(int[]) immutable pure is called

Disabling Default Struct Construction

If a struct constructor is annotated with @disable and has an empty ParameterList, the struct has disabled default construction. The only way it can be constructed is via a call to another constructor with a non-empty ParameterList.

A struct with a disabled default constructor, and no other constructors, cannot be instantiated other than via a VoidInitializer.

A disabled default constructor may not have a FunctionBody.

If any fields have disabled default construction, struct default construction is also disabled.

struct S
{
    int x;

    // Disables default construction
    @disable this();

    this(int v) { x = v; }
}
struct T
{
    int y;
    S s;
}
void main()
{
    S s;          // error: default construction is disabled
    S t = S();    // error: also disabled
    S u = S(1);   // constructed by calling `S.this(1)`
    S v = void;   // not initialized, but allowed
    S w = { 1 };  // error: cannot use { } since constructor exists
    S[3] a;       // error: default construction is disabled
    S[3] b = [S(1), S(20), S(-2)]; // ok
    T t;          // error: default construction is disabled
}
Best Practices: Disabling default construction is useful when the default value, such as null, is not acceptable.

Field initialization inside a constructor

In a constructor body, if a delegating constructor is called, all field assignments are considered assignments. Otherwise, the first instance of field assignment is its initialization, and assignments of the form field = expression are treated as equivalent to typeof(field)(expression). The values of fields may be read before initialization or construction with a delegating constructor.

struct S
{
    int num;
    int ber;
    this(int i)
    {
        num = i + 1;   // initialization
        num = i + 2;   // assignment
        ber = ber + 1; // ok to read before initialization
    }
    this(int i, int j)
    {
        this(i);
        num = i + 1;  // assignment
    }
}

If the field type has an opAssign method, it will not be used for initialization.

struct A
{
    this(int n) {}
    void opAssign(A rhs) {}
}
struct S
{
    A val;
    this(int i)
    {
        val = A(i);  // val is initialized to the value of A(i)
        val = A(2);  // rewritten to val.opAssign(A(2))
    }
}

If the field type is not mutable, multiple initialization will be rejected.

struct S
{
    immutable int num;
    this(int)
    {
        num = 1;  // OK
        num = 2;  // Error: assignment to immutable
    }
}

If the field is initialized on one path, it must be initialized on all paths.

struct S
{
    immutable int num;
    immutable int ber;
    this(int i)
    {
        if (i)
            num = 3;   // initialization
        else
            num = 4;   // initialization
    }
    this(long j)
    {
        j ? (num = 3) : (num = 4); // ok
        j || (ber = 3);  // Error: initialized on only one path
        j && (ber = 3);  // Error: initialized on only one path
    }
}

A field initialization may not appear in a loop or after a label.

struct S
{
    immutable int num;
    immutable string str;
    this(int j)
    {
        foreach (i; 0..j)
        {
            num = 1;    // Error: field initialization not allowed in loops
        }
        size_t i = 0;
    Label:
        str = "hello";  // Error: field initialization not allowed after labels
        if (i++ < 2)
            goto Label;
    }
    this(int j, int k)
    {
        switch (j)
        {
            case 1: ++j; break;
            default: break;
        }
        num = j;        // Error: `case` and `default` are also labels
    }
}

If a field's type has disabled default construction, then it must be initialized in the constructor.

struct S { int y; @disable this(); }

struct T
{
    S s;
    this(S t) { s = t; }       // ok
    this(int i) { this('c'); } // ok
    this(char) { }             // Error: s not initialized
}

Struct Copy Constructors

Copy constructors are used to initialize a struct instance from another struct of the same type.

A constructor declaration is a copy constructor declaration if and only if it is a constructor declaration that takes only one non-default parameter by reference that is of the same type as typeof(this), followed by any number of default parameters:

struct A
{
    this(ref return scope A rhs) {}                        // copy constructor
    this(ref return scope const A rhs, int b = 7) {}       // copy constructor with default parameter
}

The copy constructor is type checked as a normal constructor.

If a copy constructor is defined, implicit calls to it will be inserted in the following situations:

  1. When a variable is explicitly initialized:
  2. struct A
    {
        this(ref return scope A rhs) {}
    }
    
    void main()
    {
        A a;
        A b = a; // copy constructor gets called
    }
    
  3. When a parameter is passed by value to a function:
  4. struct A
    {
        this(ref return scope A another) {}
    }
    
    void fun(A a) {}
    
    void main()
    {
        A a;
        fun(a);    // copy constructor gets called
    }
    
  5. When a parameter is returned by value from a function and Named Returned Value Optiomization (NRVO) cannot be performed:
  6. struct A
    {
        this(ref return scope A another) {}
    }
    
    A fun()
    {
        A a;
        return a;       // NRVO, no copy constructor call
    }
    
    A a;
    A gun()
    {
        return a;       // cannot perform NRVO, rewrite to: return (A __tmp; __tmp.copyCtor(a));
    }
    
    void main()
    {
        A a = fun();
        A b = gun();
    }
    

When a copy constructor is defined for a struct (or marked @disable), the compiler no longer implicitly generates default copy/blitting constructors for that struct:

struct A
{
    int[] a;
    this(ref return scope A rhs) {}
}

void fun(immutable A) {}

void main()
{
    immutable A a;
    fun(a);        // error: copy constructor cannot be called with types (immutable) immutable
}

If a union S has fields that define a copy constructor, whenever an object of type S is initialized by copy, an error will be issued. The same rule applies to overlapped fields (anonymous unions).

A struct that defines a copy constructor is not a POD.

Copy Constructor Attributes

The copy constructor can be overloaded with different qualifiers applied to the parameter (copying from a qualified source) or to the copy constructor itself (copying to a qualified destination):

struct A
{
    this(ref return scope A another) {}                        // 1 - mutable source, mutable destination
    this(ref return scope immutable A another) {}              // 2 - immutable source, mutable destination
    this(ref return scope A another) immutable {}              // 3 - mutable source, immutable destination
    this(ref return scope immutable A another) immutable {}    // 4 - immutable source, immutable destination
}

void main()
{
    A a;
    immutable A ia;

    A a2 = a;      // calls 1
    A a3 = ia;     // calls 2
    immutable A a4 = a;     // calls 3
    immutable A a5 = ia;    // calls 4
}

The inout qualifier may be applied to the copy constructor parameter in order to specify that mutable, const, or immutable types are treated the same:

struct A
{
    this(ref return scope inout A rhs) immutable {}
}

void main()
{
    A r1;
    const(A) r2;
    immutable(A) r3;

    // All call the same copy constructor because `inout` acts like a wildcard
    immutable(A) a = r1;
    immutable(A) b = r2;
    immutable(A) c = r3;
}

Implicit Copy Constructors

A copy constructor is generated implicitly by the compiler for a struct S if all of the following conditions are met:

  1. S does not explicitly declare any copy constructors;
  2. S defines at least one direct member that has a copy constructor, and that member is not overlapped (by means of union) with any other member.

If the restrictions above are met, the following copy constructor is generated:

this(ref return scope inout(S) src) inout
{
    foreach (i, ref inout field; src.tupleof)
        this.tupleof[i] = field;
}

If the generated copy constructor fails to type check, it will receive the @disable attribute.

Struct Postblits

Postblit:
    this ( this ) MemberFunctionAttributesopt ;
    this ( this ) MemberFunctionAttributesopt FunctionBody

WARNING: The postblit is considered legacy and is not recommended for new code. Code should use copy constructors defined in the previous section. For backward compatibility reasons, a struct that explicitly defines both a copy constructor and a postblit will only use the postblit for implicit copying. However, if the postblit is disabled, the copy constructor will be used. If a struct defines a copy constructor (user-defined or generated) and has fields that define postblits, a deprecation will be issued, informing that the postblit will have priority over the copy constructor.

Copy construction is defined as initializing a struct instance from another struct of the same type. Copy construction is divided into two parts:

  1. blitting the fields, i.e. copying the bits
  2. running postblit on the result

The first part is done automatically by the language, the second part is done if a postblit function is defined for the struct. The postblit has access only to the destination struct object, not the source. Its job is to ‘fix up’ the destination as necessary, such as making copies of referenced data, incrementing reference counts, etc. For example:

struct S
{
    int[] a;    // array is privately owned by this instance
    this(this)
    {
        a = a.dup;
    }
}

Disabling struct postblit makes the object not copyable.

struct T
{
    @disable this(this);  // disabling makes T not copyable
}
struct S
{
    T t;   // uncopyable member makes S also not copyable
}

void main()
{
    S s;
    S t = s; // error, S is not copyable
}

Depending on the struct layout, the compiler may generate the following internal postblit functions:

  1. void __postblit(). The compiler assigns this name to the explicitly defined postblit this(this) so that it can be treated exactly as a normal function. Note that if a struct defines a postblit, it cannot define a function named __postblit - no matter the signature - as this would result in a compilation error due to the name conflict.
  2. void __fieldPostblit(). If a struct X has at least one struct member that in turn defines (explicitly or implicitly) a postblit, then a field postblit is generated for X that calls all the underlying postblits of the struct fields in declaration order.
  3. void __aggrPostblit(). If a struct has an explicitly defined postblit and at least 1 struct member that has a postblit (explicit or implicit) an aggregated postblit is generated which calls __fieldPostblit first and then __postblit.
  4. void __xpostblit(). The field and aggregated postblits, although generated for a struct, are not actual struct members. In order to be able to call them, the compiler internally creates an alias, called __xpostblit which is a member of the struct and which points to the generated postblit that is the most inclusive.
// struct with alias __xpostblit = __postblit
struct X
{
    this(this) {}
}

// struct with alias __xpostblit = __fieldPostblit
// which contains a call to X.__xpostblit
struct Y
{
    X a;
}

// struct with alias __xpostblit = __aggrPostblit which contains
// a call to Y.__xpostblit and a call to Z.__postblit
struct Z
{
    Y a;
    this(this) {}
}

void main()
{
    // X has __postblit and __xpostblit (pointing to __postblit)
    static assert(__traits(hasMember, X, "__postblit"));
    static assert(__traits(hasMember, X, "__xpostblit"));

    // Y does not have __postblit, but has __xpostblit (pointing to __fieldPostblit)
    static assert(!__traits(hasMember, Y, "__postblit"));
    static assert(__traits(hasMember, Y, "__xpostblit"));
    // __fieldPostblit is not a member of the struct
    static assert(!__traits(hasMember, Y, "__fieldPostblit"));

    // Z has  __postblit and __xpostblit (pointing to __aggrPostblit)
    static assert(__traits(hasMember, Z, "__postblit"));
    static assert(__traits(hasMember, Z, "__xpostblit"));
    // __aggrPostblit is not a member of the struct
    static assert(!__traits(hasMember, Z, "__aggrPostblit"));
}

Neither of the above postblits is defined for structs that don't define this(this) and don't have fields that transitively define it. If a struct does not define a postblit (implicit or explicit) but defines functions that use the same name/signature as the internally generated postblits, the compiler is able to identify that the functions are not actual postblits and does not insert calls to them when the struct is copied. Example:

struct X
{}

int a;

struct Y
{
    int a;
    X b;
    void __fieldPostPostblit()
    {
        a = 42;
    }
}

void main()
{
    static assert(!__traits(hasMember, X, "__postblit"));
    static assert(!__traits(hasMember, X, "__xpostblit"));

    static assert(!__traits(hasMember, Y, "__postblit"));
    static assert(!__traits(hasMember, Y, "__xpostblit"));

    Y y;
    auto y2 = y;
    assert(a == 0); // __fieldPostBlit does not get called
}

Postblits cannot be overloaded. If two or more postblits are defined, even if the signatures differ, the compiler assigns the __postblit name to both and later issues a conflicting function name error:

struct X
{
    this(this) {}
    this(this) const {} // error: function X.__postblit conflicts with function X.__postblit
}

The following describes the behavior of the qualified postblit definitions:

  1. const. When a postblit is qualified with const as in this(this) const; or const this(this); then the postblit is successfully called on mutable (unqualified), const, and immutable objects, but the postblit cannot modify the object because it regards it as const; hence const postblits are of limited usefulness. Example:
  2. struct S
    {
        int n;
        this(this) const
        {
            import std.stdio : writeln;
            writeln("postblit called");
            //++n; // error: cannot modify this.n in `const` function
        }
    }
    
    void main()
    {
        S s1;
        auto s2 = s1;
        const S s3;
        auto s4 = s3;
        immutable S s5;
        auto s6 = s5;
    }
    
  3. immutable. When a postblit is qualified with immutable as in this(this) immutable or immutable this(this) the code is ill-formed. The immutable postblit passes the compilation phase but cannot be invoked. Example:
  4. struct Y
    {
        // not invoked anywhere, no error is issued
        this(this) immutable
        { }
    }
    
    struct S
    {
        this(this) immutable
        { }
    }
    
    void main()
    {
        S s1;
        auto s2 = s1;    // error: immutable method `__postblit` is not callable using a mutable object
        const S s3;
        auto s4 = s3;    // error: immutable method `__postblit` is not callable using a mutable object
        immutable S s5;
        auto s6 = s5;    // error: immutable method `__postblit` is not callable using a mutable object
    }
    
  5. shared. When a postblit is qualified with shared as in this(this) shared or shared this(this) solely shared objects may invoke the postblit; attempts of postbliting unshared objects will result in compile time errors:
  6. struct S
    {
        this(this) shared
        { }
    }
    
    void main()
    {
        S s1;
        auto s2 = s1;    // error: shared method `__postblit` is not callable using a non-shared object
        const S s3;
        auto s4 = s3;    // error: shared method `__postblit` is not callable using a non-shared object
        immutable S s5;
        auto s6 = s5;    // error: shared method `__postblit` is not callable using a non-shared object
    
        // calling the shared postblit on a shared object is accepted
        shared S s7;
        auto s8 = s7;
    }
    

An unqualified postblit will get called even if the struct is instantiated as immutable or const, but the compiler issues an error if the struct is instantiated as shared:

struct S
{
    int n;
    this(this) { ++n; }
}

void main()
{
    immutable S a;      // shared S a; => error : non-shared method is not callable using a shared object
    auto a2 = a;
    import std.stdio: writeln;
    writeln(a2.n);     // prints 1
}

From a postblit perspective, qualifiying the struct definition yields the same result as explicitly qualifying the postblit.

The following table lists all the possibilities of grouping qualifiers for a postblit associated with the type of object that needs to be used in order to successfully invoke the postblit:

Qualifier Groups
object type to be invoked on const immutable shared
any object type
uncallable
shared object
uncallable
shared object
uncallable
uncallable

Note that when const and immutable are used to explicitly qualify a postblit as in this(this) const immutable; or const immutable this(this); - the order in which the qualifiers are declared does not matter - the compiler generates a conflicting attribute error, however declaring the struct as const/immutable and the postblit as immutable/const achieves the effect of applying both qualifiers to the postblit. In both cases the postblit is qualified with the more restrictive qualifier, which is immutable.

The postblits __fieldPostblit and __aggrPostblit are generated without any implicit qualifiers and are not considered struct members. This leads to the situation where qualifying an entire struct declaration with const or immutable does not have any impact on the above-mentioned postblits. However, since __xpostblit is a member of the struct and an alias of one of the other postblits, the qualifiers applied to the struct will affect the aliased postblit.

struct S
{
    this(this)
    { }
}

// `__xpostblit` aliases the aggregated postblit so the `const` applies to it.
// However, the aggregated postblit calls the field postblit which does not have
// any qualifier applied, resulting in a qualifier mismatch error
const struct B
{
    S a;        // error : mutable method B.__fieldPostblit is not callable using a const object
    this(this)
    { }
}

// `__xpostblit` aliases the field postblit; no error
const struct B2
{
    S a;
}

// Similar to B
immutable struct C
{
    S a;        // error : mutable method C.__fieldPostblit is not callable using a immutable object
    this(this)
    { }
}

// Similar to B2, compiles
immutable struct C2
{
    S a;
}

In the above situations the errors do not contain line numbers because the errors are regarding generated code.

Qualifying an entire struct as shared correctly propagates the attribute to the generated postblits:

shared struct A
{
    this(this)
    {
        import std.stdio : writeln;
        writeln("the shared postblit was called");
    }
}

struct B
{
    A a;
}

void main()
{
    shared B b1;
    auto b2 = b1;
}

Unions may have fields that have postblits. However, a union itself never has a postblit. Copying a union does not result in postblit calls for any fields. If those calls are desired, they must be inserted explicitly by the programmer:

struct S
{
    int count;
    this(this)
    {
        ++count;
    }
}

union U
{
    S s;
}

void main()
{
    U a = U.init;
    U b = a;
    assert(b.s.count == 0);
    b.s.__postblit;
    assert(b.s.count == 1);
}

Struct Destructors

Destructors are called when an object goes out of scope. Their purpose is to free up resources owned by the struct object.

Unions may have fields that have destructors. However, a union itself never has a destructor. When a union goes out of scope, destructors for it's fields are not called. If those calls are desired, they must be inserted explicitly by the programmer:

struct S
{
    ~this()
    {
        import std.stdio;
        writeln("S is being destructed");
    }
}

union U
{
    S s;
}

void main()
{
    import std.stdio;
    {
        writeln("entering first scope");
        U u = U.init;
        scope (exit) writeln("exiting first scope");
    }
    {
        writeln("entering second scope");
        U u = U.init;
        scope (exit)
        {
            writeln("exiting second scope");
            destroy(u.s);
        }
    }
}

Struct Invariants

StructInvariant:
    invariant ( ) BlockStatement
    invariant BlockStatement
    invariant ( AssertArguments ) ;

StructInvariants specify the relationships among the members of a struct instance. Those relationships must hold for any interactions with the instance from its public interface.

The invariant is in the form of a const member function. The invariant is defined to hold if all the AssertExpressions within the invariant that are executed succeed.

If the invariant does not hold, then the program enters an invalid state.

Any invariants for fields are applied before the struct invariant.

There may be multiple invariants in a struct. They are applied in lexical order.

StructInvariants must hold at the exit of the struct constructor (if any), and at the entry of the struct destructor (if any).

StructInvariants must hold at the entry and exit of all public or exported non-static member functions. The order of application of invariants is:

  1. preconditions
  2. invariant
  3. function body
  4. invariant
  5. postconditions

The invariant need not hold if the struct instance is implicitly constructed using the default .init value.

struct Date
{
    this(int d, int h)
    {
        day = d;    // days are 1..31
        hour = h;   // hours are 0..23
    }

    invariant
    {
        assert(1 <= day && day <= 31);
        assert(0 <= hour && hour < 24);
    }

  private:
    int day;
    int hour;
}

Public or exported non-static member functions cannot be called from within an invariant.

struct Foo
{
    public void f() { }
    private void g() { }

    invariant
    {
        f();  // error, cannot call public member function from invariant
        g();  // ok, g() is not public
    }
}
Undefined Behavior: happens if the invariant does not hold and execution continues. Implementation Defined:
  1. Whether the StructInvariant is executed at runtime or not. This is typically controlled with a compiler switch.
  2. The behavior when the invariant does not hold is typically the same as for when AssertExpressions fail.
Best Practices:
  1. Do not indirectly call exported or public member functions within a struct invariant, as this can result in infinite recursion.
  2. Avoid reliance on side effects in the invariant. as the invariant may or may not be executed.
  3. Avoid having mutable public fields of structs with invariants, as then the invariant cannot verify the public interface.

Identity Assignment Overload

While copy construction takes care of initializing an object from another object of the same type, assignment is defined as copying the contents of a source object over those of a destination object, calling the destination object's destructor if it has one in the process:

struct S { ... }  // S has postblit or destructor
S s;      // default construction of s
S t = s;  // t is copy-constructed from s
t = s;    // t is assigned from s

Struct assignment t=s is defined to be semantically equivalent to:

t.opAssign(s);

where opAssign is a member function of S:

ref S opAssign(ref S s)
{
    S tmp = this;   // bitcopy this into tmp
    this = s;       // bitcopy s into this
    tmp.__dtor();   // call destructor on tmp
    return this;
}

An identity assignment overload is required for a struct if one or more of these conditions hold:

  • it has a destructor
  • it has a postblit
  • it has a field with an identity assignment overload

If an identity assignment overload is required and does not exist, an identity assignment overload function of the type ref S opAssign(ref S) will be automatically generated.

A user-defined one can implement the equivalent semantics, but can be more efficient.

One reason a custom opAssign might be more efficient is if the struct has a reference to a local buffer:

struct S
{
    int[] buf;
    int a;

    ref S opAssign(ref const S s) return
    {
        a = s.a;
        return this;
    }

    this(this)
    {
        buf = buf.dup;
    }
}

Here, S has a temporary workspace buf[]. The normal postblit will pointlessly free and reallocate it. The custom opAssign will reuse the existing storage.

Nested Structs

A nested struct is a struct that is declared inside the scope of a function or a templated struct that has aliases to local functions as a template argument. Nested structs have member functions. It has access to the context of its enclosing scope (via an added hidden field).

void foo()
{
    int i = 7;
    struct SS
    {
        int x,y;
        int bar() { return x + i + 1; }
    }
    SS s;
    s.x = 3;
    s.bar(); // returns 11
}

A struct can be prevented from being nested by using the static attribute, but then of course it will not be able to access variables from its enclosing scope.

void foo()
{
    int i = 7;
    static struct SS
    {
        int x, y;
        int bar()
        {
            return i; // error, SS is not a nested struct
        }
    }
}

Unions and Special Member Functions

Unions may not have postblits, destructors, or invariants.

© 1999–2021 The D Language Foundation
Licensed under the Boost License 1.0.
https://dlang.org/spec/struct.html