선언

선언 (Declarations)

D 언어에서 "선언(declaration)"은 이름과 타입을 묶어 심볼을 만들어내는, 거의 모든 것의 출발점이에요. 변수는 물론이고 타입 이름, 함수, 심지어 별명(alias)까지 전부 선언을 통해 세상에 나타나요. 이 장에서는 D의 선언 문법이 어떻게 생겼고, 초기화는 어떻게 동작하며, alias 같은 편리한 선언들이 어떤 규칙 아래 있는지를 차근차근 살펴볼게요.

출처: https://dlang.org/spec/declaration.html

본문

문법 (Grammar)

Declaration은 함수 몸통 안에서는 DeclarationStatement로, 함수 밖에서는 DeclDef의 일부로 쓰일 수 있어요. 즉 D의 선언은 코드를 짜는 어느 위치에서든 등장할 수 있는 기본 단위라는 뜻이에요.

Declaration:
    FuncDeclaration
    VarDeclarations
    AliasDeclaration
    AggregateDeclaration
    EnumDeclaration
    ImportDeclaration
    ConditionalDeclaration
    StaticForeachDeclaration
    StaticAssert
    TemplateDeclaration
    TemplateMixinDeclaration
    TemplateMixin

애그리게이트 (Aggregates)

클래스, 인터페이스, 구조체, 유니언처럼 여러 멤버를 묶는 선언을 하나로 묶어요.

AggregateDeclaration:
    ClassDeclaration
    InterfaceDeclaration
    StructDeclaration
    UnionDeclaration

변수 선언 (Variable Declarations)

변수 선언은 크게 두 갈래로 나뉘어요. 타입과 식별자를 명시해서 만드는 일반 변수 선언, 그리고 타입을 생략해 추론에 맡기는 AutoDeclaration가 그것이에요. 튜플 패턴으로 여러 변수를 한 번에 풀어쓰는 일도 가능하죠.

VarDeclarations:
    StorageClassesopt BasicType TypeSuffixesopt IdentifierInitializers ;
    AutoDeclaration
    TuplePattern = AssignExpression ;

IdentifierInitializers: 
    IdentifierInitializer
    IdentifierInitializer , IdentifierInitializers

IdentifierInitializer: 
    Identifier
    Identifier TemplateParametersopt = Initializer
    BitfieldDeclarator
    BitfieldDeclarator = Initializer

BitfieldDeclarator:
    : AssignExpression
    Identifier : ConditionalExpression

Declarator: 
    TypeSuffixesopt Identifier
  • StorageClassesenum이 포함되면 각 Identifier는 매니페스트 상수(manifest constant)가 돼요.
  • 타입이 생략되면 그 선언은 AutoDeclaration가 돼요.
  • BitfieldDeclarator가 있으면 그 선언은 비트 필드예요.
  • IdentifierTemplateParameters가 붙으면 변수 템플릿이 돼요.
  • TuplePattern이 있으면 AssignExpression이 각 구성요소 선언으로 풀려요.

자세한 선언 문법은 아래 "선언 문법" 절에서 다룰게요.

저장 클래스 (Storage Classes)

저장 클래스가 어떤 것인지에 대한 개괄은 "타입 한정자 vs. 저장 클래스" 절에서 자세히 봐요. 여기서는 문법만 먼저 확인할게요.

StorageClasses:
    StorageClass
    StorageClass StorageClasses

StorageClass:
    LinkageAttribute
    AlignAttribute
    AtAttribute
    deprecated
    enum
    static
    extern
    abstract
    final
    override
    synchronized
    auto
    scope
    const
    immutable
    inout
    shared
    __gshared
    Property
    nothrow
    pure
    ref

선언 문법 (Declaration Syntax)

D의 선언 문법은 보통 오른쪽에서 왼쪽으로 읽어요. 배열도 예외가 아니에요. 포인터 기호 *는 그 변수가 가리키는 대상을, []는 그 변수가 담는 배열을 차례로 겹쳐 읽으면 돼요.

int x;    // x is an int
int* x;   // x is a pointer to int
int** x;  // x is a pointer to a pointer to int

int[] x;  // x is an array of ints
int*[] x; // x is an array of pointers to ints
int[]* x; // x is a pointer to an array of ints

int[3] x;     // x is a static array of 3 ints
int[3][5] x;  // x is a static array of 5 static arrays of 3 ints
int[3]*[5] x; // x is a static array of 5 pointers to static arrays of 3 ints

포인터와 배열, 그리고 타입 접미사(TypeSuffix)에 대한 더 자세한 내용은 각각 "Pointers", "Arrays", "TypeSuffix" 절을 참고해요.

함수 포인터 (Function Pointers)

함수 포인터는 function 키워드로 선언해요. int function(char)처럼 "반환 타입 + function 키워드 + 매개변수 타입 목록"의 순서로 읽어요.

int function(char) x; // x is a pointer to
                     // a function taking a char argument
                     // and returning an int
int function(char)[] x; // x is an array of
                     // pointers to functions
                     // taking a char argument
                     // and returning an int

C 스타일 선언 (C-Style Declarations)

C 스타일의 배열·함수 포인터·배열 포인터 선언은 D에서 지원하지 않아요. 다음 C 선언들은 그저 비교를 위해 보여주는 것일 뿐이에요.

int x[3];          // C static array of 3 ints
int x[3][5];       // C static array of 3 arrays of 5 ints

int (*x[5])[3];    // C static array of 5 pointers to static arrays of 3 ints
int (*x)(char);    // C pointer to a function taking a char argument
                   // and returning an int
int (*[] x)(char); // C array of pointers to functions
                   // taking a char argument and returning an int

이유 (Rationale):

  • D에서는 타입을 오른쪽에서 왼쪽으로 곧바로 읽기 쉬워요. 반면 C에서는 괄호가 필요할 때가 있고, 타입을 시계 방향/나선형 규칙으로 반복해서 읽어야 하죠.
  • C 함수 포인터 선언 a (*b)(c);을 보면 C 파서는 이걸 모호함 없이 파싱하려고 타입 조회를 시도해야 해요. 왜냐하면 이게 함수 포인터를 반환하고 즉시 호출되는 함수 a의 호출일 수도 있거든요. D의 함수 포인터 문법은 모호함이 없어서 타입을 미리 앞선 선언(forward declaration)으로 만들어둘 필요가 없어요.

여러 심볼 선언하기 (Declaring Multiple Symbols)

여러 심볼을 한 번에 선언할 때, 모든 선언은 같은 타입이어야 해요. C처럼 한 줄에 서로 다른 타입을 섞을 수 없어요.

int x, y;   // x and y are ints
int* x, y;  // x and y are pointers to ints
int[] x, y; // x and y are arrays of ints

이건 C와 대조되는 부분이에요. C에서는 타입이 첫 심볼에만 적용되고 나머지는 각자 다를 수 있죠:

int x, *y;  // x is an int, y is a pointer to int
int x[], y; // x is an array/pointer, y is an int

초기화 (Initialization)

초기화 구문(Initializer)을 주지 않으면 변수는 그 타입의 기본값인 .init 값으로 설정돼요.

초기화 구문 (Initializers)

Initializer:
    VoidInitializer
    NonVoidInitializer

NonVoidInitializer:
    ArrayInitializer
    StructInitializer
    AssignExpression

변수는 NonVoidInitializer로 초기화할 수 있어요. 그중에서도 배열 초기화나 구조체 초기화가 표현식 초기화보다 우선해요. 아래 예시에서 S s = {};은 함수 리터럴 표현식이 아니라 구조체 초기화로 해석돼요.

struct S { int i; }

S s = {}; // struct initializer, not a function literal expression
S[] a = [{2}]; // array initializer holding a struct initializer
//a = [{2}]; // invalid array literal expression

빈 초기화 (Void Initialization)

VoidInitializer:
    void

보통 변수는 초기화가 돼요. 그런데 변수의 초기화 구문이 void이면 그 변수는 초기화되지 않아요. 포인터처럼 안전하지 않은 값을 담을 수 있는 타입의 변수에 빈 초기화를 쓰는 것은 @safe 코드에서 허용되지 않아요.

구현 정의 (Implementation Defined): 빈 초기화된 변수의 값을 설정하기 전에 읽으면 그 값이 무엇일지는 구현마다 달라요.

void bad()
{
    int x = void;
    writeln(x);  // print implementation defined value
}

정의되지 않은 동작 (Undefined Behavior): 빈 초기화된 변수의 값을 설정하기 전에 읽었는데, 그 값이 참조·포인터·불변식을 가진 구조체 인스턴스라면 동작이 정의되지 않아요.

void muchWorse()
{
    char[] p = void;
    writeln(p);  // may result in apocalypse
}

모범 사례 (Best Practices):

  1. 빈 초기화는 정적 배열이 스택에 있지만 일부만 사용될 때 유용해요. 예를 들어 임시 버퍼처럼요. 빈 초기화는 코드를 잠재적으로 빨라지게 하지만, 배열 요소를 읽기 전에 항상 세팅해야 한다는 위험도 함께 가져와요.
  2. 구조체에도 같은 얘기가 적용돼요.
  3. 개별 지역 변수에 빈 초기화를 쓰는 건 거의 유용하지 않아요. 현대 최적화 컴파일러는 나중에 초기화되는 변수의 죽은 저장(dead store)을 제거해 버리거든요.
  4. 핫 코드 경로에서는 빈 초기화가 실제로 결과를 개선하는지 프로파일링으로 확인해 볼 가치가 있어요.

묵시적 타입 추론 (Implicit Type Inference)

AutoDeclaration:
    StorageClasses AutoAssignments ;

AutoAssignments:
    AutoAssignment
    AutoAssignments , AutoAssignment

AutoAssignment:
    Identifier TemplateParametersopt = Initializer
    TuplePattern = AssignExpression

선언이 StorageClass로 시작하면서, 타입을 추론할 수 있는 NonVoidInitializer가 있다면 선언의 타입을 생략할 수 있어요. 이걸 이용하면 auto 키워드나 다른 저장 클래스와 함께 타입을 알아서 추론시킬 수 있죠.

static x = 3;      // x is type int
auto y = 4u;       // y is type uint

auto s = "Apollo"; // s is type immutable(char)[] i.e., string

class C { ... }

auto c = new C();  // c is a handle to an instance of class C

NonVoidInitializer는 앞선 참조(forward reference)를 포함할 수 없어요. 이 제한은 나중에 없어질 수도 있어요. 어쨌든 묵시적으로 추론된 타입은 실행 시점이 아니라 컴파일 시점에 선언에 정적으로 결속돼요.

배열 리터럴(ArrayLiteral)은 정적 배열 타입이 아니라 동적 배열 타입으로 추론돼요:

auto v = ["resistance", "is", "useless"]; // type is string[], not string[3]

언패킹 선언 (Unpacking Declarations)

VarDeclarationsAutoDeclaration는 튜플 패턴으로 AssignExpression에서 하나 이상의 변수를 초기화하는 걸 지원해요. 이때 AssignExpression은 값 시퀀스(value sequence)로 묵시적으로 변환될 수 있어야 해요.

TuplePattern:
    ( TuplePatternComponents )

TuplePatternComponents:
    TuplePatternComponent ,
    TuplePatternComponent , TuplePatternComponent
    TuplePatternComponent , TuplePatternComponents

TuplePatternComponent:
    StorageClassesopt Identifier
    StorageClassesopt BasicType TypeSuffixesopt Identifier
    StorageClassesopt TuplePattern
  • 튜플 패턴의 각 구성요소는 값 시퀀스의 각 요소와 차례로 대응돼요.
  • 구성요소들은 콤마로 구분돼요.
  • 구성요소가 하나뿐인 패턴에서는 닫는 괄호 앞에 콤마가 와야 해요.
import std.typecons : tuple;

(int a, auto b) = tuple(1, "2"); // unpacking declaration
assert(a == 1);
assert(b == "2");

위 언패킹 선언은 예를 들어 다음과 같은 코드로 낮춰져요(lowered):

auto temp = tuple(1, "2");
int a = temp[0];
auto b = temp[1];

참고: std.typecons.Tuple을 봐요.

튜플 패턴에 저장 클래스가 없으면, 변수를 선언하는 각 구성요소는 타입이거나 자신에게 적용되는 저장 클래스가 있어야 해요:

(int a, b) = tuple(1, "2"); // Error: `b` has no type or storage class

패턴에 저장 클래스가 있으면 각 구성요소는 그냥 식별자만으로 충분해요. 각 변수의 타입은 대응하는 시퀀스 요소에서 추론돼요:

import std.typecons : tuple;

auto (a, b) = tuple(1, "2");
static assert(is(typeof(a) == int));
static assert(is(typeof(b) == string));

auto (a, immutable b, c) = tuple(1, "2", 3.0);
static assert(is(typeof(a) == int));
static assert(is(typeof(b) == immutable string));
static assert(is(typeof(c) == double));

패턴은 중첩될 수도 있어요:

auto t = tuple(1, tuple("2", 3.0));
// the following are equivalent:
auto (a, (b, c)) = t;
(int a, (string b, double c)) = t;
(int a, auto (b, c)) = t;
(int a, (auto b, auto c)) = t;

static이나 enum은 선언 전체에 적용할 수 있지만, 개별 구성요소에는 적용할 수 없어요.

전역·정적 초기화 (Global and Static Initializers)

전역 변수나 정적 변수의 Initializer는 컴파일 시점에 평가할 수 있어야 해요. 실행 시점의 초기화는 정적 생성자(static constructor)로 처리해요.

구현 정의 (Implementation Defined): 일부 포인터가 다른 함수나 데이터의 주소로 초기화될 수 있는지 여부.

정적으로 할당되는 데이터 (Statically Allocated Data)

함수나 애그리게이트 타입 밖에 있는 VarDeclarations는 정적으로 할당돼요. 기본적으로 그런 변수는 다음 저장 클래스 중 하나를 가지지 않는 한 스레드-로컬(thread-local)이에요:

  • const 또는 immutable
  • shared 또는 __gshared

함수 안의 VarDeclarations는 기본적으로 스택에 할당돼요. 하지만 다음 저장 클래스들은 함수 안이나 애그리게이트 타입 안에서도 데이터를 정적으로 할당해요:

  • static - 스레드-로컬 데이터를 할당
  • shared static 또는 __gshared - 전역 데이터를 할당

참고: StorageClassesenum 또는 extern이 포함되면 변수 선언은 저장 공간을 아예 할당받지 않아요.

구현 정의 (Implementation Defined): 초기화가 void이거나 비트가 전부 0인 정적 선언은 저장 공간을 절약하기 위해 바이너리의 .bss 섹션에 들어갈 수도 있어요.

별명 선언 (Alias Declarations)

2024 에디션: AliasAssignments 형태만 사용해요. 레거시 형태는 아래 문법으로 확인할 수 있어요.

AliasDeclaration:
    alias StorageClassesopt BasicType TypeSuffixesopt Identifiers ;
    alias StorageClassesopt BasicType FuncDeclarator ;
    alias AliasAssignments ;

Identifiers:
    Identifier
    Identifier , Identifiers

AliasAssignments:
    AliasAssignment
    AliasAssignments , AliasAssignment

AliasAssignment:
    Identifier TemplateParametersopt = StorageClassesopt Type
    Identifier TemplateParametersopt = FunctionLiteral
    Identifier TemplateParametersopt = StorageClassesopt Type Parameters MemberFunctionAttributesopt

AliasDeclaration은 타입이나 다른 심볼을 가리키는 심볼 이름을 만들어요. 그 이름은 이제 대상이 등장할 수 있는 어디에서든 쓸 수 있어요. 별명을 붙일 수 있는 것들은 다음과 같아요:

  • 타입
  • 함수 타입 (기본 인자 포함)
  • 변수
  • 매니페스트 상수
  • 모듈
  • 패키지
  • 함수
  • 오버로드 집합
  • 함수 리터럴
  • 템플릿
  • 템플릿 인스턴스화
  • 다른 별명 선언

타입 별명 (Type Aliases)

alias myint = abc.Foo.bar;

별명이 붙은 타입은 별명의 대상이 되는 타입과 의미적으로 동일해요. 디버거는 이 둘을 구분하지 못하고, 함수 오버로딩 관점에서도 차이가 없어요. 예를 들어:

alias myint = int;

void foo(int x) { ... }
void foo(myint m) { ... } // error, multiply defined function foo

타입 별명은 때로 다른 심볼 별명과 구분하기 어려워 보일 수 있어요:

alias abc = foo.bar; // is it a type or a symbol?

모범 사례 (Best Practices): 단순한 기본 타입 이름을 별명할 때를 제외하면, 타입 별명 이름은 대문자로 시작하는 게 좋아요.

심볼 별명 (Symbol Aliases)

심볼은 다른 심볼의 별명으로 선언할 수 있어요. 예를 들어:

import planets;

alias myAlbedo = planets.albedo;
...
int len = myAlbedo("Saturn"); // actually calls planets.albedo()

다음 별명 선언들은 모두 유효해요:

template Foo2(T) { alias t = T; }
alias t1 = Foo2!(int);
alias t2 = Foo2!(int).t;
alias t3 = t1.t;
alias t4 = t2;

t1.t v1;  // v1 is type int
t2 v2;    // v2 is type int
t3 v3;    // v3 is type int
t4 v4;    // v4 is type int

별명이 붙은 심볼은 긴 정규화된 심볼 이름을 줄여 쓰는 약칭으로, 또는 어떤 심볼에서 다른 심볼로 참조를 우회시키는 방법으로 유용해요:

version (Win32)
{
    alias myfoo = win32.foo;
}
version (linux)
{
    alias myfoo = linux.bar;
}

별명은 임포트한 모듈이나 패키지에서 현재 스코프로 심볼을 '가져오는' 데에도 쓸 수 있어요:

static import string;
...
alias strlen = string.strlen;

오버로드 집합 별명 (Aliasing an Overload Set)

별명은 오버로드된 함수들의 집합도 '가져올' 수 있어요. 그리고 그 함수들은 현재 스코프의 함수들과 함께 오버로드될 수 있어요:

class B
{
    int foo(int a, uint b) { return 2; }
}

class C : B
{
    // declaring an overload hides any base class overloads
    int foo(int a) { return 3; }
    // redeclare hidden overload
    alias foo = B.foo;
}

void main()
{
    import std.stdio;

    C c = new C();
    c.foo(1, 2u).writeln;   // calls B.foo
    c.foo(1).writeln;       // calls C.foo
}

변수 별명 (Aliasing Variables)

변수는 별명을 붙일 수 있지만, 표현식은 별명을 붙일 수 없어요:

int i = 0;
alias a = i; // OK
alias b = a; // alias a variable alias
a++;
b++;
assert(i == 2);

//alias c = i * 2; // error
//alias d = i + i; // error

애그리게이트의 멤버는 별명을 붙일 수 있지만, 비-정적 필드 별명은 그 부모 타입 밖에서는 접근할 수 없어요.

struct S
{
    static int i = 0;
    int j;
    alias a = j; // OK

    void inc() { a++; }
}

alias a = S.i; // OK
a++;
assert(S.i == 1);

alias b = S.j; // allowed
static assert(b.offsetof == 0);
//b++;   // error, no instance of S
//S.a++; // error, no instance of S

S s = S(5);
s.inc();
assert(s.j == 6);
//alias c = s.j; // scheduled for deprecation

함수 타입 별명 (Aliasing a Function Type)

함수 타입도 별명을 붙일 수 있어요:

alias Fun = int(string);
int fun(string) {return 0;}
static assert(is(typeof(fun) == Fun));

alias MemberFun1 = int() const;
alias MemberFun2 = const int();
// leading attributes apply to the func, not the return type
static assert(is(MemberFun1 == MemberFun2));

타입 별명은 함수를 다른 기본 인자로 호출하거나, 인자를 필수에서 기본으로(또는 그 반대로) 바꿀 때도 쓸 수 있어요:

import std.stdio : writeln;

void fun(int v = 6)
{
    writeln("v: ", v);
}

void main()
{
    fun();  // prints v: 6

    alias Foo = void function(int=7);
    Foo foo = &fun;
    foo();  // prints v: 7
    foo(8); // prints v: 8
}
import std.stdio : writefln;

void main()
{
    fun(4);          // prints a: 4, b: 6, c: 7

    Bar bar = &fun;
    //bar(4);           // compilation error, because the `Bar` alias
                        // requires an explicit 2nd argument
    bar(4, 5);          // prints a: 4, b: 5, c: 9
    bar(4, 5, 6);       // prints a: 4, b: 5, c: 6

    Baz baz = &fun;
    baz();              // prints a: 2, b: 3, c: 4
}

alias Bar = void function(int, int, int=9);
alias Baz = void function(int=2, int=3, int=4);

void fun(int a, int b = 6, int c = 7)
{
    writefln("a: %d, b: %d, c: %d", a, b, c);
}

별명 할당 (Alias Assign)

AliasAssign:
    Identifier = Type

AliasDeclaration에는 AliasAssign으로 새로운 값을 할당할 수 있어요:

template Gorgon(T)
{
    alias A = long;
    A = T; // assign new value to A
    alias Gorgon = A;
}
pragma(msg, Gorgon!int); // prints int
  • AliasAssign과 그에 대응하는 AliasDeclaration은 반드시 같은 TemplateDeclaration 안에 선언되어야 해요.
  • 대응하는 AliasDeclarationAliasAssign보다 어휘적으로 앞에 나타나야 해요.
  • 대응하는 AliasDeclaration은 오버로드된 심볼을 가리킬 수 없어요.
  • AliasDeclaration 또는 AliasAssign의 좌변 값(lvalue)은, 그 lvalue에 대한 다른 AliasAssign의 우변을 제외하고는, 그보다 앞서 사용될 수 없어요.

모범 사례 (Best Practices): AliasAssign은 재귀 계산보다 반복 계산을 할 때 특히 유용해요. 재귀 방식이 만들어내는 수많은 중간 템플릿을 피할 수 있거든요.

import std.meta : AliasSeq;

static if (0) // recursive method for comparison
{
    template Reverse(T...)
    {
        static if (T.length == 0)
            alias Reverse = AliasSeq!();
        else
            alias Reverse = AliasSeq!(Reverse!(T[1 .. T.length]), T[0]);
    }
}
else // iterative method minimizes template instantiations
{
    template Reverse(T...)
    {
        alias A = AliasSeq!();
        static foreach (t; T)
            A = AliasSeq!(t, A); // Alias Assign
        alias Reverse = A;
    }
}

enum X = 3;
alias TK = Reverse!(int, const uint, X);
pragma(msg, TK); // prints tuple(3, (const(uint)), (int))

별명 재할당 (Alias Reassignment)

AliasReassignment:
    Identifier = StorageClassesopt Type
    Identifier = FunctionLiteral
    Identifier = StorageClassesopt BasicType Parameters MemberFunctionAttributesopt

템플릿 안의 별명 선언에는 새로운 값을 재할당할 수 있어요.

import std.meta : AliasSeq;

template staticMap(alias F, Args...)
{
    alias A = AliasSeq!();
    static foreach (Arg; Args)
        A = AliasSeq!(A, F!Arg); // alias reassignment
    alias staticMap = A;
}

enum size(T) = T.sizeof;
static assert(staticMap!(size, char, wchar, dchar) == AliasSeq!(1, 2, 4));

Identifier는 어휘적으로 앞서는 AliasDeclaration을 가리켜야 해요. 둘 다 같은 TemplateDeclaration의 멤버여야 해요.

AliasReassignment의 우변은 AliasDeclaration의 우변을 대체해요.

일단 AliasDeclarationAliasReassignment의 우변이 아닌 어떤 맥락에서라도 참조된 뒤에는 더 이상 재할당할 수 없어요.

이유 (Rationale): 별명 재할당은 컴파일 시간을 단축하고 메모리 소모를 낮출 수 있어요. 그리고 재귀 방식의 대안보다 훨씬 단순한 코드만 필요해요.

외부 선언 (Extern Declarations)

저장 클래스 extern이 붙은 변수 선언은 모듈 안에서 저장 공간을 할당받지 않아요. 그 변수는 이름이 맞는 다른 오브젝트 파일 어딘가에서 정의되어 링크되어야 해요.

extern 선언에는 선택적으로 extern 링크 속성이 이어질 수 있어요. 링크 속성이 없으면 기본값은 extern(D)예요:

// variable allocated and initialized in this module with C linkage
extern(C) int foo;

// variable allocated outside this module with C linkage
// (e.g. in a statically linked C library or another module)
extern extern(C) int bar;

모범 사례 (Best Practices): 외부 선언의 주된 용도는 C나 C++ 파일에 있는 전역 변수 선언과 함수를 연결하는 거예요.

타입 한정자 vs. 저장 클래스 (Type Qualifiers vs. Storage Classes)

타입 한정자(type qualifier)와 저장 클래스는 서로 다른 개념이에요.

타입 한정자는 기존의 기본 타입에서 파생 타입을 만들어내요. 그리고 그 결과 타입은 그 타입의 여러 인스턴스를 만드는 데 쓰일 수 있어요. 예를 들어 immutable 타입 한정자로 불변 타입의 변수를 만들 수 있어요:

immutable(int)   x; // typeof(x) == immutable(int)
immutable(int)[] y; // typeof(y) == immutable(int)[]
                    // typeof(y[0]) == immutable(int)

// Type constructors create new types that can be aliased:
alias ImmutableInt = immutable(int);
ImmutableInt z;     // typeof(z) == immutable(int)

반면 저장 클래스는 새로운 타입을 만들지 않아요. 다만 선언되는 변수나 함수가 사용하는 저장 공간의 종류만을 설명할 뿐이에요. 예를 들어 멤버 함수에 const 저장 클래스를 붙이면 그 함수가 묵시적인 this 인자를 수정하지 않는다는 뜻이 돼요:

struct S
{
    int x;
    int method() const
    {
        //x++;    // Error: this method is const and cannot modify this.x
        return x; // OK: we can still read this.x
    }
}

일부 키워드는 타입 한정자와 저장 클래스 양쪽으로 쓰일 수 있어요. 하지만 ref처럼 새 타입을 만드는 데는 쓸 수 없는 저장 클래스도 있어요.

ref 저장 클래스 (ref Storage Class)

ref로 선언된 매개변수는 참조로 전달돼요:

void func(ref int i)
{
    i++; // modifications to i will be visible in the caller
}

void main()
{
    auto x = 1;
    func(x);
    assert(x == 2);

    // However, ref is not a type qualifier, so the following is illegal:
    //ref(int) y; // Error: ref is not a type qualifier.
}

함수 자체도 ref로 선언할 수 있어요. 그러면 그 함수의 반환 값이 참조로 전달돼요:

ref int func2()
{
    static int y = 0;
    return y;
}

void main()
{
    func2() = 2; // The return value of func2() can be modified.
    assert(func2() == 2);

    // However, the reference returned by func2() does not propagate to
    // variables, because the 'ref' only applies to the return value itself,
    // not to any subsequent variable created from it:
    auto x = func2();
    static assert(is(typeof(x) == int)); // N.B.: *not* ref(int);
                                     // there is no such type as ref(int).
    x++;
    assert(x == 3);
    assert(func2() == 2); // x is not a reference to what func2() returned; it
                          // does not inherit the ref storage class from func2().
}

ref 변수 (ref Variables)

버전 2.111부터 ref로 지역·정적·외부·전역 변수를 선언할 수 있어요.

struct S { int a; }

void main()
{
    S s;
    ref int r = s.a;
    r = 3;
    assert(s.a == 3);
}

모범 사례 (Best Practices): 포인터 연산이 필요 없을 때는 포인터 대신 ref 변수를 사용해요.

auto ref도 지역·정적·외부·전역 변수를 선언하는 데 쓸 수 있어요.

void f()
{
    auto ref x = 0;
    auto ref y = x;
    static assert(!__traits(isRef, x));
    static assert( __traits(isRef, y));
}

auto ref 템플릿 함수 매개변수에 대한 내용도 참고해요.

한정된 타입을 반환하는 메서드 (Methods Returning a Qualified Type)

const 같은 일부 키워드는 타입 한정자와 저장 클래스 양쪽으로 쓰일 수 있어요. 둘 중 무엇인지는 그 키워드가 나타나는 문법에 따라 정해져요.

struct S
{
    /* Is const here a type qualifier or a storage class?
     * Is the return value const(int), or is this a const function that returns
     * (mutable) int?
     */
    const int* func() // a const function
    {
        //++p;          // error, this.p is const
        //return p;     // error, cannot convert const(int)* to int*
        return null;
    }

    const(int)* func() // a function returning a pointer to a const int
    {
        ++p;          // ok, this.p is mutable
        return p;     // ok, int* can be implicitly converted to const(int)*
    }

    int* p;
}

모범 사례 (Best Practices): 혼동을 피하려면 반환 타입에는 괄호를 쓰는 타입 한정자 문법을, 함수 저장 클래스는 반환 타입과 시각적으로 혼동될 수 있는 좌변이 아니라 선언의 오른쪽에 쓰는 게 좋아요:

struct S
{
    // Now it is clear that the 'const' here applies to the return type:
    const(int) func1() { return 1; }

    // And it is clear that the 'const' here applies to the function:
    int func2() const { return 1; }
}

더 알아보기 (Learn more)