구조체와 유니온

구조체와 유니온 (Structs, Unions)

클래스가 참조 타입(reference types)인 반면, struct와 union은 값 타입(value types)이에요. struct는 데이터와 그 데이터에 대한 연산의 단순한 집합이며, union은 중복 저장 공간(overlapping storage)을 공유하는 필드들을 가져요. 이 문서는 struct/union의 선언, 레이아웃, 비트 필드, 초기화, 리터럴, 생성자·복사 생성자·이동 생성자·postblit·소멸자, 그리고 Alias This까지 다루어요.

출처: Structs, Unions

본문

클래스가 참조 타입인 반면, struct와 union은 값 타입이에요.

개요 (Overview)

Struct

StructDeclaration:
    struct Identifier ;
    struct Identifier AggregateBody
    StructTemplateDeclaration
    AnonStructDeclaration

AnonStructDeclaration:
    struct AggregateBody
AggregateBody:
    { DeclDefsopt }

Struct는 데이터와 그 데이터에 대한 연관 연산의 단순한 집합이에요. struct의 비정적 데이터 멤버를 필드(fields)라 불러요. struct 인스턴스의 멤버는 . 연산자로 접근해요. 다음 예제는 단일 정수 필드를 가진 struct 타입을 선언해요:

struct S
{
    int i;
}

void main()
{
    S a; // declare struct instance
    a.i = 3;

    S b = a; // copy a
    a.i++;
    assert(a.i == 4);
    assert(b.i == 3);
}

lvalue에서 struct 인스턴스를 할당(또는 초기화)하면 원래 struct를 복사해요. struct는 정체성(identity)을 갖지 않는 것으로 정의돼요. 즉 구현은 편의에 따라 struct의 비트 복사를 자유롭게 만들 수 있어요.

저장 공간 (Storage)

지역 변수의 경우 struct/union 인스턴스는 기본적으로 스택에 할당돼요. 힙에 할당하려면 new를 사용하며, 이는 포인터를 줘요. struct나 union에 대한 포인터는 . 연산자로 멤버에 접근할 때 자동으로 역참조돼요.

struct S { int i; }

S* p = new S;
S* q = p;

p.i = 2; // `p.i` is the same as `(*p).i`
assert(q.i == 2); // q points to the same struct instance as p
new 비활성화 (Disabling new)
NewDeclaration:
    new ( ) ;

@disable new();로 AggregateDeclaration에 new를 허용하지 않을 수 있어요:

struct S
{
    @disable new();
}

void main()
{
    S s; // OK
    S* p = new S; // Error, `new` is disabled
}

Note: 그런 애그리게이트 인스턴스는 여전히 메모리 버퍼에 emplace될 수 있어요.

Union

UnionDeclaration:
    union Identifier ;
    union Identifier AggregateBody
    UnionTemplateDeclaration
    AnonUnionDeclaration

AnonUnionDeclaration:
    union AggregateBody

struct는 순차적으로 저장되는 여러 필드를 포함할 수 있어요. 반대로 union의 여러 필드는 중복 저장 공간을 사용해요.

union U
{
    ubyte i;
    char c;
}

void main()
{
    U u;
    u.i = 3;
    assert(u.c == '\x03');
    u.c++;
    assert(u.i == 4);
}

멤버 (Members)

다음 표는 struct나 union이 포함할 수 있는 선언들을 보여줘요:

멤버 Struct Union
필드 (Fields)
비트 필드 (Bit Fields)
정적 필드 (Static fields)
익명 Struct/Union (Anonymous Structs and Unions)
멤버 함수 (Member Functions)
정적 멤버 함수
생성자 (Constructors)
복사 생성자 (Copy Constructors)
Postblit
소멸자 (Destructors)
불변식 (Invariants)
연산자 오버로딩 (Operator Overloading)
Alias This
기타 선언 (DeclDef 참조)

Union 제한 (Union Limitations)

Union은 postblit, 소멸자, 불변식을 가질 수 없어요. 소멸자가 있는 구성된(constructed) 필드는 수동으로 파괴해야 할 수 있어요.

재귀 Struct와 Union (Recursive Structs and Unions)

Struct와 union은 자신의 비정적 인스턴스를 포함할 수 없지만, 같은 타입에 대한 포인터는 포함할 수 있어요.

struct S
{
    S* ptr;     // OK
    S[] slice;  // OK

    S s;        // error
    S[2] array; // error

    static S global; // OK
}

Struct 레이아웃 (Struct Layout)

필드는 어휘적 순서로 배치돼요. 필드는 적용 중인 Align 속성에 따라 정렬돼요. 필드를 정렬하기 위해 필드 사이에 이름 없는 패딩이 삽입돼요. 첫 필드와 객체 시작 사이에는 패딩이 없어요.

0이 아닌 크기의 필드가 없는 (일명 Empty Struct) extern(D) struct는 크기가 1바이트예요.

extern(C) struct C {}
struct D {}

static assert(C.sizeof == 0);
static assert(D.sizeof == 1);

자신의 둘러싼 스코프의 컨텍스트에 접근하는 비정적 함수-중첩 D struct는 추가 필드를 가져요.

Implementation Defined: (구현 정의 사항) struct 필드의 기본 레이아웃은 연관 C 컴파일러와 정확히 일치해요. g++clang++는 빈 struct의 처리 방식이 달라요. 둘 다 sizeof에서 1을 반환하지만, clang++는 그것들을 매개변수 스택에 넣지 않는 반면 g++는 넣어요. 이는 g++와 clang++ 사이의 이진 비호환성(binary incompatibility)이에요. dmd는 OSX와 FreeBSD에서 clang++ 동작을, Linux와 다른 Posix 플랫폼에서 g++ 동작을 따라요. clanggcc는 둘 다 빈 struct에 대해 sizeof에서 0을 반환해요. clang++와 g++에서 extern "C++"를 사용해도 그들 각자의 C 컴파일러 동작을 따르도록 하지 않아요.

Undefined Behavior: (미정의 동작) 패딩 데이터에 접근할 수 있지만, 그 내용은 정의되지 않아요. 0이 아닌 크기의 필드가 없는 struct를 extern (C) 함수에 전달하거나 반환하지 마세요. C11 6.7.2.1p8에 따르면 이는 미정의 동작이에요.

Best Practices: (모범 사례) 외부에서 정의된 레이아웃과 일치하도록 struct를 배치할 때는 align 속성을 사용해 정확한 일치를 기술하세요. 결과가 예상대로인지 확인하려면 Static Assert를 사용하세요. 패딩 내용이 종종 0이지만, 그것에 의존하지 마세요. C 및 C++ 코드와 연동할 때 빈 struct를 피하세요. 가변 인자(variadic) 함수의 매개변수나 인자로 빈 struct를 사용하지 마세요.

비트 필드 선언 (Bit Field Declarations)

비트 필드는 BitfieldDeclarator로 선언돼요. 비트 필드 너비는 AssignExpression 또는 ConditionalExpression으로 지정돼요. 그것은 컴파일 타임에 평가되고, 0부터 비트 필드 타입의 비트 수까지의 정수여야 해요.

비트 필드는 Initializer를 가질 수 있는데, 이는 컴파일 타임 상수로 평가되어야 하며 비트 필드의 너비에 맞아야 해요.

비트 필드의 타입은 부호 있든 없든 정수 타입이어야 해요. 비트 필드에 할당되는 값은 지정된 너비에 맞아야 해요. 비트 필드는 비트 필드 타입의 값을 담기에 충분히 큰 메모리 단위에 배치돼요.

익명 비트 필드는 그것을 위한 Identifier를 갖지 않아요. 너비가 0인 비트 필드는 익명이어야 해요. 너비 0인 비트 필드는 다음 비트 필드가 다음 단위에 배치되게 해요.

비트 필드의 주소는 취할 수 없어요. ref 선언은 비트 필드로 초기화될 수 없어요. 비트 필드 배열은 허용되지 않아요. 비트 필드는 struct, union, class의 필드만 될 수 있어요. 비트 필드는 비정적이어야 해요.

struct B
{
    int x:3 = 2, y:2;
}

static assert(B.sizeof == 4);

int vaporator(B b)
{
    b.x = 4;
    b.y = 2;
    return b.x + b.y; // returns 6
}

Implementation Defined: (구현 정의 사항) 비트 필드를 읽고 쓰는 것은 같은 단위의 다른 비트 필드의 읽기·쓰기를 일으킬 수 있어요. 비동기 접근이 가능한 곳에서 비트 필드를 사용하면 신뢰할 수 없는 결과가 생겨요.

Implementation Defined: (구현 정의 사항) 비트 필드의 레이아웃은 구현 정의예요. 실제로는 연관 C 컴파일러 및 ImportC와 동일할 것으로 기대돼요.

Best Practices: (모범 사례) 데이터 이식성이 필요하면 std.bitmanip.bitfields를 사용하세요. 연관 C 컴파일러와의 데이터 호환이 필요하면 이 비트 필드를 사용하세요. 보장되지는 않지만, intuint 비트 필드를 고수하면 가장 큰 이진 호환성을 얻을 수 있어요.

Plain Old Data (POD)

struct나 union은 다음 기준을 충족하면 Plain Old Data (POD)예요:

  1. static이거나 중첩되지 않음
  2. postblit, 복사 생성자, 소멸자, 또는 할당 연산자가 없음
  3. 그 자체가 non-POD인 필드가 없음

Best Practices: (모범 사례) C 코드와 연동하는 struct나 union은 POD여야 해요.

불투명 Struct/Union (Opaque Structs and Unions)

불투명(opaque) struct와 union 선언은 AggregateBody가 없어요:

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

멤버들은 사용자에게 완전히 숨겨지므로, 그 타입에 대한 유일한 연산은 그 타입의 내용에 대한 지식을 요구하지 않는 것들이에요. 예를 들어:

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: (모범 사례) 이는 PIMPL 관용구를 구현하는 데 사용될 수 있어요.

초기화 (Initialization)

Struct의 기본 초기화 (Default Initialization of Structs)

기본적으로 struct 필드는 Initializer가 제공되면 그 값으로 초기화되고, 그렇지 않으면 기본 초기화(default initialized)돼요.

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

필드 Initializer는 컴파일 타임에 평가돼요. 중첩 struct의 경우 컨텍스트 포인터는 기본 초기화돼요.

Struct의 정적 초기화 (Static Initialization of Structs)

StructInitializer:
    { StructMemberInitializersopt }

StructMemberInitializers:
    StructMemberInitializer
    StructMemberInitializer ,
    StructMemberInitializer , StructMemberInitializers

StructMemberInitializer:
    NonVoidInitializer
    Identifier : NonVoidInitializer

StructInitializer가 제공되면, 각 StructMemberInitializer는 일치하는 필드를 초기화해요:

  • Identifier : NonVoidInitializer 문법을 사용하는 StructMemberInitializer는 어떤 순서로든 나타날 수 있어요. 식별자는 필드 이름과 일치해야 해요.
  • 첫 StructMemberInitializer가 Identifier를 지정하지 않으면, StructDeclaration의 첫 필드를 가리켜요.
  • Identifier 없는 이후의 NonVoidInitializer는 이전 StructMemberInitializer가 가리킨 필드 다음에 (어휘적 순서로) 오는 다음 필드를 가리켜요.

StructMemberInitializer가 다루지 않는 어떤 필드든 기본 초기화돼요.

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

필드를 두 번 이상 초기화하는 것은 오류예요:

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

Union의 기본 초기화 (Default Initialization of Unions)

Union은 기본적으로 첫 필드의 Initializer가 무엇이든 그것으로 초기화되고, 제공된 것이 없으면 첫 필드 타입의 기본 초기화 값으로 초기화돼요. union이 첫 필드보다 크면 나머지 비트는 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; }     // error: union field `b` with default initialization `4` must be before field `a`
union W { int a = 4; long b = 5; } // error: overlapping default initialization for `a` and `b`

기본 초기화 값은 컴파일 타임에 평가돼요.

Implementation Defined: (구현 정의 사항) 기본 초기화된 필드가 아닌 다른 필드들이 설정되는 값.

Union의 정적 초기화 (Static Initialization of Unions)

Union은 struct와 유사하게 초기화되지만, 멤버 초기화 값은 하나만 허용돼요. 멤버 초기화 값이 식별자를 지정하지 않으면 union의 첫 멤버를 초기화해요.

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`

union이 초기화된 필드보다 크면 나머지 비트는 0으로 설정돼요.

Implementation Defined: (구현 정의 사항) 초기화된 필드가 아닌 다른 필드들이 설정되는 값.

Struct의 동적 초기화 (Dynamic Initialization of Structs)

정적 초기화 문법은 비정적 변수를 초기화하는 데에도 사용될 수 있어요. 초기화 값은 컴파일 타임에 평가 가능하지 않아도 돼요.

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
}

Struct는 같은 타입의 다른 값으로부터 동적으로 초기화될 수 있어요:

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

struct에 생성자가 있고, struct가 다른 타입의 값으로 초기화되면 생성자가 호출돼요:

struct S
{
    int a;

    this(int v)
    {
        this.a = v;
    }
}

S s = 3; // sets s.a to 3 using S's constructor

struct에 생성자가 없지만 opCall이 struct에 대해 오버로드되었고, struct가 다른 타입의 값으로 초기화되면 opCall 연산자가 호출돼요:

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

Union의 동적 초기화 (Dynamic Initialization of Unions)

정적 초기화 문법은 비정적 변수를 초기화하는 데에도 사용될 수 있어요. 초기화 값은 컴파일 타임에 평가 가능하지 않아도 돼요.

union U { int a; double b; }

void test(int i)
{
    U u = { a : i };   // u.a = i
    U v = { b : 5.0 }; // v.b = 5.0
}

Struct 리터럴 (Struct Literals)

struct 리터럴은 struct의 이름 뒤에 괄호로 감싼 이름 붙은 인자 목록이 오는 형태예요:

struct S { int x; float y; }

S s1 = S(1, 2); // set field x to 1, field y to 2
S s2 = S(y: 2, x: 1); // same as above
assert(s1 == s2);

struct에 생성자나 opCall이라는 멤버 함수가 있으면, 그 struct에 대한 struct 리터럴은 가능하지 않아요. 해결 방법은 opCall 연산자 오버로딩을 참조하세요.

Struct 리터럴은 문법적으로 함수 호출과 같아요. 인자는 다음과 같이 필드에 할당돼요:

  1. 첫 인자에 이름이 없으면, 어휘적으로 먼저 정의된 struct 필드에 할당돼요.
  2. 이름 붙은 인자는 같은 이름의 struct 필드에 할당돼요. 그런 필드가 없으면 오류예요.
  3. 어떤 다른 인자라도, 이전 인자의 struct 필드에 상대적으로 다음 어휘적으로 정의된 struct 필드에 할당돼요. 그런 필드가 없으면(즉, 이전 인자가 마지막 struct 필드에 할당하면) 오류예요.
  4. 필드를 두 번 이상 할당하는 것도 오류예요.
  5. 값이 할당되지 않은 어떤 필드든 각자의 기본 초기화 값으로 초기화돼요.

Note: 이 규칙들은 함수 호출과 일관되며, Matching Arguments to Parameters를 참조하세요.

struct에 union 필드가 있으면, struct 리터럴 안에서 union의 멤버 하나만 초기화할 수 있어요. 이는 union 리터럴의 동작과 일치해요.

struct S { int x = 1, y = 2, z = 3; }

S s0 = S(y: 5, 6, x: 4); // `6` is assigned to field `z`, which comes after `y`
assert(s0.z == 6);

S s1 = S(y: 5, z: 6);    // Field x is not assigned, set to default initializer `1`
assert(s1.x == 1);

//S s2 = S(y: 5, x: 4, 5); // Error: field `y` is assigned twice
//S s3 = S(z: 2, 3);       // Error: no field beyond `z`

Union 리터럴 (Union Literals)

union 리터럴은 struct 리터럴과 같지만, 초기화 값 표현식으로 하나의 필드만 초기화할 수 있어요. union 메모리의 나머지는 0으로 초기화돼요.

union U
{
    byte a;
    char[2] b;
}

U u = U(2);
assert(u.a == 2);
assert(u.b == [2, 0]);

익명 Struct/Union (Anonymous Structs and Unions)

부모 class, struct, union의 멤버로 struct나 union 뒤의 식별자를 생략하면 익명 struct나 union을 선언할 수 있어요. 익명 struct는 부모 타입에 순차적으로 저장된 필드들을 선언해요. 익명 union은 부모 타입에 중복되는 필드들을 선언해요.

익명 union은 class나 struct 안에서, 별도의 union 타입을 가진 부모 필드 이름을 짓지 않고 필드 메모리를 공유할 때 유용해요.

struct S
{
    int a;
    union
    {
        byte b;
        char c;
    }
}

S s = S(1, 2);
assert(s.a == 1);
assert(s.b == 2);
assert(s.c == 2); // overlaps with `b`

반대로 익명 struct는 union 안에서 순차적으로 저장되는 여러 필드를 선언할 때 유용해요.

union U
{
    int a;
    struct
    {
        uint b;
        bool c;
    }
}

U u = U(1);
assert(u.a == 1);
assert(u.b == 1); // overlaps with `a`
assert(u.c == false); // no overlap

Struct 속성 (Struct Properties)

이름 설명
.alignof struct를 정렬해야 하는 크기 경계
.tupleof 모든 struct 필드의 심볼 시퀀스 — 자세한 내용은 class .tupleof 참조

Struct 필드 속성 (Struct Field Properties)

속성 설명
.offsetof struct 시작부터 필드까지의 바이트 오프셋. 예제는 align 속성 참조

Const, Immutable 및 Shared Struct

struct 선언은 const, immutable, shared의 저장소 클래스를 가질 수 있어요. 이는 struct의 각 멤버를 const, immutable, 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 생성자 (Union Constructors)

Union은 struct와 같은 방식으로 생성돼요.

Struct 생성자 (Struct Constructors)

Struct 생성자는 정적 초기화나 struct 리터럴이 허용하는 것보다 더 복잡한 구성이 필요할 때 struct 인스턴스를 초기화하는 데 사용돼요.

생성자는 함수 이름이 this이고 반환 값이 없는 것으로 정의돼요. 문법은 class 생성자와 같아요. struct 생성자는 struct 이름 뒤에 Parameters가 오는 것으로 호출돼요. ParameterList가 비어 있으면 struct 인스턴스는 기본 초기화돼요.

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
}

이름 붙은 인자(named arguments)는 생성자로 전달되고 struct 필드 이름이 아니라 매개변수 이름과 일치해요.

struct S
{
    int x;
    int y;
    this(int y, int z) { this.x = y; this.y = z; }
}
S a = S(x: 3, y: 4); // Error: constructor has no parameter named `x`
S b = S(y: 3, 4); // `y: 3` will set field `x` through parameter `y`

기본 생성자(즉 빈 ParameterList를 가진 것)는 허용되지 않아요.

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

위임 생성자 (Delegating Constructors)

생성자는 공통 초기화를 공유하기 위해 같은 struct의 다른 생성자를 호출할 수 있어요. 이를 위임 생성자(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(6L); // delegating constructor call
        // At this point: j=1, k=6
        j = i;
        // At this point: j=i, k=6
    }
}

다음 제한이 적용돼요:

  1. 생성자 코드가 위임 생성자 호출을 포함하면, 생성자를 통한 모든 가능한 실행 경로는 정확히 하나의 위임 생성자 호출을 만들어야 해요:
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
        }
    }
}
  1. 위임 생성자 호출을 하기 전에 this를 암시적 또는 명시적으로 참조하는 것은 불법이에요.
  2. 위임 생성자가 반환되면 모든 필드는 구성된(constructed) 것으로 간주돼요.
  3. 위임 생성자 호출은 라벨 뒤에 나타날 수 없어요.

Struct 인스턴스화 (Struct Instantiation)

struct 인스턴스가 생성되면 다음 단계가 일어나요:

  1. 원시 데이터가 struct 정의에 제공된 값들로 정적으로 초기화돼요. 이 연산은 객체의 정적 버전을 새로 할당된 것에 메모리 복사하는 것과 동등해요.
  2. struct에 대해 정의된 생성자가 있으면, 인자 목록과 일치하는 생성자가 호출돼요.
  3. struct 불변식 검사가 켜져 있으면, struct 불변식이 생성자 끝에서 호출돼요.

생성자 속성 (Constructor Attributes)

생성자는 비활성화될 수 있어요. 한정된 생성자(const, immutable, shared)는 그 특정 한정자로 객체 인스턴스를 구성해요.

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);
}

생성자는 서로 다른 한정자로 오버로드될 수 있어요.

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

void fun()
{
    S m = S(1);
    shared s = shared S(2);
    immutable i = immutable S(3);
}
순수 생성자 (Pure Constructors)

생성자가 고유한(unique) 객체를 만들 수 있으면(즉 pure이면), 객체는 어떤 한정자로든 암시적으로 변환될 수 있어요.

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.
}

void fun()
{
    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 Initialization)

struct 생성자가 @disable로 표시되고 빈 ParameterList를 가지면, struct는 기본 초기화가 비활성화돼요. 그것이 구성될 수 있는 유일한 방법은 비어 있지 않은 ParameterList를 가진 다른 생성자를 호출하는 것이에요. 다른 유효한 생성자가 없으면, VoidInitializer를 통해서만 인스턴스화될 수 있어요.

비활성화된 생성자는 FunctionBody를 가질 수 없어요. 어떤 필드가 기본 초기화가 비활성화되었으면, struct 기본 초기화도 비활성화돼요.

struct S
{
    int x;

    // Disables default initialization
    @disable this();

    this(int v) { x = v; }
}
struct T
{
    int y;
    S s;
}
void main()
{
    S s;          // error: default initialization 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 initialization is disabled
    S[3] b = [S(1), S(20), S(-2)]; // ok
    T t;          // error: default initialization is disabled
}

Best Practices: (모범 사례) null 같은 필드의 기본값이 받아들여지지 않을 때 기본 초기화를 비활성화하는 것이 유용해요.

생성자 안의 필드 초기화 (Field initialization inside a constructor)

생성자 본문에서, 위임 생성자가 호출되면 모든 필드 할당은 할당(assignment)으로 간주돼요. 그렇지 않으면, 필드 할당의 첫 인스턴스가 초기화(initialization)이고, field = expression 형태의 할당은 typeof(field)(expression)과 동등하게 취급돼요. 필드의 값은 위임 생성자로 초기화 또는 구성되기 전에 읽힐 수 있어요.

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
    }
}

필드 타입에 opAssign 메서드가 있으면, 초기화에는 사용되지 않아요.

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))
    }
}

필드 타입이 mutable이 아니면, 여러 초기화는 거부돼요.

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

필드가 한 경로에서 초기화되면 모든 경로에서 초기화되어야 해요.

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
    }
}

필드 초기화는 루프 안이나 라벨 뒤에 나타날 수 없어요.

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
    }
}

필드의 타입이 기본 구성을 비활성화했으면, 생성자에서 초기화되어야 해요.

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 복사 생성자 (Struct Copy Constructors)

Warning: (경고) 계획은 복사 생성자가 postblit 생성자를 대체하여, postblit 생성자는 레거시 코드에만 남게 하는 것이에요. 하지만 동적 배열과 연관 배열을 다루는 druntime의 컴파일러 훅이 아직 모두 복사 생성자를 제대로 지원하도록 갱신되지 않았기 때문에(issue #20970), 동적 배열이나 연관 배열에서 사용될 수 있는 어떤 타입이든 postblit 생성자를 복사 생성자 대신 사용해야 해요. 복사 생성자는 동적 배열의 요소나 연관 배열의 키·값에 대해 호출되어야 하는 모든 경우에 호출되지 않을 거예요. Postblit 생성자는 이 문제가 없어요.

하위 호환성 이유로, 복사 생성자와 postblit을 둘 다 명시적으로 정의한 struct는 암시적 복사에 postblit만 사용해요. 하지만 postblit이 비활성화되면 복사 생성자가 사용돼요. struct가 복사 생성자(사용자 정의 또는 생성된)를 정의하고 postblit을 정의하는 필드를 가지면, postblit이 복사 생성자보다 우선한다는 것을 알리는 deprecation이 발행돼요.

복사 생성자는 같은 타입의 다른 인스턴스에서 struct 인스턴스를 초기화하는 데 사용돼요. 복사 생성자를 정의한 struct는 POD가 아니에요.

생성자 선언이 다음 요구사항을 충족하면 복사 생성자 선언이에요:

  • 기본 인자가 없는 매개변수를 정확히 하나 취하고, 그 뒤에 기본 인자가 있는 임의 개수의 매개변수가 따라와요.
  • 첫 매개변수가 ref 매개변수예요.
  • 첫 매개변수의 타입이 typeof(this)와 같은 타입이며, 선택적으로 하나 이상의 타입 한정자가 적용돼요.
  • 템플릿 생성자 선언이 아니에요.
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
}

복사 생성자는 일반 생성자로 타입 검사돼요. 복사 생성자가 정의되면, 다음 상황에서 암시적 호출이 삽입돼요:

  1. 변수가 명시적으로 초기화될 때:
  2. 매개변수가 함수에 값으로 전달될 때:
  3. 매개변수가 함수에서 값으로 반환되고 Named Return Value Optimization (NRVO)가 수행될 수 없을 때:

복사 비활성화 (Disabled Copying)

struct에 대해 복사 생성자가 정의되면(또는 @disable로 표시되면), 컴파일러는 그 struct에 대한 기본 복사/blitting 생성자를 더 이상 암시적으로 생성하지 않아요:

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
}
struct A
{
    @disable this(ref A);
}

void main()
{
    A a;
    A b = a; // error: copy constructor is disabled
}

union U에 복사 생성자를 정의하는 필드가 있으면, 타입 U의 객체가 복사로 초기화될 때마다 오류가 발행돼요. 같은 규칙이 겹친 필드(익명 union)에 적용돼요.

struct S
{
    this(ref S);
}

union U
{
    S s;
}

void main()
{
    U a;
    U b = a; // error, could not generate copy constructor for U
}

복사 생성자 속성 (Copy Constructor Attributes)

복사 생성자는 매개변수(한정된 소스에서 복사) 또는 복사 생성자 자체(한정된 대상으로 복사)에 적용된 서로 다른 한정자로 오버로드될 수 있어요:

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
}

inout 한정자를 복사 생성자 매개변수에 적용해 mutable, const, immutable 타입이 동일하게 취급되도록 지정할 수 있어요:

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)

다음 조건이 모두 충족되면 컴파일러가 struct S에 대해 복사 생성자를 암시적으로 생성해요:

  1. S가 복사 생성자를 명시적으로 선언하지 않음
  2. S가 복사 생성자를 가진 직접 멤버를 최소한 하나 정의하고, 그 멤버가 (union으로) 다른 어떤 멤버와 겹치지 않음

위 제한이 충족되면 다음 복사 생성자가 생성돼요:

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

생성된 복사 생성자가 타입 검사에 실패하면 @disable 속성을 받아요.

Struct 이동 생성자 (Struct Move Constructors)

이동 생성자(move constructors)는 복사 생성자와 매우 유사해요. 차이는 복사 생성자가 원본의 복사본을 만드는 반면, 이동 생성자는 원본의 내용을 옮기고 원본의 수명(lifetime)이 끝난다는 점이에요.

Note: 이동 생성자와 같은 struct에서 postblit을 사용하지 마세요. 이동 생성자를 선언하면 복사 생성자도 선언하세요.

생성자 선언이 다음 요구사항을 충족하면 이동 생성자 선언이에요:

  • 기본 인자가 없는 매개변수를 정확히 하나 취하고, 그 뒤에 기본 인자가 있는 임의 개수의 매개변수가 따라와요.
  • 첫 매개변수가 ref 매개변수가 아니에요.
  • 첫 매개변수의 타입이 typeof(this)와 같은 타입이며, 선택적으로 하나 이상의 타입 한정자가 적용돼요.
  • 템플릿 생성자 선언이 아니에요.
struct A
{
    this(ref return scope A rhs) {}                    // copy constructor
    this(return scope A rhs) {}                        // move constructor
    this(return scope const A rhs, int b = 7) {}       // move constructor with default parameter
}

이동 생성자는 일반 생성자로 타입 검사돼요. 이동 생성자의 첫 매개변수는 rvalue만 받아들여요. lvalue는 __rvalue(Expression)을 사용해 rvalue가 되도록 강제할 수 있어요.

이동 생성자가 정의되면, 다음 상황에서 암시적 호출이 삽입돼요:

  1. 변수가 명시적으로 초기화될 때:
  2. 매개변수가 함수에 값으로 전달될 때:

이동 비활성화 (Disabled Moving)

struct에 대해 이동 생성자가 정의되면(또는 @disable로 표시되면), 컴파일러는 그 struct에 대한 기본 이동 생성자를 더 이상 암시적으로 생성하지 않아요:

struct A
{
    this(ref A);
    @disable this(A);
}

void main()
{
    A a;
    A b = __rvalue(a); // error: move constructor is disabled
}

union U에 이동 생성자를 정의하는 필드가 있으면, 타입 U의 객체가 이동으로 초기화될 때마다 오류가 발행돼요. 같은 규칙이 겹친 필드(익명 union)에 적용돼요.

struct S
{
    this(ref S);
    this(S);
}

union U
{
    S s;
}

void main()
{
    U a;
    U b = __rvalue(a); // error, could not generate move constructor for U
}

이동 생성자 속성 (Move Constructor Attributes)

이동 생성자는 매개변수(한정된 소스에서 이동) 또는 이동 생성자 자체(한정된 대상으로 이동)에 적용된 서로 다른 한정자로 오버로드될 수 있어요:

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

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

    A a2 = __rvalue(a);      // calls 1
    A a3 = __rvalue(ia);     // calls 2

    A b;
    immutable A ib;

    immutable A b4 = __rvalue(b);     // calls 3
    immutable A b5 = __rvalue(ib);    // calls 4
}

inout 한정자를 이동 생성자 매개변수에 적용해 mutable, const, immutable 타입이 동일하게 취급되도록 지정할 수 있어요:

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

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

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

암시적 이동 생성자 (Implicit Move Constructors)

다음 조건이 모두 충족되면 컴파일러가 struct S에 대해 이동 생성자를 암시적으로 생성해요:

  1. S가 이동 생성자를 명시적으로 선언하지 않음
  2. S가 이동 생성자를 가진 직접 멤버를 최소한 하나 정의하고, 그 멤버가 (union으로) 다른 어떤 멤버와 겹치지 않음

위 제한이 충족되면 다음 이동 생성자가 생성돼요:

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

생성된 이동 생성자가 타입 검사에 실패하면 @disable 속성을 받아요.

import core.stdc.stdio;

struct T
{
    int i;
    inout this(ref inout T t) { this.i = t.i - 1; printf("this(ref T)\n"); }
    inout this(inout T t)     { this.i = t.i + 1; printf("this(T)\n"); }
}

struct S
{
    T t;
}

void main()
{
    S s;
    s.t.i = 3;
    S u = s;
    printf("u.t.i = %d\n", u.t.i);
    assert(u.t.i == 2);

    S v = __rvalue(u);
    printf("v.t.i = %d\n", v.t.i);
    assert(v.t.i == 3);
}

Struct Postblit

Postblit:
    this ( this ) MemberFunctionAttributesopt FunctionBody
    this ( this ) MemberFunctionAttributesopt MissingFunctionBody

Warning: (경고) 계획은 복사 생성자가 postblit 생성자를 대체하여, postblit 생성자는 레거시 코드에만 남게 하는 것이에요. 하지만 동적 배열과 연관 배열을 다루는 druntime의 컴파일러 훅이 아직 모두 복사 생성자를 제대로 지원하도록 갱신되지 않았기 때문에(issue #20970), 동적 배열이나 연관 배열에서 사용될 수 있는 어떤 타입이든 postblit 생성자를 복사 생성자 대신 사용해야 해요. 복사 생성자는 동적 배열의 요소나 연관 배열의 키·값에 대해 호출되어야 하는 모든 경우에 호출되지 않을 거예요. Postblit 생성자는 이 문제가 없어요.

하위 호환성 이유로, 복사 생성자와 postblit을 둘 다 명시적으로 정의한 struct는 암시적 복사에 postblit만 사용해요. 하지만 postblit이 비활성화되면 복사 생성자가 사용돼요. struct가 복사 생성자(사용자 정의 또는 생성된)를 정의하고 postblit을 정의하는 필드를 가지면, postblit이 복사 생성자보다 우선한다는 것을 알리는 deprecation이 발행돼요.

복사 구성(copy construction)은 같은 타입의 다른 인스턴스에서 struct 인스턴스를 초기화하는 것으로 정의돼요. 복사 구성은 두 부분으로 나뉘어요:

  1. 필드를 blitting, 즉 비트를 복사
  2. 결과에 postblit 실행

첫 부분은 언어가 자동으로 하고, 두 번째 부분은 struct에 대해 postblit 함수가 정의되어 있으면 해요. postblit은 대상 struct 객체에만 접근할 수 있고 소스에는 접근할 수 없어요. 그것의 작업은 참조된 데이터의 복사본 만들기, 참조 횟수 증가시키기 등과 같이 필요에 따라 대상을 '고치는(fix up)' 것이에요. 예를 들어:

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

struct postblit을 비활성화하면 객체를 복사할 수 없게 돼요.

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
}

struct 레이아웃에 따라 컴파일러는 다음 내부 postblit 함수들을 생성할 수 있어요:

  1. void __postblit(). 컴파일러는 명시적으로 정의된 postblit this(this)에 이 이름을 할당해, 정확히 일반 함수처럼 취급될 수 있게 해요. struct가 postblit을 정의하면, __postblit이라는 이름의 함수를 정의할 수 없어요 — 시그니처가 어떻든 — 이름 충돌로 인해 컴파일 오류가 발생하기 때문이에요.
  2. void __fieldPostblit(). struct X가 (명시적 또는 암시적으로) postblit을 정의하는 struct 멤버를 최소한 하나 가지면, X에 대해 field postblit이 생성되어, 선언 순서로 struct 필드들의 모든 기반 postblit을 호출해요.
  3. void __aggrPostblit(). struct가 명시적으로 정의된 postblit과 postblit이 있는(명시적 또는 암시적) struct 멤버를 최소한 1개 가지면, __fieldPostblit을 먼저 호출한 다음 __postblit을 호출하는 집계된(aggregated) postblit이 생성돼요.
  4. void __xpostblit(). field와 aggregated postblit은 struct를 위해 생성되지만 실제 struct 멤버는 아니에요. 그것들을 호출할 수 있으려면, 컴파일러는 내부적으로 __xpostblit이라 불리는 alias를 생성하는데, 이는 struct의 멤버이며 가장 포괄적인 생성된 postblit을 가리켜요.
// 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"));
}

위 postblit들 중 어느 것도 this(this)를 정의하지 않고 그것을 전이적으로 정의하는 필드도 없는 struct에 대해서는 정의되지 않아요. struct가 postblit(암시적 또는 명시적)을 정의하지 않지만 내부 생성된 postblit과 같은 이름/시그니처의 함수를 정의하면, 컴파일러는 그 함수들이 실제 postblit이 아님을 식별할 수 있고, struct가 복사될 때 그것들을 호출을 삽입하지 않아요. 예제:

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
}

Postblit은 오버로드될 수 없어요. 두 개 이상의 postblit이 정의되면, 시그니처가 다르더라도, 컴파일러는 둘 다에 __postblit 이름을 할당하고 나중에 충돌 함수 이름 오류를 발행해요:

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

다음은 한정된 postblit 정의의 동작을 설명해요:

  1. const. postblit이 this(this) const;const this(this);처럼 const로 한정되면, postblit은 mutable(비한정), const, immutable 객체에서 성공적으로 호출되지만, 객체를 const로 간주하므로 수정할 수 없어요. 따라서 const postblit은 유용성이 제한돼요.
  2. immutable. postblit이 this(this) immutable이나 immutable this(this)처럼 immutable로 한정되면 코드는 잘못된 형식(ill-formed)이에요. immutable postblit은 컴파일 단계를 통과하지만 호출될 수 없어요.
  3. shared. postblit이 this(this) sharedshared this(this)처럼 shared로 한정되면 shared 객체만 postblit을 호출할 수 있어요. 비공유 객체를 postbliting하려는 시도는 컴파일 타임 오류를 일으켜요.

비한정 postblit은 struct가 immutable이나 const로 인스턴스화되어도 호출되지만, struct가 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
}

postblit 관점에서, struct 정의를 한정하는 것은 postblit을 명시적으로 한정하는 것과 같은 결과를 산출해요.

다음 표는 객체 타입과 연관된 postblit에 대한 한정자 그룹핑의 모든 가능성을 나열해요. 그 객체 타입은 postblit을 성공적으로 호출하기 위해 사용되어야 해요:

한정자 그룹 호출될 객체 타입 const immutable shared
모든 객체 타입
uncallable
공유 객체 (shared object)
uncallable
공유 객체
uncallable
uncallable

this(this) const immutable;이나 const immutable this(this);처럼 const와 immutable이 postblit을 명시적으로 한정하는 데 사용될 때 — 한정자 선언 순서는 무관하고 — 컴파일러가 충돌 속성 오류를 생성하지만, struct를 const/immutable로 선언하고 postblit을 immutable/const로 선언하면 두 한정자 모두를 postblit에 적용하는 효과를 이뤄요. 두 경우 모두 postblit은 더 제한적인 한정자(immutable)로 한정돼요.

__fieldPostblit__aggrPostblit postblit은 암시적 한정자 없이 생성되고 struct 멤버로 간주되지 않아요. 이는 struct 선언 전체를 constimmutable로 한정하는 것이 위에서 언급한 postblit들에 아무 영향도 주지 않는 상황으로 이어져요. 하지만 __xpostblit은 struct의 멤버이고 다른 postblit 중 하나의 alias이므로, struct에 적용된 한정자는 앨리어스된 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;
}

위 상황들에서 오류는 생성된 코드에 관한 것이므로 줄 번호(line numbers)를 포함하지 않아요.

struct 전체를 shared로 한정하면 생성된 postblit에 속성을 올바르게 전파해요:

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;
}

Union은 postblit이 있는 필드를 가질 수 있어요. 하지만 union 자체는 결코 postblit을 갖지 않아요. union을 복사해도 어떤 필드에 대한 postblit 호출도 일어나지 않아요. 그 호출들이 필요하면 프로그래머가 명시적으로 삽입해야 해요:

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);
}

멤버 함수 (Member Functions / Methods)

struct/union은 클래스처럼 비정적 멤버 함수를 가질 수 있어요. 그런 함수(인스턴스 메서드라 불림)는 struct 인스턴스에 대한 참조인 숨겨진 this 매개변수를 가져요. 메서드는 immutable 메서드 같은 MemberFunctionAttributes를 가질 수 있어요.

인스턴스 메서드는 메서드가 const가 아니어도 rvalue struct 인스턴스에서 여전히 호출될 수 있어요:

struct S
{
    int i;
    int f() => ++i;
}

void main()
{
    //S().i++; // cannot modify, `S().i` is not an lvalue
    assert(S().f() == 1); // OK
}

Rationale: (근거) 인스턴스 메서드는 필드를 변경하는 것 외에 다른 부수 효과를 가질 수 있고, 유용한 반환 값을 만들 수도 있어요. 일반적인 경우, 메서드가 반환된 후 필드에 대한 변경을 버리는 것이 반드시 논리 오류를 나타내는 것은 아니에요.

Struct 소멸자 (Struct Destructors)

소멸자는 객체가 스코프를 벗어날 때나 (기본적으로) 할당 전에 암시적으로 호출돼요. 그 목적은 struct 객체가 소유한 자원을 해제하는 것이에요.

struct S
{
    int i;

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

void main()
{
    auto s1 = S(1);
    {
        auto s2 = S(2);
        // s2 destructor called
    }
    S(3); // s3 destructor called
    // s1 destructor called
}

struct에 그 자체로 소멸자가 있는 다른 struct 타입의 필드가 있으면, 그 소멸자는 부모 소멸자 끝에서 호출돼요. 부모 소멸자가 없으면 컴파일러가 하나를 생성해요. 마찬가지로, 소멸자가 있는 struct 타입의 정적 배열은 배열이 스코프를 벗어날 때 각 요소에 대해 소멸자가 호출돼요.

struct S
{
    char c;

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

struct Q
{
    S a;
    S b;
}

void main()
{
    Q q = Q(S('a'), S('b'));
    S[2] arr = [S('0'), S('1')];
    // destructor called for arr[1], arr[0], q.b, q.a
}

struct 인스턴스에 대한 소멸자는 destroy를 사용해 일찍 호출될 수도 있어요. 소멸자는 인스턴스가 스코프를 벗어날 때 여전히 다시 호출된다는 점에 주의하세요.

Struct 소멸자는 RAII에 사용돼요.

Union 필드 파괴 (Union Field Destruction)

Union은 소멸자가 있는 필드를 가질 수 있어요. 하지만 union 자체는 결코 소멸자를 갖지 않아요. union이 스코프를 벗어날 때 필드들에 대한 소멸자는 호출되지 않아요. 그 호출들이 필요하면 프로그래머가 명시적으로 삽입해야 해요:

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 불변식 (Struct Invariants)

Invariant:
    invariant ( ) BlockStatement
    invariant BlockStatement
    invariant ( AssertArguments ) ;

Struct 불변식은 struct 인스턴스의 멤버들 사이의 관계를 지정해요. 그 관계들은 인스턴스에 대한 공개 인터페이스(public interface)와의 어떤 상호작용에서도 유지되어야 해요.

불변식은 const 멤버 함수 형태예요. 불변식은 불변식 안에서 실행되는 모든 AssertExpression이 성공하면 유지되는 것으로 정의돼요.

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;
}

struct에는 여러 불변식이 있을 수 있어요. 그것들은 어휘적 순서로 적용돼요. Struct 불변식은 struct 생성자(있다면)의 종료 시점과 struct 소멸자(있다면)의 진입 시점에 유지되어야 해요. Struct 불변식은 모든 공개 또는 exported 비정적 멤버 함수의 진입과 종료 시점에 유지되어야 해요.

불변식 적용 순서는 다음과 같아요:

  1. 사전조건(preconditions)
  2. 불변식 (invariant)
  3. 함수 본문
  4. 불변식
  5. 사후조건(postconditions)

struct 인스턴스가 기본 .init 값을 사용해 암시적으로 구성되면 불변식은 유지될 필요가 없어요. 불변식이 유지되지 않으면 프로그램은 무효 상태(invalid state)에 들어가요.

Implementation Defined: (구현 정의 사항) struct 불변식이 런타임에 실행되는지 여부. 이는 전형적으로 컴파일러 스위치로 제어돼요. 불변식이 유지되지 않을 때의 동작은 전형적으로 AssertExpression이 실패할 때와 같아요.

Undefined Behavior: (미정의 동작) 불변식이 유지되지 않고 실행이 계속되면 발생해요.

불변식 안에서 공개 또는 exported 비정적 멤버 함수를 호출할 수 없어요.

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

    invariant
    {
        f();  // error, cannot call public member function from invariant
        g();  // ok, g() is not public
    }
}

Best Practices: (모범 사례) struct 불변식 안에서 exported 또는 공개 멤버 함수를 간접적으로 호출하지 마세요. 이는 무한 재귀를 일으킬 수 있어요. 불변식은 실행될 수도 있고 안 될 수도 있으므로, 불변식의 부수 효과에 의존하지 마세요. 불변식이 공개 인터페이스를 검증할 수 없게 되므로, 불변식이 있는 struct의 변경 가능한(mutable) 공개 필드를 피하세요.

정체성 할당 오버로드 (Identity Assignment Overload)

복사 구성이 같은 타입의 다른 객체에서 객체를 초기화하는 것을 다루는 반면, 할당(assignment)은 소스 객체의 내용을 대상 객체의 것 위에 복사하는 것으로 정의되며, 그 과정에서 대상 객체의 소멸자(있으면)를 호출해요:

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 할당 t = s는 의미적으로 다음과 동등하다고 정의돼요:

t.opAssign(s);

여기서 opAssign은 S의 멤버 함수예요.

다음 조건 중 하나 이상이 성립하면 struct에 identity assignment overload가 필요해요:

  • 그것은 소멸자를 가짐
  • 그것은 postblit을 가짐
  • 그것은 identity assignment overload가 있는 필드를 가짐

identity assignment overload가 필요하고 존재하지 않으면, 다음 lowering으로 identity assignment overload 함수가 자동 생성돼요:

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;
}

사용자 정의된 것은 동등한 의미를 구현할 수 있지만, 더 효율적일 수 있어요.

커스텀 opAssign이 더 효율적일 수 있는 한 가지 이유는 struct가 지역 버퍼에 대한 참조를 가질 때예요:

struct S
{
    int[] buf;
    int a;

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

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

여기서 S는 임시 작업 공간 buf[]를 가져요. 컴파일러 생성 opAssign은 그것을 무의미하게 해제하고 재할당할 거예요. 커스텀 opAssign은 기존 저장 공간을 재사용해요.

Alias This

AliasThis:
    alias Identifier this ;
    alias this = Identifier ;

AliasThis 선언은 하위 타입(subtype)으로 만들 멤버를 이름 짓고, Identifier가 그 멤버를 이름 지어요. struct나 union 인스턴스는 AliasThis 멤버로 암시적으로 변환될 수 있어요.

struct S
{
    int x;
    alias x this;
}

int foo(int i) { return i * 2; }

void main()
{
    S s;
    s.x = 7;
    int i = -s;
    assert(i == -7);
    i = s + 8;
    assert(i == 15);
    i = s + s;
    assert(i == 14);
    i = 9 + s;
    assert(i == 16);
    i = foo(s);  // implicit conversion to int
    assert(i == 14);
}

멤버가 class나 struct이면, 정의되지 않은 조회(undefined lookups)가 AliasThis 멤버로 전달돼요.

class Foo
{
    int baz = 4;
    int get() { return 7; }
}

struct Bar
{
    Foo foo;
    alias foo this;
}

void main()
{
    Bar bar = Bar(new Foo());
    int i = bar.baz;
    assert(i == 4);
    i = bar.get();
    assert(i == 7);
}

Identifier가 매개변수 없는 속성 멤버 함수(property member function)를 가리키면, 변환과 정의되지 않은 조회가 함수의 반환 값으로 전달돼요.

struct S
{
    int x;
    @property int get()
    {
        return x * 2;
    }
    alias get this;
}

void main()
{
    S s;
    s.x = 2;
    int i = s;
    assert(i == 4);
}

struct 선언이 opCmpopEquals 메서드를 정의하면, 그것은 AliasThis 멤버의 것보다 우선해요. opCmp 메서드와 달리, opEquals 메서드는 사용자 정의된 것이 제공되지 않으면 struct 선언에 대해 암시적으로 정의된다는 점에 주의하세요. 즉, AliasThis 멤버의 opEquals가 사용되어야 하면 명시적으로 정의되어야 해요:

struct S
{
    int a;
    bool opEquals(S rhs) const
    {
        return this.a == rhs.a;
    }
}

struct T
{
    int b;
    S s;
    alias s this;
}

void main()
{
    S s1, s2;
    T t1, t2;

    assert(s1 == s2);      // calls S.opEquals
    assert(t1 == t2);      // calls compiler generated T.opEquals that implements member-wise equality

    assert(s1 == t1);      // calls s1.opEquals(t1.s);
    assert(t1 == s1);      // calls t1.s.opEquals(s1);
}
struct U
{
    int a;
    bool opCmp(U rhs) const
    {
        return this.a < rhs.a;
    }
}

struct V
{
    int b;
    U u;
    alias u this;
}

void main()
{
    U u1, u2;
    V v1, v2;

    assert(!(u1 < u2));    // calls U.opCmp
    assert(!(v1 < v2));    // calls U.opCmp because V does not define an opCmp method
                           // so the alias this of v1 is employed; U.opCmp expects a
                           // paramter of type U, so alias this of v2 is used

    assert(!(u1 < v1));    // calls u1.opCmp(v1.u);
    assert(!(v1 < u1));    // calls v1.u.opCmp(v1);
}

속성은 AliasThis에 대해 무시돼요. struct/union은 단일 AliasThis 멤버만 가질 수 있어요.

중첩 Struct (Nested Structs)

struct는 다음 경우 중 하나면 중첩 struct(nested struct)예요:

  1. 함수의 스코프 안에 선언됨, 또는
  2. 지역 함수의 스택 데이터를 앨리어스하는 하나 이상의 템플릿 인자를 가진 템플릿 struct.

중첩 struct는 멤버 함수를 가질 수 있어요. 숨겨진 필드를 통해 둘러싼 스코프의 컨텍스트에 접근할 수 있어요.

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
}

static 속성은 struct가 중첩되는 것을 방지해요. 그렇게 되면 struct는 둘러싼 스코프에 접근할 수 없어요.

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

Warning: (경고) 중첩 struct의 경우, .init은 기본 초기화(default initialization)와 같지 않아요.

더 알아보기