식 (Expressions)

식(expression)은 평가(evaluation)를 지정하는 연산자와 피연산자의 나열이에요. 식의 문법, 평가 순서, 의미(semantics)를 다루는 페이지로, D에서 값을 계산하고 할당·검사·무시하는 데 쓰이는 모든 연산자 표현식을 설명해요.

출처: Expressions

본문

식 (Expressions)

Expression:
    CommaExpression

식은 평가를 지정하는 연산자와 피연산자의 나열이에요. 식의 문법과 평가 순서, 의미는 다음과 같아요.

식은 결과 타입을 가진 값을 계산하는 데 쓰여요. 이 값들은 할당되거나, 검사되거나, 무시될 수 있고, 식은 부수 효과(side effect)를 가질 수도 있어요.

정의와 용어 (Definitions and Terms)

완전 식 (Full Expression)

어떤 식 expr에 대해, expr의 완전 식(full expression)은 다음과 같이 정의돼요. expr이 다른 식 expr1의 부분식(subexpression)으로 파싱된다면, expr의 완전 식은 expr1의 완전 식이에요. 그렇지 않으면, expr은 자기 자신의 완전 식이에요.

각 식은 유일한 완전 식을 가져요. 예:

return f() + g() * 2;

위에서 g() * 2의 완전 식은 f() + g() * 2이지만, f() + g()의 완전 식은 아니에요. 후자는 부분식으로 파싱되지 않기 때문이에요.

참고: 정의는 단순하지만, 함수 리터럴과 관련해 약간의 미묘함이 있어요:

return (() => x + f())() * g();

위에서 f()의 완전 식은 x + f()이고, return에 전달된 식이 아니에요. x + f()의 부모가 함수 리터럴 타입이지 식 타입이 아니기 때문이에요.

Lvalue

다음 식들(그리고 오직 그것들만)을 lvalue 식, 즉 lvalue라고 불러요:

  • struct와 union 멤버 함수 안의 this;
  • 변수, 함수 이름, 또는 참조로 반환하는 함수의 호출;
  • PostfixExpression모듈 스코프 연산자의 결과 중 점 오른쪽 끝이 변수, 필드(직접 또는 static), 함수 이름, 또는 참조로 반환하는 함수의 호출일 때;
  • 다음 식들의 결과:
    • 내장 단항 연산자 +(lvalue에 적용될 때), *, ++(접두만), --(접두만);
    • 내장 인덱싱 연산자 [](하지만 슬라이싱 연산자는 아님);
    • 내장 대입 연산자, 즉 =, +=, *=, /=, %=, &=, |=, ^=, ~=, <<=, >>=, >>>=, ^^=;
    • 사용자 정의 연산자 — 단, 낮춤(lowering)의 결과로 호출된 함수가 참조로 반환하는 경우에만;
    • 다음 상황의 ConditionalExpression 연산자 e ? e1 : e2:
      • e1e2가 같은 타입의 lvalue이거나;
      • e1e2 중 하나가 타입 T의 lvalue이고, 다른 하나가 그것을 T의 lvalue로 변환하는 alias this를 가질 때;
    • 믹스인 식 — 단, mixin 인자들을 컴파일한 결과식의 컴파일이 lvalue인 경우에만;
    • 타입 T의 lvalue에 적용된 cast(U) 식 — 단, T*U*로 암시적으로 변환될 수 있을 때;
    • cast(TypeCtorsopt) — lvalue에 적용될 때.

Rvalue

lvalue가 아닌 식은 rvalue예요. Rvalue에는 모든 리터럴, __FILE__·__LINE__ 같은 특수 값 키워드, enum 값, RvalueExpression, 그리고 위에서 lvalue로 정의되지 않은 식의 결과가 포함돼요.

내장 주소 연산자(단항 &)는 lvalue에만 적용할 수 있어요.

ref 선언은 lvalue에만 바인딩돼요.

ref r = 1; // error
enum e = 1;
int* p = &e; // error

최소 단락 식 (Smallest Short-Circuit Expression)

완전 식 fullexpr의 부분식인 식 expr이 주어졌을 때, 최소 단락 식(smallest short-circuit expression, 있다면)은 expr이 그 부분식이 되도록 하는, AndAndExpression(&&) 또는 OrOrExpression(||)인 fullexpr의 가장 짧은 부분식 scexpr이에요. 예:

((f() * 2 && g()) + 1) || h()

위 부분식 f() * 2의 최소 단락 식은 f() * 2 && g()이에요. 예:

(f() && g()) + h()

위 부분식 h()는 최소 단락 식이 없어요.

평가 순서 (Order Of Evaluation)

Best Practices: 평가 순서가 잘 정의되어 있더라도, 그것에 의존하는 코드를 쓰는 것은 권장되지 않아요.

증가·감소 (Increment and Decrement)

내장 접두 단항 식 ++--는 다음처럼 대입으로 낮춰진(rewritten) 것처럼 평가돼요:

Expression Equivalent
++expr ((expr) += 1)
--expr ((expr) -= 1)

따라서 접두 ++--의 결과는 부수 효과가 적용된 후의 lvalue예요.

내장 접미 단항 식 ++--는 다음처럼 람다 호출로 낮춰진 것처럼 평가돼요:

Expression Equivalent
expr++ (ref x){auto t = x; ++x; return t;}(expr)
expr-- (ref x){auto t = x; --x; return t;}(expr)

따라서 접미 ++--의 결과는 부수 효과가 적용되기 직전의 rvalue예요.

int i = 0;
assert(++i == 1);
assert(i++ == 1);
assert(i == 2);

int* p = [1, 2].ptr;
assert(*p++ == 1);
assert(*p == 2);

이항 식 (Binary Expressions)

AssignExpression, OrOrExpression, AndAndExpression을 제외한 이항 식은 어휘 순서(왼쪽에서 오른쪽)로 평가돼요. 예:

int i = 2;
i = ++i * i++ + i;
assert(i == 3 * 3 + 4);

OrOrExpressionAndAndExpression은 왼쪽 피연산자를 먼저 평가해요. 그리고 OrOrExpression은 왼쪽이 0이 아닌 값으로 평가되지 않을 때에만 오른쪽을 평가하고, AndAndExpression은 왼쪽이 0이 아닌 값으로 평가될 때에만 오른쪽을 평가해요.

구현 정의(Implementation Defined): AssignExpression의 피연산자 평가 순서.

조건 식 (Conditional Expressions)

ConditionalExpression은 왼쪽 피연산자를 먼저 평가해요. 결과가 0이 아니면 두 번째 피연산자가 평가되고, 아니면 세 번째 피연산자가 평가돼요.

함수 호출 (Function Calls)

extern(D) 링키지(기본 링키지)를 가진 함수 호출은 다음 순서로 평가돼요:

  1. 필요하다면 호출할 함수의 주소를 평가해요(예: 계산된 함수 포인터나 delegate의 경우).
  2. 인자들을 왼쪽에서 오른쪽으로 평가해요.
  3. 실행이 함수로 전달돼요.

함수 포인터를 호출하는 예:

void function(int a, int b, int c) fun()
{
    writeln("fun() called");
    static void r(int a, int b, int c) { writeln("callee called"); }
    return &r;
}
int f1() { writeln("f1() called"); return 1; }
int f2() { writeln("f2() called"); return 2; }
int f3(int x) { writeln("f3() called"); return x + 3; }
int f4() { writeln("f4() called"); return 4; }

// evaluates fun() then f1() then f2() then f3() then f4()
// after which control is transferred to the callee
fun()(f1(), f3(f2()), f4());

구현 정의(Implementation Defined): extern(D) 외의 링키지를 가진 함수의 인자 평가 순서.

임시값의 수명 (Lifetime of Temporaries)

식과 문장은 rvalue를 만들거나 소비할 수 있어요. 이런 값을 임시값(temporary)이라고 부르며, 이름이나 보이는 스코프가 없어요. 그 수명은 이 절에 정의된 대로 자동으로 관리돼요.

임시값을 산출하는 각 평가에 대해, 그 임시값의 수명은 평가 시점에 시작돼요. 이는 식으로 초기화된 평범한 이름 있는 값의 생성과 유사해요.

임시값의 수명 종료는 일반적인 스코프 규칙을 따르지 않고 다음과 같이 정의돼요:

  • 만약 완전 식이 최소 단락 식 expr을 가지고 그리고 임시값이 && 또는 || 연산자의 오른쪽에서 생성되며 그리고 그 오른쪽이 평가된다면, 임시값의 소멸자들은 오른쪽 식이 평가되어 bool로 변환된 직후 평가돼요. 소멸자 평가는 생성 역순으로 진행돼요.
  • 그 밖의 모든 경우, 함수 호출 목적으로 생성된 임시값들은 완전 식의 끝으로 지연돼요. 소멸 순서는 생성 순서의 역순이에요.
  • 식의 어떤 부분식이 예외를 던지면, 그 부분식 평가까지 생성된 모든 임시값은 위 규칙에 따라 소멸돼요. 아직 생성되지 않은 임시값에 대해서는 소멸자 호출이 발행되지 않아요.

참고: 이 규칙의 직관은, 임시값의 소멸자가 완전 식의 끝에 생성 역순으로 지연된다는 것인데, 예외적으로 &&||의 오른쪽은 더 큰 식의 일부일 때조차 자체 완전 식으로 간주된다는 점이에요.

참고: ConditionalExpression e1 ? e2 : e3은 조건부로 식을 평가하지만 특별한 경우가 아니에요. e1e2·e3 중 하나가 임시값을 만들 수 있는데, 그 소멸자들은 생성 역순으로 완전 식의 끝에 삽입돼요.

예:

import std.stdio;

struct S
{
    int x;
    this(int n) { x = n; writefln("S(%s)", x); }
    ~this() { writefln("~S(%s)", x); }
}

void main()
{
    bool b = (S(1) == S(2) || S(3) != S(4)) && S(5) == S(6);
}

위 코드의 출력은:

S(1)
S(2)
S(3)
S(4)
~S(4)
~S(3)
S(5)
S(6)
~S(6)
~S(5)
~S(2)
~S(1)

먼저 S(1)S(2)가 어휘 순서로 평가돼요. 규칙에 따라 그들은 완전 식의 끝에 역순으로 소멸돼요. 비교 S(1) == S(2)는 false를 산출하므로 ||의 오른쪽이 평가되어 S(3)S(4)가 역시 어휘 순서로 평가돼요. 그러나 이들의 소멸은 완전 식의 끝으로 지연되지 않아요. 대신 S(4)S(3)|| 식의 끝에서 소멸돼요. 그 소멸 후 S(5)S(6)이 어휘 순서로 생성돼요. 다시 그들은 완전 식 끝이 아니라 && 식 바로 끝에서 소멸돼요. 결과적으로 S(6)·S(5)의 소멸이 S(2)·S(1)보다 먼저 수행돼요.

콤마 식 (Comma Expression)

CommaExpression:
    AssignExpression
    CommaExpression , AssignExpression

,의 왼쪽 피연산자가 평가된 다음 오른쪽 피연산자가 평가돼요. C에서는 콤마 식의 결과가 오른쪽 피연산자의 결과인데, D에서는 콤마 식의 결과를 사용하는 것이 허용되지 않아요. 결과적으로 콤마 식은 각 피연산자가 부수 효과를 가질 때에만 유용해요.

int x, y;
// expression statement
x = 1, y = 1;
// evaluate a comma expression at the end of each loop iteration
for (; y < 10; x++, y *= 2)
    writefln("%s, %s", x, y);

이유(Rationale): 콤마 식은 괄호 중첩 실수나, 단일 식 대신 인자들의 나열을 기대한 경우 등으로 의도치 않게 사용돼 왔어요. 그런 버그는 코드 리뷰에서 발견하기 어려울 수 있어요. 결과 사용을 금지하면 그런 버그가 오류로 바뀌어요.

대입 식 (Assign Expressions)

AssignExpression:
    ConditionalExpression
    ConditionalExpression = AssignExpression
    ConditionalExpression += AssignExpression
    ConditionalExpression -= AssignExpression
    ConditionalExpression *= AssignExpression
    ConditionalExpression /= AssignExpression
    ConditionalExpression %= AssignExpression
    ConditionalExpression &= AssignExpression
    ConditionalExpression |= AssignExpression
    ConditionalExpression ^= AssignExpression
    ConditionalExpression ~= AssignExpression
    ConditionalExpression <<= AssignExpression
    ConditionalExpression >>= AssignExpression
    ConditionalExpression >>>= AssignExpression
    ConditionalExpression ^^= AssignExpression

모든 대입 식에서 왼쪽 피연산자는 수정 가능한 lvalue여야 해요. 대입 식의 타입은 왼쪽 피연산자의 타입이고, 결과는 대입이 발생한 후 왼쪽 피연산자의 값이에요. 결과 식은 수정 가능한 lvalue예요.

  • 미정의 동작(Undefined Behavior): 두 피연산자 중 하나가 참조 타입이고 다음 중 하나에 해당할 때:
    • 피연산자들이 부분적으로 겹치는 저장 공간을 가짐
    • 피연산자들의 저장 공간이 정확히 겹치지만 타입이 다름
  • 구현 정의(Implementation Defined): 두 피연산자 모두 참조 타입이 아니고 다음 중 하나에 해당할 때:
    • 피연산자들이 부분적으로 겹치는 저장 공간을 가짐
    • 피연산자들의 저장 공간이 정확히 겹치지만 타입이 다름

단순 대입 식 (Simple Assignment Expression)

연산자가 =이면 단순 대입이에요.

  • 왼쪽 피연산자가 opAssign을 정의한 struct라면, 그 동작은 오버로드된 함수에 의해 정의돼요.
  • 왼쪽·오른쪽 피연산자가 같은 struct 타입이고 그 struct가 Postblit을 가진다면, 복사 연산은 Struct Postblit에 설명된 대로예요.
  • lvalue가 동적 배열의 .length 속성이라면, 동작은 동적 배열 길이 설정에 설명된 대로예요.
  • 왼쪽 피연산자가 슬라이스 식이라면, 동작은 배열 복사배열 채우기에 설명된 대로예요.
  • lvalue가 배열이라면, 동작은 배열 대입에 설명된 대로예요.
  • lvalue가 사용자 정의 속성이라면, 동작은 속성 함수에 설명된 대로예요.
  • 그 외에는 오른쪽 피연산자가 왼쪽 피연산자의 타입으로 암시적으로 변환되어 대입돼요.

대입 연산자 식 (Assignment Operator Expressions)

내장 타입의 인자에 대해, 대입 연산자 식 예컨대

a op= b

은 의미상 다음과 동등해요:

a = cast(typeof(a))(a op b)

단, 다음 예외가 있어요:

  • 피연산자 a는 한 번만 평가됨
  • op 오버로딩은 op= 오버로딩과 다른 함수를 사용함
  • >>>=의 왼쪽 피연산자는 시프트 전에 정수 확장(Integer Promotions)을 거치지 않음

축소 변환(narrowing conversions)은 허용돼요. 절단 변환(truncating conversions)은 오류가 돼요.

void f(short s)
{
    byte b;
    b += s; // OK, though it may overflow
    //b += 1.5F; // Deprecated, truncation
}

사용자 정의 타입에 대해, 대입 연산자 식은 이항 연산자와 별도로 오버로드돼요. 그래도 왼쪽 피연산자는 lvalue여야 해요.

조건 식 (Conditional Expressions)

ConditionalExpression:
    OrOrExpression
    OrOrExpression ? Expression : ConditionalExpression

첫 번째 식은 bool로 변환되어 평가돼요.

  • true면 두 번째 식이 평가되고, 그 결과가 조건 식의 결과가 돼요.
  • false면 세 번째 식이 평가되고, 그 결과가 조건 식의 결과가 돼요.
  • 두 번째나 세 번째 식 중 하나가 void 타입이면 결과 타입은 void예요. 그렇지 않으면 두 식은 공통 타입으로 암시적으로 변환되고 그것이 조건 식의 결과 타입이 돼요.

참고: 조건 식이 대입 식의 왼쪽 피연산자일 때는 모호성 해소를 위해 괄호가 필요해요:

bool test;
int a, b, c;
...
test ? a = b : c = 2;   // error
(test ? a = b : c) = 2; // OK

이렇게 하면 의도가 명확해져요. 첫 문장은 다음 코드로 오독하기 쉽기 때문이에요:

test ? a = b : (c = 2);

논리 식 (Logical Expressions)

참고(!expr에 대해서는 UnaryExpression 참조).

OrOr 식

OrOrExpression:
    AndAndExpression
    OrOrExpression || AndAndExpression

OrOrExpression의 결과 타입은 bool이에요. 단, 오른쪽 피연산자가 void 타입이면 결과는 void예요.

OrOrExpression은 왼쪽 피연산자를 평가해요.

왼쪽 피연산자가 bool로 변환되어 true로 평가되면 오른쪽 피연산자는 평가되지 않아요. OrOrExpression의 결과 타입이 bool이면 식의 결과는 true예요.

왼쪽 피연산자가 false이면 오른쪽 피연산자가 평가돼요. OrOrExpression의 결과 타입이 bool이면 식의 결과는 오른쪽 피연산자를 bool로 변환한 값이에요.

AndAnd 식

AndAndExpression:
    OrExpression
    AndAndExpression && OrExpression

AndAndExpression의 결과 타입은 bool이에요. 단, 오른쪽 피연산자가 void 타입이면 결과는 void예요.

AndAndExpression은 왼쪽 피연산자를 평가해요.

왼쪽 피연산자가 bool로 변환되어 false로 평가되면 오른쪽 피연산자는 평가되지 않아요. AndAndExpression의 결과 타입이 bool이면 식의 결과는 false예요.

왼쪽 피연산자가 true이면 오른쪽 피연산자가 평가돼요. AndAndExpression의 결과 타입이 bool이면 식의 결과는 오른쪽 피연산자를 bool로 변환한 값이에요.

비트 식 (Bitwise Expressions)

비트 식은 그 피연산자에 비트 연산을 수행해요. 피연산자는 정수 타입이어야 해요. 먼저 일반 산술 변환(Usual Arithmetic Conversions)이 수행된 다음 비트 연산이 수행돼요.

참고: OrExpression, XorExpression, AndExpression이 ShiftExpression, ComplementExpression을 참조하세요.

참고: OrExpression, XorExpression, AndExpression이 EqualExpression, IdentityExpression, RelExpression의 양쪽에 나타나면 컴파일 오류예요. 대신 괄호를 사용해 명확히 하세요.

int x, a, b;
x = a & 5 == b; // error
x = a & 5 is b; // error
x = a & 5 <= b; // error

x = (a & 5) == b; // OK
x = a & (5 == b); // OK

Or 식

OrExpression:
    XorExpression
    OrExpression | XorExpression

피연산자들이 OR로 결합돼요.

Xor 식

XorExpression:
    AndExpression
    XorExpression ^ AndExpression

피연산자들이 XOR로 결합돼요.

And 식

AndExpression:
    CmpExpression
    AndExpression & CmpExpression

피연산자들이 AND로 결합돼요.

비교 식 (Compare Expressions)

CmpExpression:
    EqualExpression
    IdentityExpression
    RelExpression
    InExpression
    ShiftExpression

동등 식 (Equality Expressions)

EqualExpression:
    ShiftExpression == ShiftExpression
    ShiftExpression != ShiftExpression

동등 식은 두 피연산자를 동등(==) 또는 부등(!=)으로 비교해요. 결과의 타입은 bool이에요. 부등은 동등의 논리적 부정으로 정의돼요.

  • 피연산자가 정수 값이라면 비교 전에 일반 산술 변환을 적용해 공통 타입으로 만들고, 동등은 공통 타입의 비트 패턴이 정확히 일치하는 것으로 정의돼요.
  • 피연산자가 포인터라면 동등은 피연산자의 비트 패턴이 정확히 일치하는 것으로 정의돼요. 두 타입이 일치해야 하거나, 하나가 typeof(null)일 수 있어요.
  • float, double, real 값에 대해서는 비교 전에 일반 산술 변환을 적용해 공통 타입으로 만들어요. -0+0은 동등한 것으로 간주돼요. 어느 한쪽 또는 양쪽이 NaN이면 ==는 false, !=는 true를 반환해요. 그 외에는 공통 타입의 비트 패턴으로 동등을 비교해요.
  • static·dynamic 배열에 대해 동등은 배열 길이가 일치하고 각 요소가 동등한지 비교하는 것으로 정의돼요. 요소 타입은 공통 타입을 가져야 해요.
assert(5 == 5L);
assert(byte(4) == 4F);

int i = 1, j = 1;
assert(&i != &j);
assert(&i != null);

// elements of different types are comparable, even when different sizes
int[] ia = ['A', 'B', 'C'];
assert(ia == "ABC");
byte[] ba = [1, 2];
assert(ba == [1F, 2F]);

Deprecated: 복소수에 대해 동등은 다음과 동등한 것으로 정의돼요:

x.re == y.re && x.im == y.im
클래스 & struct 동등 (Class & Struct Equality)

클래스 참조에 대해, a == b.object.opEquals(a, b)로 다시 쓰여지는데, 이것은 null을 처리해요. 이는 두 객체의 내용을 비교하려는 의도지만, 이를 위해선 적절한 opEquals 메서드 오버라이드가 정의되어야 해요. 루트 Object 클래스가 제공하는 기본 opEqualsis 연산자와 동등해요.

struct 객체에 대해, 식 (a == b)a.opEquals(b)로 다시 쓰여지고, 그것이 실패하면 b.opEquals(a)로 다시 쓰여져요.

클래스 참조와 struct 객체 모두에 대해 (a != b)!(a == b)로 다시 쓰여져요.

자세한 내용은 opEquals를 참조하세요.

Struct 동등 (Struct Equality)

struct 객체에 대해 동등은 opEquals() 멤버 함수의 결과를 의미해요. opEquals()가 제공되지 않으면 하나가 생성돼요. 동등은 해당 객체 필드들의 모든 동등 결과의 논리곱으로 정의돼요.

struct S
{
    int i = 4;
    string s = "four";
}

S s;
assert(s == S());
s.s = "foul";
assert(s != S());

구현 정의(Implementation Defined): struct 객체의 정렬 틈새(alignment gaps)의 내용.

겹치는 필드가 있다면(union에서 발생), 기본 동등은 겹치는 각 필드들을 비교해요.

Best Practices: opEquals()는 겹치는 필드 중 어떤 것이 유효한 데이터를 담고 있는지 고려할 수 있어요. opEquals()는 부동소수점 NaN 값이 항상 부등으로 비교되는 기본 동작을 재정의할 수 있어요. 다음의 경우 memcmp()opEquals()를 구현할 때 주의하세요:

  • 정렬 틈새가 있는 경우
  • 어떤 필드가 opEquals()를 가진 경우
  • NaN 또는 -0 값을 담을 수 있는 부동소수점 필드가 있는 경우

동일성 식 (Identity Expressions)

IdentityExpression:
    ShiftExpression is ShiftExpression
    ShiftExpression ! is ShiftExpression

is 연산자는 식 값의 동일성(identity)을 비교해요. 비동일성을 비교하려면 e1 !is e2를 사용하세요. 결과 타입은 bool이에요. 피연산자들은 비교 전에 일반 산술 변환을 거쳐 공통 타입으로 만들어져요.

클래스/인터페이스 객체에 대해 동일성은 객체 참조가 동일한 것으로 정의돼요. 클래스 참조는 is로 null과 효율적으로 비교할 수 있어요. 인터페이스 객체는 캐스트된 클래스와 같은 참조를 가질 필요가 없다는 점을 주의하세요. 인터페이스가 다른 인터페이스/클래스 값과 클래스 인스턴스를 공유하는지 검사하려면, is로 비교하기 전에 두 피연산자를 Object로 캐스트하세요.

interface I { void g(); }
interface I1 : I { void g1(); }
interface I2 : I { void g2(); }
interface J : I1, I2 { void h(); }

class C : J
{
    override void g() { }
    override void g1() { }
    override void g2() { }
    override void h() { }
}

void main() @safe
{
    C c = new C;
    I i1 = cast(I1) c;
    I i2 = cast(I2) c;
    assert(i1 !is i2); // not identical
    assert(c !is i2); // not identical
    assert(cast(Object) i1 is cast(Object) i2); // identical
}

struct 객체와 부동소수점 값에 대해 동일성은 피연산자들의 비트가 동일한 것으로 정의돼요.

static·dynamic 배열에 대해 두 배열의 동일성은 두 배열이 같은 메모리 위치를 가리키고 같은 개수의 요소를 담을 때 성립돼요.

Object o;
assert(o is null);

auto a = [1, 2];
assert(a is a[0..$]);
assert(a !is a[0..1]);

auto b = [1, 2];
assert(a !is b);

Deprecated: 주소와 길이로 static 배열을 비교하는 데 is를 쓰는 것은 권장되지 않아요. 그렇게 하려면 슬라이스 연산자를 써서 배열의 슬라이스를 비교하세요. 예: a1[] is a2[].

다른 피연산자 타입에 대해 동일성은 동등과 같은 것으로 정의돼요.

동일성 연산자 is는 오버로드할 수 없어요.

관계 식 (Relational Expressions)

RelExpression:
    ShiftExpression < ShiftExpression
    ShiftExpression <= ShiftExpression
    ShiftExpression > ShiftExpression
    ShiftExpression >= ShiftExpression

먼저 피연산자에 일반 산술 변환이 수행돼요. 관계 식의 결과 타입은 bool이에요.

두 피연산자가 모두 포인터라면 호환 타입을 가리켜야 해요. 또한 같은 메모리 객체, 또는 같은 메모리 객체 바로 다음의 메모리 위치를 가리켜야 해요.

배열 비교 (Array Comparisons)

static·dynamic 배열에 대해, CmpExpression의 결과는 배열의 첫 번째로 같지 않은 요소에 연산자를 적용한 결과예요. 두 배열이 동등하게 비교되지만 길이가 다르다면, 더 짧은 배열이 더 긴 배열보다 "작은" 것으로 비교돼요.

정수 비교 (Integer Comparisons)

정수 비교는 두 피연산자가 모두 정수 타입일 때 발생해요.

정수 비교 연산자:

Operator Relation
< less
> greater
<= less or equal
>= greater or equal
== equal
!= not equal

<, <=, >, >= 식에서 한 피연산자가 부호 있고 다른 하나가 부호 없는 것은 오류예요. 캐스트로 두 피연산자를 모두 부호 있게 또는 모두 부호 없게 만드세요.

부동소수점 비교 (Floating Point Comparisons)

한쪽 또는 양쪽 피연산자가 부동소수점이면 부동소수점 비교가 수행돼요.

CmpExpression은 NaN 피연산자를 가질 수 있어요. 한쪽 또는 양쪽이 NaN이면 부동소수점 비교 연산은 다음과 같이 반환돼요:

Operator Relation Returns
< less false
> greater false
<= less or equal false
>= greater or equal false
== equal false
!= unordered, less, or greater true

Best Practices: IdentityExpression으로 T.nan을 검사할 수 있지만, 런타임에서 생성되는 NaN 부동소수점 값은 다른 것도 있어요. 모두 처리하려면 std.math.traits.isNaN을 사용하세요.

클래스와 struct 비교 (Class and Struct Comparisons)

struct 객체에 대해, RelExpression은 먼저 일치하는 opCmp 메서드 호출을 평가하는 비교를 수행해요.

클래스 참조에 대해, RelExpression은 다음 중 하나인 int로 평가되는 비교를 수행해요:

  • 0 — 두 객체 참조가 동일할 때
  • -1 — 왼쪽 식이 null일 때
  • 1 — 오른쪽 식이 null일 때
  • 일치하는 opCmp 호출의 결과
class C
{
    override int opCmp(Object o) { assert(0); }
}

void main()
{
    C c;
    //if (c < null) {}  // compile-time error
    assert(c is null);
    assert(c < new C); // C.opCmp is not called
}

둘째, 클래스·struct 객체에 대해 평가된 int가 주어진 연산자로 0과 비교되어 RelExpression의 결과를 이뤄요. 자세한 내용은 opCmp를 참조하세요.

in 식 (In Expressions)

InExpression:
    ShiftExpression in ShiftExpression
    ShiftExpression ! in ShiftExpression

연관 배열 같은 컨테이너가 특정 키를 포함하는지 검사할 수 있어요:

int[string] foo;
...
if ("hello" in foo)
{
    // the string was found
}

InExpression의 결과는 연관 배열에 대한 포인터예요. 컨테이너에 일치하는 키가 없으면 포인터는 null이에요. 일치하면 포인터는 그 키와 연관된 값을 가리켜요.

!in 식은 in 연산의 논리적 부정이에요.

in 식은 관계 식 <, <= 등과 같은 우선순위를 가져요.

참고: in오버로드할 때는 보통 opBinaryRight만 정의돼요. 연산이 보통 키 타입이 아니라 in 연산자 오른쪽에 나타나는 컨테이너에 의해 정의되기 때문이에요.

시프트 식 (Shift Expressions)

ShiftExpression:
    AddExpression
    ShiftExpression << AddExpression
    ShiftExpression >> AddExpression
    ShiftExpression >>> AddExpression

피연산자는 정수 타입이어야 하고 정수 확장을 거쳐요. 결과 타입은 확장 후 왼쪽 피연산자의 타입이에요. 결과 값은 오른쪽 피연산자의 값만큼 비트를 시프트한 결과예요.

  • <<는 왼쪽 시프트예요.
  • >>는 부호 있는 오른쪽 시프트예요.
  • >>>는 부호 없는 오른쪽 시프트예요.

구현 정의(Implementation Defined): 음수 값만큼, 또는 시프트되는 양의 크기와 같거나 더 많은 비트만큼 시프트한 결과는 정의되지 않아요. 시프트 값이 컴파일 타임에 알려지면 그렇게 하는 것은 컴파일 오류가 돼요.

int c;

int s = -3;
auto y = c << s; // implementation defined value

auto x = c << 33;  // error, max shift count allowed is 31

가산 식 (Additive Expressions)

AddExpression:
    MulExpression
    AddExpression + MulExpression
    AddExpression - MulExpression
    AddExpression ~ MulExpression

가산 식 (Add Expressions)

가산 연산 +-의 경우:

  • 피연산자가 정수 타입이면 일반 산술 변환을 거친 뒤 일반 산술 변환으로 공통 타입으로 만들어져요.
  • 두 피연산자가 정수 타입이고 계산에서 오버플로나 언더플로가 발생하면 래핑(wrapping)이 일어나요. 예:
    • uint.max + 1 == uint.min
    • uint.min - 1 == uint.max
    • int.max + 1 == int.min
    • int.min - 1 == int.max
  • 어느 한쪽이 부동소수점 타입이면 다른 쪽이 암시적으로 부동소수점으로 변환되고 일반 산술 변환으로 공통 타입이 돼요.
  • 부동소수점 피연산자의 가산 식은 결합적(associative)이지 않아요.
포인터 산술 (Pointer Arithmetic)

첫 번째 피연산자가 포인터이고 두 번째가 정수 타입이면, 결과 타입은 첫 번째 피연산자의 타입이고 결과 값은 첫 번째 피연산자가 가리키는 타입의 크기만큼 두 번째 피연산자를 곱한 것을 더한(또는 뺀) 포인터예요.

int[] a = [1,2,3];
int* p = a.ptr;
assert(*p == 1);

*(p + 2) = 4; // same as `p[2] = 4`
assert(a[2] == 4);

IndexOperation은 포인터와 함께 사용될 수도 있고, 정수를 더한 다음 결과를 역참조하는 것과 같은 동작을 해요.

두 번째 피연산자가 포인터이고 첫 번째가 정수 타입이며 연산자가 +라면, 피연산자들이 뒤집혀서 방금 설명한 포인터 산술이 적용돼요.

포인터 산술로 포인터를 만드는 것은 @safe 코드에서 허용되지 않아요.

두 피연산자가 모두 포인터이고 연산자가 +라면 불법이에요.

두 피연산자가 모두 포인터이고 연산자가 -라면, 포인터들을 뺀 다음 그 결과를 피연산자들이 가리키는 타입의 크기로 나눠요. 이 계산에서 void의 가정된 크기는 1바이트예요. 포인터들이 다른 타입을 가리키면 오류예요. 결과의 타입은 ptrdiff_t예요. 두 피연산자는 호환 타입을 가리켜야 해요. 두 피연산자는 같은 메모리 객체, 또는 같은 메모리 객체 바로 다음의 메모리 위치를 가리켜야 해요.

int[] a = [1,2,3];
ptrdiff_t d = &a[2] - a.ptr;
assert(d == 2);

연결 식 (Cat Expressions)

가산 연산 ~의 경우:

CatExpression은 컨테이너의 데이터를 다른 데이터와 연결해 새 컨테이너를 만들어요.

동적 배열에 대해 다른 피연산자는 다른 배열이거나, 배열의 요소 타입으로 암시적으로 변환되는 단일 값이어야 해요. 배열 연결을 참조하세요.

곱 식 (Mul Expressions)

MulExpression:
    UnaryExpression
    MulExpression * UnaryExpression
    MulExpression / UnaryExpression
    MulExpression % UnaryExpression

피연산자는 산술 타입이어야 해요. 일반 산술 변환을 거쳐요.

정수 피연산자에 대해 *, /, %는 각각 곱셈, 나눗셈, 계수(modulus) 연산에 해당해요. 곱셈에서 오버플로는 무시되고 정수 타입에 맞게 잘라내요.

나눗셈 (Division)

정수 피연산자의 /% 연산자에서, 몫은 0 방향으로 반올림되고 나머지는 피제수(dividend)와 같은 부호를 가져요.

다음과 같은 나눗셈·계수 정수 피연산자:

  • 분모가 0인 경우
  • 부호 있는 int.min이 분자이고 -1이 분모인 경우
  • 부호 있는 long.min이 분자이고 -1L이 분모인 경우

는 컴파일 타임 실행 중 만나면 불법이에요.

미정의 동작(Undefined Behavior): 런타임에 만나면 발현돼요. core.checkedint로 검사하고 정의된 동작을 선택할 수 있어요.

부동소수점 (Floating Point)

부동소수점 피연산자에 대해 */ 연산은 IEEE 754 부동소수점 동등물에 해당해요. %는 IEEE 754 나머지와 같지 않아요. 예를 들어 15.0 % 10.0 == 5.0인 반면, IEEE 754에서는 remainder(15.0,10.0) == -5.0이에요.

부동소수점 피연산자의 곱 식은 결합적이지 않아요.

단항 식 (Unary Expressions)

UnaryExpression:
    & UnaryExpression
    ++ UnaryExpression
    -- UnaryExpression
    * UnaryExpression
    - UnaryExpression
    + UnaryExpression
    ! UnaryExpression
    ComplementExpression
    DeleteExpression
    CastExpression
    ThrowExpression
    PowExpression
Operator Description
& Take memory address of an lvalue — see pointers & order of evaluation
++ Increment before use
-- Decrement before use
* Dereference/indirection — typically for pointers
- Negative
+ Positive
! Logical NOT

단항 -+ 연산 전에 일반적인 정수 확장이 수행돼요.

보수 식 (Complement Expressions)

ComplementExpression:
    ~ UnaryExpression

ComplementExpression은 정수 타입(bool 제외)에 동작해요. 값의 모든 비트가 반전돼요. 보수 연산 전에 일반적인 정수 확장이 수행돼요.

삭제 식 (Delete Expressions)

DeleteExpression:
    delete UnaryExpression

Legacy: delete제거되었어요. 대신 가능하면 destroy를, 최후의 수단으로 core.memory.__delete를 사용하세요.

UnaryExpression이 클래스 객체 참조이고 그 클래스에 소멸자가 있으면, 그 객체 인스턴스에 대해 소멸자가 호출돼요.

다음으로, UnaryExpression이 클래스 객체 참조이거나 struct 인스턴스에 대한 포인터이고, 클래스나 struct가 오버로드된 연산자 delete를 가진다면, 그 클래스 객체 인스턴스나 struct 인스턴스에 대해 그 연산자 delete가 호출돼요.

그 외에는 가비지 컬렉터가 호출되어 클래스 인스턴스나 struct 인스턴스에 할당된 메모리를 즉시 해제해요.

UnaryExpression이 포인터나 동적 배열이면, 가비지 컬렉터가 호출되어 메모리를 즉시 해제해요.

delete 수행 후 포인터, 동적 배열, 참조는 null로 설정돼요. 삭제 후 다른 참조로 그 데이터를 참조하려는 시도는 미정의 동작을 초래해요.

UnaryExpression이 스택에 할당된 변수라면, 그 인스턴스에 대해 클래스 소멸자(있다면)가 호출돼요. 가비지 컬렉터는 호출되지 않아요.

미정의 동작(Undefined Behavior): 가비지 컬렉터가 할당하지 않은 메모리를 해제하는 데 delete를 사용하는 것. delete의 피연산자였던 데이터를 참조하는 것.

캐스트 식 (Cast Expressions)

CastExpression:
    cast ( Type ) UnaryExpression
    CastQual

CastExpression은 UnaryExpression을 Type으로 변환해요.

cast(foo) -p; // cast (-p) to type foo
(foo) - p;      // subtract p from foo
기본 데이터 타입 (Basic Data Types)

기본 타입에 대한 암시적 변환을 수행할 수 없는 상황에서는, 캐스트를 사용해 메모리 영역의 재해석을 타입 시스템이 받아들이도록 강제할 수 있어요.

이런 시나리오의 예는 더 넓은 타입을 더 좁은 타입에 저장하려는 경우로 대표돼요:

int a;
byte b = a; // cannot implicitly convert expression a of type int to byte

소스 타입이 목적 타입보다 넓을 때 캐스팅하면 값이 목적 타입 크기로 절단돼요.

int a = 64389; // 00000000 00000000 11111011 10000101
byte b = cast(byte) a;       // 10000101
ubyte c = cast(ubyte) a;     // 10000101
short d = cast(short) a;     // 11111011 10000101
ushort e = cast(ushort) a;   // 11111011 10000101

writeln(b);
writeln(c);
writeln(d);
writeln(e);

정수 타입에서 더 좁은 타입에서 더 넓은 타입으로의 캐스팅은 부호 확장(sign extension)으로 수행돼요.

ubyte a = 133;  // 10000101
byte b = a;     // 10000101

writeln(a);
writeln(b);

ushort c = a;   // 00000000 10000101
short d = b;    // 11111111 10000101

writeln(c);
writeln(d);

참고: 정수 캐스팅 참조.

클래스 참조 (Class References)

클래스 참조를 파생 클래스 참조로 캐스팅하는 것은 정말 다운캐스트인지 확인하는 런타임 검사와 함께 수행돼요. 아니면 결과가 null이에요.

class A {}
class B : A {}

void main()
{
    A a = new A;
    //B b = a;         // error, need cast
    B b = cast(B) a; // b is null if a is not a B
    assert(b is null);

    a = b;         // no cast needed
    a = cast(A) b; // no runtime check needed for upcast
    assert(a is b);
}

객체 o가 클래스 B의 인스턴스인지 판별하려면 캐스트를 사용하세요:

if (cast(B) o)
{
    // o is an instance of B
}
else
{
    // o is not an instance of B
}

포인터 타입을 클래스 타입으로, 또는 그 반대로 캐스팅하는 것은 타입 페인트(즉, reinterpret 캐스트)로 수행돼요.

포인터 (Pointers)

포인터 변수를 다른 포인터 타입으로 캐스팅하는 것은 역참조 결과로 얻을 값과, 포인터 산술이 수행되는 바이트 수를 수정해요.

int val = 25185; // 00000000 00000000 01100010 01100001
char *ch = cast(char*)(&val);

writeln(*ch);    // a
writeln(cast(int)(*ch)); // 97
writeln(*(ch + 1));  // b
writeln(cast(int)(*(ch + 1)));   // 98

마찬가지로, 동적으로 할당된 배열을 더 작은 크기의 타입으로 캐스팅하면 초기 배열의 바이트가 새 차원에 따라 나누어지고 재그룹화돼요.

import core.stdc.stdlib;

int *p = cast(int*) malloc(5 * int.sizeof);
for (int i = 0; i < 5; i++) {
    p[i] = i + 'a';
}
// p = [97, 98, 99, 100, 101]

char* c = cast(char*) p;     // c = [97, 0, 0, 0, 98, 0, 0, 0, 99 ...]
for (int i = 0; i < 5 * int.sizeof; i++) {
    writeln(c[i]);
}

타입 A의 포인터를 타입 B의 포인터로 캐스팅하는데 B가 A보다 넓을 때, A의 크기를 초과하는 메모리에 접근하려는 시도는 미정의 동작을 초래해요.

char c = 'a';
int *p = cast(int*) (&c);
writeln(*p);

배열 캐스팅 (Arrays)

T[] a;
...
cast(U[]) a

리터럴이 아닌 동적 배열 a를 다른 동적 배열 타입 U[]로 캐스팅하는 것은, 결과가 a가 참조했던 모든 데이터 바이트를 포함할 때에만 허용돼요. 이는 a의 요소 바이트 길이가 U.sizeof로 나누어떨어지는지 런타임 검사로 강제돼요. 나머지가 있으면 런타임 오류가 발생해요. 캐스트는 타입 페인트로 수행되고, 결과 배열의 길이는 (a.length * T.sizeof) / U.sizeof로 설정돼요.

byte[] a = [1,2,3];
//auto b = cast(int[])a; // runtime error: array cast misalignment

int[] c = [1, 2, 3];
auto d = cast(byte[])c; // ok
// prints:
// [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0]
writeln(d);

미정의 동작(Undefined Behavior): 어떤 요소의 바이트 표현이 0이나 1이 아닌 리터럴이 아닌 배열을 bool[]로 캐스팅하는 것.

참고: 배열 리터럴 캐스팅 참조.

정적으로 알려진 길이의 슬라이스는 각 데이터의 바이트 수가 일치할 때 static 배열 타입으로 캐스팅될 수 있어요.

void f(int[] b)
{
    char[4] a;
    static assert(!__traits(compiles, a = cast(char[4]) b)); // unknown length
    static assert(!__traits(compiles, a = cast(char[4]) b[0..2])); // too many bytes

    a = cast(char[4]) b[0..1]; // OK
    const i = 1;
    a = cast(char[4]) b[i..2]; // OK
}

참고: 슬라이스의 static 배열로의 변환 참조.

Static 배열 캐스팅 (Static Arrays)

static 배열을 다른 static 배열로 캐스팅하는 것은 배열 길이와 요소 크기의 곱이 일치할 때에만 수행돼요. 불일치는 불법이에요. 캐스트는 타입 페인트(일명 reinterpret 캐스트)로 수행돼요. 배열의 내용은 바뀌지 않아요.

byte[16] b = 3; // set each element to 3
assert(b[0] == 0x03);
int[4] ia = cast(int[4]) b;
// print elements as hex
foreach (i; ia)
    writefln("%x", i);
/* prints:
   3030303
   3030303
   3030303
   3030303
 */

기본 타입은 크기가 일치할 때 단일 요소 static 배열 타입으로 캐스팅될 수 있어요. 반대로 단일 요소 static 배열은 같은 크기의 기본 타입으로 캐스팅될 수 있어요. 캐스트는 타입 페인트(일명 reinterpret 캐스트)로 수행돼요.

int foo = 42;
(cast(int[1]) foo)[] = 1;
assert(foo == 1); // foo's bytes reinterpreted as int[1]

int bar = 42;
auto sa = cast(int[1]) bar;
bar = 0;
bar = cast(int) sa;
assert(bar == 42); // int[1]'s bytes reinterpreted as int

// float and int are both 4 bytes
float f = 3.14f;
int[1] fi = cast(int[1]) f;
assert(fi[0] != 0); // value was reinterpreted

정수 캐스팅 (Integers)

정수를 더 작은 정수 타입으로 캐스팅하면 값이 최하위 비트 방향으로 절단돼요. 목적 타입이 부호 있고 절단 후 최상위 비트가 설정되면, 그 비트는 값에서 잃어버리고 부호 비트가 설정돼요.

uint a = 260;
auto b = cast(ubyte) a;
assert(b == 4); // truncated like 260 & 0xff

int c = 128;
assert(cast(byte)c == -128); // reinterpreted

부호 있는 타입과 부호 없는 타입 사이의 변환은 목적 타입이 소스 값을 표현할 수 없을 때 값을 재해석해요.

short c = -1;
ushort d = c;
assert(d == ushort.max);
assert(uint(c) == uint.max);

ubyte e = 255;
byte f = e;
assert(f == -1); // reinterpreted
assert(short(e) == 255); // no change

부동소수점 캐스팅 (Floating Point)

부동소수점 리터럴을 한 타입에서 다른 타입으로 캐스팅하면 타입이 바뀌지만, 상수 폴딩을 위해 내부적으로는 완전 정밀도로 유지돼요.

void test()
{
    real a = 3.40483L;
    real b;
    b = 3.40483;     // literal is not truncated to double precision
    assert(a == b);
    assert(a == 3.40483);
    assert(a == 3.40483L);
    assert(a == 3.40483F);
    double d = 3.40483; // truncate literal when assigned to variable
    assert(d != a);     // so it is no longer the same
    const double x = 3.40483; // assignment to const is not
    assert(x == a);     // truncated if the initializer is visible
}

부동소수점 값을 정수 타입으로 캐스팅하는 것은 절단을 사용해 정수로 변환하는 것과 동등해요. 부동소수점 값이 정수 타입의 범위를 벗어나면 캐스트는 유효하지 않은 결과를 만들어요(이것은 C, C++에서도 마찬가지예요).

void main()
{
    int a = cast(int) 0.8f;
    assert(a == 0);
    long b = cast(long) 1.5;
    assert(b == 1L);
    long c = cast(long) -1.5;
    assert(c == -1);

    // if the float overflows, the cast returns the integer value of
    // 80000000_00000000H (64-bit operand) or 80000000H (32-bit operand)
    long d = cast(long) float.max;
    assert(d == long.min);
    int e = cast(int) (1234.5 + int.max);
    assert(e == int.min);

    // for types represented on 16 or 8 bits, the result is the same as
    // 32-bit types, but the most significant bits are ignored
    short f = cast(short) float.max;
    assert(f == 0);
}

Struct 캐스팅 (Structs)

e는 struct 타입 S로 캐스팅될 수 있어요:

컴파일러는 PostfixExpression S(e)를 시도해요. 그것이 실패하면:

  • e가 struct 또는 static 배열 인스턴스일 때, 그 데이터는 목적 타입 S로 재해석돼요. 데이터 크기가 일치해야 해요.
  • 그렇지 않으면 오류예요.

참고: 다음 예들은 LittleEndian 바이트 순서를 가정해요.

struct S
{
    int i;
}
struct R
{
    short[2] a;
}

S s = cast(S) 5; // same as S(5)
assert(s.i == 5);
static assert(!__traits(compiles, cast(S) long.max)); // S(long.max) is invalid

R r = R([1, 2]);
s = cast(S) r; // reinterpret r
assert(s.i == 0x00020001);

byte[4] a = [1, 0, 2, 0];
assert(r == cast(R) a); // reinterpret a

struct 인스턴스는 .sizeof 속성이 각각 같은 결과를 줄 때 static 배열 타입으로 캐스팅될 수 있어요.

struct S { short a, b, c; }

S s = S(1, 2, 3);
static assert(!__traits(compiles, cast(short[2]) s)); // size mismatch

short[3] x = cast(short[3]) s;
assert(x.tupleof == s.tupleof);

auto y = cast(byte[6]) s;
assert(y == [1, 0, 2, 0, 3, 0]);

한정자 캐스팅 (Qualifier Cast)

CastQual:
    cast ( TypeCtorsopt ) UnaryExpression

타입이나 한정자 없이 캐스팅하면 UnaryExpression 타입에서 최상위 const, immutable, shared, inout 타입 한정자를 제거해요. 파생 데이터 타입의 경우 하위 타입은 한정된 채로 유지돼요.

shared int x;
static assert(is(typeof(cast() x) == int));

const int[] a;
// element type remains const
static assert(is(typeof(cast() a) == const(int)[]));

struct S { int p; }
const S cs;
static assert(is(typeof(cast() cs) == S));

cast(TypeCtors)는 먼저 UnaryExpression 타입의 최상위 타입 한정자를 제거한 다음 TypeCtors를 추가해요:

shared int x;
static assert(is(typeof(cast(const) x) == const int));

shared int[] a;
// element type remains shared
static assert(is(typeof(cast(const) a) == const shared(int)[]));

void로 캐스팅 (Casting to void)

식을 void 타입으로 캐스팅하는 것은 결과가 사용되지 않음을 표시하기 위해 허용돼요. ExpressionStatement에서 "no effect" 오류를 피하는 데 적절히 쓰일 수 있어요.

void foo(lazy void exp) {}
void main()
{
    foo(10);            // NG - expression '10' has no effect
    foo(cast(void)10);  // OK
}

throw 식 (Throw Expression)

ThrowExpression:
    throw AssignExpression

AssignExpression은 평가되어 Throwable 또는 Throwable에서 파생된 클래스에 대한 참조를 산출해야 해요. 그 참조는 예외로 던져져, 현재 제어 흐름을 중단하고 try-문의 적절한 catch 절에서 계속해요. 이 과정은 해당 try 블록에 들어온 이후 전달된 적용 가능한 scope(exit)/scope(failure)를 실행해요.

throw new Exception("message");

Throwable은 immutable, const, inout, shared로 한정되면 안 돼요. 런타임이 던져진 객체를 수정할 수 있으므로(예: 스택 트레이스 포함) const나 immutable 객체를 위반하게 돼요.

ThrowExpression은 다른 식에 중첩될 수 있어요:

void foo(int function() f) {}

void main() {
    foo(() => throw new Exception());
}

ThrowExpression의 타입은 noreturn이에요.

Best Practices: 프로그램 버그를 보고하고 프로그램을 중단하는 데에는 Error보다 Assert 식을 사용하세요.

거듭제곱 식 (Pow Expressions)

PowExpression:
    PostfixExpression
    PostfixExpression ^^ UnaryExpression

PowExpression은 왼쪽 피연산자를 오른쪽 피연산자의 거듭제곱으로 올려요.

접미 식 (Postfix Expressions)

PostfixExpression:
    PrimaryExpression
    PostfixExpression . Identifier
    PostfixExpression . TemplateInstance
    PostfixExpression . NewExpression
    PostfixExpression ++
    PostfixExpression --
    PostfixExpression ( NamedArgumentListopt )
    TypeCtorsopt BasicType ( NamedArgumentListopt )
    PostfixExpression IndexOperation
    PostfixExpression SliceOperation

관련 개념: property, aggregate 타입에 대한 포인터, UFCS, nested class, 평가 순서, 식 호출, static opCall, 타입 생성.

Operation Description
. Identifier Either:
Access a member of a type or expression.
Access a member of a module, package, aggregate type or instance, enum or template instance.
Dereference a pointer instance and access a member of it.
Call a free function using UFCS.
. NewExpression Instantiate a nested class
++ Increment after use
-- Decrement after use
(args) Either:
Call a function with optional arguments
Call opCall on a user-defined type with optional arguments
IndexOperation Select a single element
SliceOperation Select a series of elements

접미 인자 목록 (Postfix Argument Lists)

ArgumentList:
    AssignExpression
    AssignExpression ,
    AssignExpression , ArgumentList

NamedArgumentList:
    NamedArgument
    NamedArgument ,
    NamedArgument , NamedArgumentList

NamedArgument:
    Identifier : AssignExpression
    AssignExpression

호출 가능 식 (Callable Expressions)

호출 가능 식은 괄호 안의 명명된 인자 목록에 앞설 수 있어요. 다음 식들은 호출될 수 있어요:

  • 함수
  • 함수 포인터
  • delegate
  • opCall을 정의하는 aggregate 타입 인스턴스
void f(int, int);

void g()
{
    f(5, 6);
    (&f)(5, 6);
}

인자를 매개변수에 일치시키기 (Matching Arguments to Parameters)

NamedArgumentList의 인자들은 다음과 같이 함수 매개변수에 일치돼요:

  • 첫 번째 인자에 이름이 없으면 첫 번째 함수 매개변수에 배정돼요.
  • 명명된 인자는 같은 이름을 가진 함수 매개변수에 배정돼요. 그런 매개변수가 없으면 오류예요.
  • 이름 없는 인자는 앞선 인자의 매개변수에 상대적인 다음 매개변수에 배정돼요. 그런 매개변수가 없으면(즉 앞선 인자가 마지막 매개변수에 배정될 때) 오류예요.
  • 한 매개변수를 두 번 이상 배정하는 것은 오류예요.
  • 매개변수에 인자를 배정하지 않는 것도 오류예요. 단, 매개변수가 기본 인자를 가질 때는 예외예요.

인자 목록으로 타입 생성하기 (Constructing a Type with an Argument List)

타입은 인자 목록에 앞설 수 있어요. 참고:

인덱스 연산 (Index Operations)

IndexOperation:
    [ ArgumentList ]

기본 PostfixExpression이 평가돼요. 특수 변수 $가 선언되어 기본 PostfixExpression의 요소 수로 설정돼요(가능할 때). ArgumentList 평가를 위해 새 선언 스코프가 생성되고 $는 그 스코프에만 나타나요.

  • PostfixExpression이 static 또는 dynamic 배열 타입의 식이면, 인덱싱 결과는 배열의 i번째 요소의 lvalue예요. 여기서 i는 ArgumentList에서 평가된 정수예요. 배열 인덱싱 참조.
  • PostfixExpression이 포인터 p면 결과는 *(p + i)예요 (포인터 산술 참조).
  • 기본 PostfixExpression이 ValueSeq이면 ArgumentList는 하나의 인자로만 구성되어야 하고, 그것은 정수 상수로 정적으로 평가될 수 있어야 해요. 그 정수 상수 n이 ValueSeq에서 n번째 식을 선택하며, 그것이 IndexOperation의 결과예요. n이 ValueSeq 범위를 벗어나면 오류예요.

인덱스 연산자는 오버로드될 수 있어요. ArgumentList에서 여러 인덱스를 사용하는 것은 연산자 오버로딩에서만 지원돼요.

슬라이스 연산 (Slice Operations)

SliceOperation:
    [ ]
    [ Slice ]
    [ Slice , ]

Slice:
    AssignExpression
    AssignExpression , Slice
    AssignExpression .. AssignExpression
    AssignExpression .. AssignExpression , Slice

기본 PostfixExpression이 평가돼요. 특수 변수 $가 선언되어 PostfixExpression의 요소 수로 설정돼요(가능할 때). AssignExpression .. AssignExpression 평가를 위해 새 선언 스코프가 생성되고 $는 그 스코프에만 나타나요.

  • 기본 PostfixExpression이 static 또는 dynamic 배열 a이면, 슬라이스 결과는 a[i]부터 a[j-1]까지(포함)의 요소를 참조하는 동적 배열이에요. 여기서 i와 j는 각각 첫 번째와 두 번째 AssignExpression에서 평가된 정수예요. 배열 슬라이싱 참조.
  • 기본 PostfixExpression이 포인터 p이면, 결과는 p[i]부터 p[j-1]까지(포함)의 요소를 참조하는 동적 배열이에요. 여기서 i와 j는 각각 첫 번째와 두 번째 AssignExpression에서 평가된 정수예요.
  • 기본 PostfixExpression이 ValueSeq이면, 슬라이스 결과는 정수 상수로 정적으로 평가되어야 하는 상한과 하한으로 형성된 새 ValueSeq예요. 그 경계가 범위를 벗어나면 오류예요.

첫 번째 AssignExpression은 슬라이스의 포함하는 하한으로, 두 번째 AssignExpression은 배타적인 상한으로 취해져요. 식의 결과는 PostfixExpression의 요소 슬라이스예요.

[ ] 형태가 쓰이면 슬라이스는 기본 PostfixExpression의 모든 요소예요. 기본 식은 포인터가 될 수 없어요.

슬라이스 연산자는 오버로드될 수 있어요. 둘 이상의 Slice를 사용하는 것은 연산자 오버로딩에서만 지원돼요.

SliceOperation은 수정 가능한 lvalue가 아니에요.

static 배열로의 슬라이스 변환 (Slice Conversion to Static Array)

슬라이스 경계를 컴파일 타임에 알 수 있으면, 슬라이스 식은 static 배열 lvalue로 암시적으로 변환될 수 있어요. 예:

arr[a .. b]     // typed T[]

ab가 모두 정수(상수 폴딩될 수 있음)이면, 슬라이스 식은 타입 T[b - a]의 static 배열로 변환될 수 있어요.

참고: static 배열은 길이가 일치하는지 런타임 검사를 수행하면서 슬라이스로부터 대입될 수도 있어요.

void f(int[2] sa) {}

int[] arr = [1, 2, 3];

void test()
{
    //f(arr); // error, can't convert
    f(arr[1 .. 3]); // OK
    //f(arr[0 .. 3]); // error

    int[2] g() { return arr[0 .. 2]; }
}
void bar(ref int[2] a)
{
    assert(a == [2, 3]);
    a = [4, 5];
}

void main()
{
    int[] arr = [1, 2, 3];

    // slicing an lvalue gives an lvalue
    bar(arr[1 .. 3]);
    assert(arr == [1, 4, 5]);
}

기본 식 (Primary Expressions)

PrimaryExpression:
    Identifier
    . Identifier
    TemplateInstance
    . TemplateInstance
    $
    LiteralExpression
    AssertExpression
    MixinExpression
    ImportExpression
    NewExpression
    FundamentalType . Identifier
    TypeCtoropt ( Type ) . Identifier
    ( Type ) . TemplateInstance
    FundamentalType ( NamedArgumentListopt )
    TypeCtoropt ( Type ) ( NamedArgumentListopt )
    Typeof
    TypeidExpression
    IsExpression
    ( Expression )
    SpecialKeyword
    RvalueExpression
    TraitsExpression

LiteralExpression:
    this
    super
    null
    true
    false
    IntegerLiteral
    FloatLiteral
    CharacterLiteral
    StringLiteral
    InterpolationExpressionSequence
    ArrayLiteral
    AssocArrayLiteral
    FunctionLiteral

관련 개념: . Identifier모듈 스코프 연산자, 인덱싱/슬라이싱, type propertystatic member, 균일 생성, 부분식입니다.

Expression Description
. Identifier Access a member of a module or package
$ Number of elements in an object being indexed/sliced
( Type ). Identifier Access a static member or type property of a type
FundamentalType (arg) Construct an instance of scalar type with optional argument
( Type )(args) Construct a type with optional arguments
( Expression ) Evaluate an expression - useful as a subexpression

this

생성자나 비정적 멤버 함수 안에서 this는 그 함수가 호출된 객체에 대한 참조로 해석돼요.

typeof(this)는 aggregate 타입 정의 안 어디서든 유효해요. 클래스 멤버 함수가 typeof(this)에 대한 명시적 참조로 호출되면 비가상 호출이 이루어져요:

class A
{
    char get() { return 'A'; }

    char foo() { return typeof(this).get(); } // calls `A.get`
    char bar() { return this.get(); } // dynamic, same as just `get()`
}

class B : A
{
    override char get() { return 'B'; }
}

void main()
{
    B b = new B();

    assert(b.foo() == 'A');
    assert(b.bar() == 'B');
}

클래스에 대한 this로의 대입은 허용되지 않아요. 참고: 위임 생성자, 템플릿 this 매개변수.

super

superthis와 동일하지만, this의 기본 클래스로 캐스팅된 것이에요. 기본 클래스가 없으면 오류예요. (기본 클래스가 없는 유일한 extern(D) 클래스는 Object지만, extern(C++) 클래스는 지정하지 않으면 기본 클래스가 없다는 점에 주의하세요.) super에 대한 명시적 참조로 멤버 함수가 호출되면 비가상 호출이 이루어져요.

super로의 대입은 허용되지 않아요. 참고: 기본 클래스 생성.

null

null은 포인터, 함수 포인터, delegate, 동적 배열, 연관 배열, 클래스 객체의 null 값을 나타내요. 아직 타입으로 캐스팅되지 않았다면 고유한 타입 typeof(null)이 주어지고, 포인터·함수 포인터·delegate 등의 null 값으로 변환하는 것은 정확한 변환(exact conversion)이에요. 타입으로 캐스팅된 후에는 그런 변환이 암시적이지만 더 이상 정확하지는 않아요.

문자열 리터럴 (String Literals)

StringLiteral 문법 참조.

문자열 리터럴은 읽기 전용이에요. StringPostfix가 없는 문자열 리터럴은 다음 타입들 중 어느 것으로든 암시적으로 변환될 수 있고, 모두 가중치가 같아요:

immutable(char)*
immutable(wchar)*
immutable(dchar)*
immutable(char)[]
immutable(wchar)[]
immutable(dchar)[]

미정의 동작(Undefined Behavior): 문자열 리터럴에 쓰는 것. @safe 코드에서는 허용되지 않아요.

기본적으로 문자열 리터럴은 동적 배열로 타입이 정해지지만, 요소 수는 컴파일 타임에 알 수 있어요. 그래서 모든 문자열 리터럴은 immutable static 배열로 암시적으로 변환될 수 있어요:

void foo(char[2] a)
{
    assert(a[0] == 'b');
}
void bar(ref const char[2] a)
{
    assert(a == "bc");
}

void main()
{
    foo("bc");
    foo("b"); // OK
    //foo("bcd"); // error, too many chars
    bar("bc"); // OK, same length
    //bar("b"); // error, lengths must match
}

문자열 리터럴은 같거나 더 긴 길이의 static 배열 rvalue로 변환돼요. 추가 요소들은 0으로 채워져요. 문자열 리터럴은 같은 길이의 static 배열 lvalue로도 변환될 수 있어요.

문자열 리터럴에는 '\0'이 추가되어 null 종료 const char* 문자열을 기대하는 C나 C++ 함수에 전달하기 쉬워요. '\0'은 문자열 리터럴의 .length 속성에는 포함되지 않아요.

문자열 리터럴의 연결은 ~ 연산자를 사용해야 하고 컴파일 타임에 해결돼요. 연산자 없이 C 스타일의 암시적 연결은 오류가 나기 쉬워서 D에서 지원되지 않아요.

16진 문자열 리터럴 (Hex String Literals)

16진 문자열 리터럴은 텍스트 데이터에 한정되지 않은 이진 데이터를 포함하므로, 다른 문자열 리터럴보다 추가 변환을 허용해요.

16진 문자열 리터럴은 상수 byte[] 또는 ubyte[]로 암시적으로 변환돼요.

immutable ubyte[] b = x"3F 80 00 00";
const byte[] c = x"3F 80 00 00";

16진 문자열 리터럴은 1보다 큰 크기의 정수 배열로 명시적으로 캐스팅될 수 있어요. 16진 문자열에서 빅 엔디안 바이트 순서가 가정돼요.

static immutable uint[] data = cast(immutable uint[]) x"AABBCCDD";
static assert(data[0] == 0xAABBCCDD);

이것은 16진 문자열의 길이가 배열 요소 크기(바이트)의 배수여야 해요.

static e = cast(immutable ushort[]) x"AA BB CC";
// Error, length of 3 bytes is not a multiple of 2, the size of a `ushort`

16진 문자열 리터럴이 상수 폴딩되면 결과는 더 이상 16진 문자열 리터럴로 간주되지 않아요.

static immutable byte[] b = x"AA" ~ "G"; // Error: cannot convert `string` to `immutable byte[]`

배열 리터럴 (Array Literals)

ArrayLiteral:
    [ ArgumentListopt ]

배열 리터럴은 대괄호 [ 와 ] 사이의 쉼표로 구분된 식들의 목록이에요. 식들은 동적 배열의 요소를 이뤄요. 배열의 길이는 요소의 수예요.

배열의 요소 타입은 모든 요소의 공통 타입으로 추론되고, 각 식은 그 타입으로 암시적으로 변환돼요. 기대된 배열 타입이 있을 때, 리터럴의 요소들은 기대된 요소 타입으로 암시적으로 변환돼요.

auto a1 = [1, 2, 3];   // type is int[], with elements 1, 2 and 3
auto a2 = [1u, 2, 3];  // type is uint[], with elements 1u, 2u, and 3u
byte[] a3 = [1, 2, 3]; // OK
byte[] a4 = [128];     // error

기본적으로 배열 리터럴은 동적 배열로 타입이 정해지지만, 요소 수는 컴파일 타임에 알 수 있어요. 따라서 배열 리터럴은 같은 길이의 static 배열로 암시적으로 변환될 수 있어요.

int[2] sa = [1, 2]; // OK
int[2] sb = [1];    // error

참고: 정적으로 알려진 슬라이스 길이로 동적 배열을 슬라이싱하는 것도 static 배열로의 변환을 허용해요.

어떤 ArrayMemberInitialization이 ValueSeq이면, ValueSeq의 요소들이 시퀀스 대신 식으로 삽입돼요.

GC 할당 (GC Allocation)

탈출(escaping)하는 배열 리터럴은 항상 메모리 관리 힙에 할당돼요. 따라서 함수에서 안전하게 반환될 수 있어요:

int[] foo()
{
    return [1, 2, 3];
}

배열 리터럴은 다음 경우에 GC 할당되지 않아요:

void f(scope int[] a, int[2] sa) @nogc
{
    sa = [7, 8];
}
void g(int[] b) @nogc; // `b` is not scope, so may escape

void main() @nogc
{
    int[3] sa = [1, 2, 3];
    f([1, 2], [3, 4]);
    //scope int[] a = [5, 6]; // requires `-preview=dip1000`
    //g([1, 2]); // error, array literal heap allocated
    assert([1, 2] < [3, 2]);
    assert([1, 2][1] == 2);
    foreach (e; [4, 2, 9])
        assert(e > 0);
}
캐스팅 (Casting)

배열 리터럴을 다른 배열 타입으로 캐스팅하면 배열의 각 요소가 새 요소 타입으로 캐스팅돼요. 리터럴이 아닌 배열을 캐스팅하면 배열이 새 타입으로 재해석되고 길이가 재계산돼요:

// cast array literal
const ubyte[] ct = cast(ubyte[]) [257, 257];
// this is equivalent to:
// const ubyte[] ct = [cast(ubyte) 257, cast(ubyte) 257];
writeln(ct);  // writes [1, 1]

// cast other array expression
// --> normal behavior of CastExpression
byte[] arr = [1, 1];
short[] rt = cast(short[]) arr;
writeln(rt);  // writes [257]

즉, 배열 리터럴을 캐스팅하면 각 초기화 요소의 타입이 변경돼요.

Best Practices: 요소들이 기대된 타입으로 암시적으로 변환될 수 있을 때 배열 리터럴을 캐스팅하는 것은 피하세요. 대신 그 타입의 변수를 선언하고 배열 리터럴로 초기화하세요. 캐스팅은 암시적 변환보다 버그가 더 많아요.

연관 배열 리터럴 (Associative Array Literals)

AssocArrayLiteral:
    [ KeyValuePairs ]

KeyValuePairs:
    KeyValuePair
    KeyValuePair , KeyValuePairs

KeyValuePair:
    KeyExpression : ValueExpression

KeyExpression:
    AssignExpression

ValueExpression:
    AssignExpression

연관 배열 리터럴은 대괄호 [ 와 ] 사이의 쉼표로 구분된 key:value 쌍의 목록이에요. 목록은 비어 있을 수 없어요. 모든 키의 공통 타입은 연관 배열의 키 타입으로 취해지고 모든 키는 그 타입으로 암시적으로 변환돼요. 모든 값의 공통 타입은 연관 배열의 값 타입으로 취해지고 모든 값은 그 타입으로 암시적으로 변환돼요. AssocArrayLiteral은 아무것도 정적으로 초기화하는 데 사용될 수 없어요.

[21u: "he", 38: "ho", 2: "hi"]; // type is string[uint],
                              // with keys 21u, 38u and 2u
                              // and values "he", "ho", and "hi"

KeyValuePairs의 키나 값 중 어떤 것이 ValueSeq이면, ValueSeq의 요소들이 시퀀스 대신 인자로 삽입돼요.

연관 배열 초기화는 중복 키를 담을 수 있지만, 그 경우 어휘 순서상 마지막으로 만난 KeyValuePair가 저장돼요.

auto aa = [21: "he", 38: "ho", 2: "hi", 2:"bye"];
assert(aa[2] == "bye")

함수 리터럴 (Function Literals)

FunctionLiteral:
    function RefOrAutoRefopt BasicTypeWithSuffixesopt ParameterWithAttributesopt FunctionLiteralBody
    delegate RefOrAutoRefopt BasicTypeWithSuffixesopt ParameterWithMemberAttributesopt FunctionLiteralBody
    RefOrAutoRefopt ParameterWithMemberAttributes FunctionLiteralBody
    BlockStatement
    Identifier => AssignExpression

RefOrAutoRef:
    ref
    auto ref

BasicTypeWithSuffixes:
    BasicType TypeSuffixesopt

ParameterWithAttributes:
    Parameters FunctionAttributesopt

ParameterWithMemberAttributes:
    Parameters MemberFunctionAttributesopt

FunctionLiteralBody:
    => AssignExpression
    SpecifiedFunctionBody

FunctionLiteral은 익명 함수와 익명 delegate를 식에 직접 내장할 수 있게 해줘요. 짧은 함수 리터럴은 람다로 알려져 있어요.

BasicTypeWithSuffixes는 함수나 delegate의 반환 타입이에요. 생략하면 본문에서 추론돼요.

ParameterWithAttributes 또는 ParameterWithMemberAttributes는 함수의 매개변수를 지정하는 데 쓰일 수 있어요. 생략하면 함수는 빈 매개변수 목록 ( )으로 기본 설정돼요.

매개변수 타입은 생략될 수 있어요. 그것이 추론되거나, 리터럴이 템플릿이 돼요. Parameter가 Declarator 없이 BasicType으로서 Identifier를 가지면, Identifier는 매개변수 이름이 되고 타입은 지정되지 않아요.

함수 리터럴은 별칭화될 수 있어요.

예:

// Literal with `int` parameter and `int` return type
function int(int x) { return x; }
(int x) { return x; } // Same (unless delegate expected)
(int x) => x          // Same

(x) => x    // Template, unless parameter type can be inferred
x => x      // Same

() { ... }  // Literal with no parameters and inferred return type
{ ... }     // Same

(템플릿이 아닌) 함수 리터럴의 타입은 함수 포인터delegate예요. 예:

int function(char c) fp; // declare pointer to a function

void test()
{
    static int foo(char c) { return 6; }

    fp = &foo;
}

은 정확히 다음과 동등해요:

int function(char c) fp;

void test()
{
    fp = function int(char c) { return 6; };
}

FunctionLiteralBody가 둘러싼 함수의 비정적 지역 변수에 접근한다면 delegate가 필요해요.

int abc(int delegate(int i));

void test()
{
    int b = 3;
    int foo(int c) { return 6 + b; }

    abc(&foo);
}

은 정확히 다음과 동등해요:

int abc(int delegate(int i));

void test()
{
    int b = 3;

    abc( delegate int(int c) { return 6 + b; } );
}

ref 사용은 반환 값이 참조로 반환됨을 선언해요:

void main()
{
    int x;
    auto dg = delegate ref int() { return x; };
    dg() = 3;
    assert(x == 3);
}

참고: 함수 리터럴을 중첩 함수와 비교할 때, 함수 형태는 static 또는 비중첩 함수와 유사하고 delegate 형태는 비정적 중첩 함수와 유사해요. 즉 delegate 리터럴은 둘러싼 함수의 비정적 지역 변수에 접근할 수 있지만 함수 리터럴은 그럴 수 없어요.

delegate 추론 (Delegate Inference)

리터럴이 function이나 delegate를 생략하고 문맥에서 기대된 타입이 없으면, 둘러싼 함수의 변수에 접근하면 delegate로, 그렇지 않으면 함수 포인터로 추론돼요.

void test()
{
    int b = 3;

    auto fp = (uint c) { return c * 2; }; // inferred as function pointer
    auto dg = (int c) { return 6 + b; }; // inferred as delegate

    static assert(!is(typeof(fp) == delegate));
    static assert(is(typeof(dg) == delegate));
}

delegate가 기대되면, 둘러싼 함수의 변수에 접근하지 않더라도 리터럴은 delegate로 추론돼요:

void abc(int delegate(int i)) {}
void def(uint function(uint s)) {}

void test()
{
    int b = 3;

    abc( (int c) { return 6 + b; } );  // inferred as delegate
    abc( (int c) { return c * 2; } );  // inferred as delegate

    def( (uint c) { return c * 2; } ); // inferred as function
    //def( (uint c) { return c * b; } );  // error!
    // Because the FunctionLiteral accesses b, its type
    // is inferred as delegate. But def cannot accept a delegate argument.
}
매개변수 타입 추론 (Parameter Type Inference)

함수 리터럴의 타입을 문맥에서 유일하게 결정할 수 있으면, 매개변수 타입 추론이 가능해요.

void foo(int function(int) fp);

void test()
{
    int function(int) fp = (n) { return n * 2; };
    // The type of parameter n is inferred as int.

    foo((n) { return n * 2; });
    // The type of parameter n is inferred as int.
}
auto fp = (i) { return 1; }; // error, cannot infer type of `i`
함수 리터럴 템플릿 (Function Literal Templates)

함수 리터럴은 다음 중 하나를 가질 때 템플릿이 돼요:

  • 지정되지 않은 매개변수 타입과 그 타입을 추론할 문맥이 없을 때
  • auto ref 매개변수

템플릿은 리터럴의 각 지정되지 않은 매개변수 타입에 대해 타입 매개변수를 가져요. 리터럴이 호출될 때 암시적 인스턴스화가 지원돼요(함수 템플릿의 IFTI처럼). auto ref 매개변수는 리터럴이 암시적으로 인스턴스화될 때만 지원돼요.

alias fpt = (i) { return i; }; // OK, infer type of `i` when called
//auto fpt(T) = (T i) { return i; }; // equivalent
static assert(__traits(isTemplate, fpt));

auto v = fpt(4);    // `i` is inferred as int
auto d = fpt(10.3); // `i` is inferred as double

alias fp = fpt!float; // `fp` is a function pointer
static assert(is(typeof(*fp) == function));
auto f = fp(0); // f is a float

함수 리터럴 템플릿 매개변수는 언패킹을 사용할 수 있어요.

반환 타입 추론 (Return Type Inference)

FunctionLiteral의 반환 타입은 AssignExpression 또는 BlockStatement의 ReturnStatement로부터 추론될 수 있어요. 문맥에서 다른 기대 타입이 있고, 초기 추론된 반환 타입이 기대 타입으로 암시적으로 변환되면, 반환 타입은 기대 타입으로 추론돼요.

auto fi = (int i) { return i; };
static assert(is(typeof(fi(5)) == int));

long function(int) fl = (int i) { return i; };
static assert(is(typeof(fl(5)) == long));
무항 단축 문법 (Nullary Short Syntax)

BlockStatement 함수 본문이 있을 때 함수 리터럴에 대한 매개변수는 완전히 생략될 수 있어요.

참고: 이 형태는 ExpressionStatement로 즉시 호출될 수 없어요. BlockStatement와 구별하기 위해 임의의 lookahead가 필요하기 때문이에요.

auto f = { writeln("hi"); }; // OK, f has type `void function()`
f();
{ writeln("hi"); }(); // error
() { writeln("hi"); }(); // OK

익명 delegate는 임의의 문장 리터럴처럼 동작할 수 있어요. 예를 들어 여기서 임의의 문장이 루프에 의해 실행돼요:

void loop(int n, void delegate() statement)
{
    foreach (_; 0 .. n)
    {
        statement();
    }
}

void main()
{
    int n = 0;

    loop(5, { n += 1; });
    assert(n == 5);
}
단축 본문 문법 (Shortened Body Syntax)

문법 => AssignExpression{ return AssignExpression; }과 동등해요.

void main()
{
    auto i = 3;
    auto twice = function (int x) => x * 2;
    assert(twice(i) == 6);

    auto square = delegate () => i * i;
    assert(square() == 9);

    auto n = 5;
    auto mul_n = (int x) => x * n;
    assert(mul_n(i) == 15);
}

문법 Identifier => AssignExpression(Identifier) { return AssignExpression; }과 동등해요.

// the following two declarations are equivalent
alias fp = i => 1;
alias fp = (i) { return 1; };

Best Practices: 함수 리터럴의 최소 형태는 템플릿 alias 매개변수의 인자로 가장 유용해요:

int motor(alias fp)(int i)
{
    return fp(i) + 1;
}

int engine()
{
    return motor!(i => i * 2)(6); // returns 13
}

참고: 문법 Identifier { statement; }는 지원되지 않아요. 세미콜론이 실수로 생략되면 x = Identifier; { statement; }; 문장과 쉽게 혼동되기 때문이에요.

내장 스칼라 타입의 균일 생성 문법 (Uniform construction syntax for built-in scalar types)

내장 스칼라 타입의 암시적 변환은 함수 호출 문법을 사용해 명시적으로 나타낼 수 있어요. 예:

auto a = short(1);  // implicitly convert an integer literal '1' to short
auto b = double(a); // implicitly convert a short variable 'a' to double
auto c = byte(128); // error, 128 cannot be represented in a byte

인자가 생략되면 스칼라 타입의 기본 생성(default construction)을 의미해요:

auto a = ushort();  // same as: ushort.init
auto b = wchar();   // same as: wchar.init

인자에 이름을 줄 수 없어요:

auto a = short(x: 1); // Error

참고: 일반 산술 변환 참조.

assert 식 (Assert Expressions)

AssertExpression:
    assert ( AssertArguments )

AssertArguments:
    AssignExpression
    AssignExpression ,
    AssignExpression , AssignExpression
    AssignExpression , AssignExpression ,

첫 번째 AssignExpression이 평가되고 부울 값으로 변환돼요. 값이 true가 아니면 Assert Failure가 발생하고 프로그램은 Invalid State에 들어가요.

int i = fun();
assert(i > 0);

AssertExpression은 unittest 또는 contract 안에 있으면 다른 의미를 가져요.

첫 번째 AssignExpression이 클래스 Invariant가 존재하는 클래스 인스턴스에 대한 참조면, 클래스 Invariant가 성립해야 해요.

첫 번째 AssignExpression이 struct Invariant가 존재하는 struct 인스턴스에 대한 포인터면, struct Invariant가 성립해야 해요.

AssertExpression의 타입은 void예요.

미정의 동작(Undefined Behavior): 일단 Invalid State에 들어가면 계속 실행되는 프로그램의 동작은 정의되지 않아요.

구현 정의(Implementation Defined): 첫 번째 AssertExpression이 (런타임에) 평가되는지 여부는 보통 컴파일러 스위치로 설정돼요. 평가되지 않으면 AssertExpression이 지정한 부수 효과가 발생하지 않을 수 있어요. 첫 번째 AssertExpression이 false로 평가될 때의 동작도 보통 컴파일러 스위치로 설정되며 다음 옵션을 포함할 수 있어요:

  • 특수 CPU 명령 실행으로 즉시 중단
  • 프로그램 중단
  • 해당 C 런타임 라이브러리의 assert 실패 함수 호출
  • D 런타임 라이브러리에서 AssertError 예외 던지기

참고: AssertError 던지기는 dmd의 기본값이며, 오류 메시지에서 첫 번째 AssertExpression에 사용된 특정 부분식을 보여주는 선택적 -checkaction=context 스위치가 있어요:

auto x = 4;
assert(x < 3);

사용 중일 때 위 코드는 4 >= 3 메시지와 함께 AssertError를 던져요.

Best Practices: 후속 코드가 의존하는 부수 효과를 어느 AssignExpression에도 두지 마세요. AssertExpression은 프로그램의 버그를 감지하기 위한 것이에요. 입력이나 환경 오류를 감지하는 데 사용하지 마세요. Assert Failure 후 정상 실행을 재개하려 하지 마세요.

컴파일 타임 평가 (Compile-time Evaluation)

첫 번째 AssignExpression이 전적으로 컴파일 타임 상수로 구성되고 false로 평가되면 특별한 경우예요. 후속 문장이 도달 불가능한 코드(unreachable code)임을 나타내요. 평가를 위해 컴파일 타임 함수 실행(CTFE) 호출은 시도되지 않아요. 그런 AssertExpression은 타입 noreturn을 가져요.

마찬가지로 첫 번째 AssignExpression이 타입 noreturn이면, 평가는 결코 반환되지 않고 AssertExpression도 noreturn 타입이에요.

이것은 컴파일러가 누락된 return 문이 있을 때 오류를 억제하게 해줘요:

int f(int x)
{
    if (x > 0)
    {
        return 5 / x;
    }
    assert(0);
    // no need to use a dummy return statement here
}

구현은 첫 번째 AssignExpression이 컴파일 타임에 false로 평가되는 경우를 다르게 처리할 수 있어요. 다른 assert가 무시되더라도 HLT 명령이나 그에 상응하는 것을 생성할 수 있어요.

이유(Rationale): 프로그램을 중단하면 미정의 동작이 발생하는 것을 막아요.

참고: static assert 참조.

assert 메시지 (Assert Message)

두 번째 AssignExpression은, 있다면, 타입 const(char)[]로 암시적으로 변환될 수 있어야 해요. 있을 때 구현은 그것을 평가하고 assert 실패 시 결과 메시지를 출력할 수 있어요:

void main()
{
    assert(0, "an" ~ " error message");
}

컴파일·실행하면 보통 다음 메시지를 만들어요:

[email protected](3) an error message

믹스인 식 (Mixin Expressions)

MixinExpression:
    mixin ( ArgumentList )

ArgumentList의 각 AssignExpression은 컴파일 타임에 평가되고 그 결과는 문자열로 표현 가능해야 해요. 결과 문자열들은 연결되어 하나의 문자열을 이뤄요. 문자열의 텍스트 내용은 유효한 Expression으로 컴파일될 수 있어야 하고 그렇게 컴파일돼요.

int foo(int x)
{
    return mixin("x +", 1) * 7;  // same as ((x + 1) * 7)
}

import 식 (Import Expressions)

ImportExpression:
    import ( AssignExpression )

AssignExpression은 컴파일 타임에 상수 문자열로 평가되어야 해요. 문자열의 텍스트 내용은 파일 이름으로 해석돼요. 파일이 읽히고, 파일의 정확한 내용이 16진 문자열 리터럴이 돼요.

구현은 디렉터리 트래버설 보안 취약점을 피하기 위해 파일 이름을 제한할 수 있어요. 가능한 제한은 파일 이름에서 경로 구성 요소를 허용하지 않는 것일 수 있어요.

참고로 기본적으로 import 식은 -J 스위치로 하나 이상의 경로가 전달되지 않으면 컴파일되지 않아요. 이것은 컴파일러에게 import할 파일을 어디서 찾을지 알려줘요. 이것은 보안 기능이에요.

void foo()
{
    // Prints contents of file foo.txt
    writeln(import("foo.txt"));
}

new 식 (New Expressions)

NewExpression:
    new PlacementExpressionopt Type
    new PlacementExpressionopt Type [ AssignExpression ]
    new PlacementExpressionopt Type ( NamedArgumentListopt )
    NewAnonClassExpression

PlacementExpression:
    ( AssignExpression )

NewExpression은 PlacementExpression이 없으면 가비지 수집 힙에 메모리를 할당해요.

new T는 타입 T의 인스턴스를 만들고 기본 초기화해요. 결과의 타입은:

  • T가 참조 타입(예: 클래스, 연관 배열)일 때 T
  • T가 값 타입(예: 기본 타입, struct)일 때 T*
int* i = new int;
assert(*i == 0); // int.init

Object o = new Object;
//int[] a = new int[]; // error, need length argument

Type(NamedArgumentList) 형태는 같은 타입의 단일 초기화자 또는 더 복잡한 타입을 위한 여러 인자를 전달할 수 있게 해줘요:

  • 클래스와 struct 타입의 경우 NamedArgumentList가 생성자에 전달돼요.
  • 동적 배열의 경우 인자가 초기 배열 길이를 설정해요.
  • 다차원 동적 배열의 경우 각 인자가 초기 길이에 해당해요(아래 참조).
int* i = new int(5);
assert(*i == 5);

Exception e = new Exception("info");
assert(e.msg == "info");

int[] a = new int[](2);
assert(a.length == 2);
a = new int[2]; // same, see below

Type[AssignExpression] 형태는 AssignExpression과 같은 길이의 동적 배열을 할당해요. 동적 배열을 할당할 때는 더 일반적이므로 Type(NamedArgumentList) 형태를 쓰는 것이 선호돼요.

참고: new로 static 배열을 직접 할당하는 것은 불가능해요(타입 alias를 사용하는 경우만 가능).

결과는 unique expression이며 다른 한정자로 암시적으로 변환될 수 있어요:

immutable o = new Object;
클래스 인스턴스화 (Class Instantiation)

NewExpression이 scope 저장 클래스의 함수 지역 변수의 초기화자로 클래스 타입과 함께 사용되면, 인스턴스는 스택에 할당돼요.

newnested 클래스를 할당하는 데에도 사용될 수 있어요.

다차원 배열 (Multidimensional Arrays)

다차원 배열을 할당하려면 선언은 접두 배열 선언 순서와 같은 순서로 읽혀요.

char[][] foo;   // dynamic array of strings
...
foo = new char[][30]; // allocate array of 30 strings

위 할당은 다음과 같이 쓸 수도 있어요:

foo = new char[][](30); // allocate array of 30 strings

중첩 배열을 할당하려면 여러 인자를 사용할 수 있어요:

int[][][] bar;
bar = new int[][][](5, 20, 30);

assert(bar.length == 5);
assert(bar[0].length == 20);
assert(bar[0][0].length == 30);

위 대입은 다음과 동등해요:

bar = new int[][][5];
foreach (ref a; bar)
{
    a = new int[][20];
    foreach (ref b; a)
    {
        b = new int[30];
    }
}
배치 new (Placement New)

PlacementExpression가비지 수집 힙을 사용하는 대신 NewExpression이 새로 생성된 값으로 초기화할 저장 공간을 명시적으로 제공해요.

Type이 기본 타입이나 struct이면, PlacementExpression은 sizeof(Type)보다 크거나 같은 크기를 가진 lvalue를 만들어야 해요.

PlacementExpression의 타입은 생성되는 객체의 타입과 같을 필요가 없어요.

Best Practices: PlacementExpression으로 void의 static 배열을 사용하는 것이 선호돼요.

lvalue로 제시된 객체의 수명은 NewExpression 실행으로 끝나고, 배치된 객체의 새 수명은 실행 후 시작돼요.

struct S
{
    float d;
    int i;
    char c;
}

void main()
{
    S s;
    S* p = new (s) S(); // lifetime of s ends, lifetime of *p begins
    assert(p.i == 0 && p.c == 0xFF);
}

Type이 클래스이면, PlacementExpression은 void[__traits(classInstanceSize, Type)] 또는 클래스 객체를 위한 충분한 메모리를 나타내는 동적 배열처럼 클래스 객체를 담기에 충분한 크기의 타입의 lvalue를 만들어야 해요.

class C
{
    int i, j = 4;
}

void main()
{
    void[__traits(classInstanceSize, C)] k = void;
    C c = new(k) C;
    assert(c.j == 4);
    assert(cast(void*) c == k.ptr);
}

제한(Restrictions):

  • PlacementExpression 타입은 변경 가능(mutable)하고 공유되지 않아야 해요.
  • Type은 연관 배열일 수 없어요. 연관 배열은 GC 힙에 있도록 설계됐기 때문이에요. 할당되는 연관 배열의 크기는 런타임 라이브러리가 결정하며 사용자가 설정할 수 없어요.
  • 배치 new는 @safe 코드에서 허용되지 않아요.

malloc() 같은 할당자 함수로 저장 공간을 할당하려면 간단한 템플릿을 사용할 수 있어요:

import core.stdc.stdlib;

struct S { int i = 1, j = 4, k = 9; }

ref void[T.sizeof] mallocate(T)()
{
    return malloc(T.sizeof)[0 .. T.sizeof];
}

void main()
{
    S* ps = new(mallocate!S()) S;
    assert(ps.i == 1);
    assert(ps.j == 4);
    assert(ps.k == 9);
}

typeid 식 (Typeid Expressions)

TypeidExpression:
    typeid ( Type )
    typeid ( Expression )

Type이면, Type에 해당하는 TypeInfo 클래스의 인스턴스를 반환해요.

Expression이면, Expression의 타입에 해당하는 TypeInfo 클래스의 인스턴스를 반환해요. 타입이 클래스이면 동적 타입(가장 파생된 타입)의 TypeInfo를 반환해요. Expression은 항상 실행돼요.

class A { }
class B : A { }

void main()
{
    TypeInfo tid = typeid(int);
    assert(tid.toString() == "int");

    uint i;
    tid = typeid(i++);
    assert(i == 1); // `i` was incremented
    assert(tid == typeid(uint));

    A a = new B();
    assert(typeid(a) == typeid(B)); // get dynamic type of `a`
    assert(typeid(typeof(a)) == typeid(A));
}

is 식 (Is Expressions)

IsExpression:
    is ( Type )
    is ( Type : TypeSpecialization )
    is ( Type == TypeSpecialization )
    is ( Type : TypeSpecialization , TemplateParameterList )
    is ( Type == TypeSpecialization , TemplateParameterList )
    is ( Type Identifier )
    is ( Type Identifier : TypeSpecialization )
    is ( Type Identifier == TypeSpecialization )
    is ( Type Identifier : TypeSpecialization , TemplateParameterList )
    is ( Type Identifier == TypeSpecialization , TemplateParameterList )

TypeSpecialization:
    Type
    TypeCtor
    struct
    union
    class
    interface
    enum
    __vector
    function
    delegate
    super
    return
    __parameters
    module
    package

IsExpression은 컴파일 타임에 평가되며 심볼/타입이 유효한 타입인지 확인하는 데 사용돼요. 또한 다음을 할 수 있는 형태도 있어요:

static if 조건으로 사용될 때 IsExpression은 다음도 할 수 있어요:

IsExpression의 결과는 조건이 만족되면 true, 아니면 false인 부울이에요.

Type은 테스트되는 심볼/타입이에요. 심볼의 경우 그 심볼은 Type으로 파싱되어야 해요. Type은 문법적으로 올바르지만 의미적으로는 올바르지 않아도 돼요. 의미적으로 올바르지 않으면 조건이 만족되지 않아요.

TypeSpecialization은 Type이 패턴 매칭되는 타입 또는 타입 관련 키워드예요.

참고: IsExpression은 typeof와 함께 사용해 표현식 타입 검사가 올바른지 확인할 수 있어요. 예를 들어 is(typeof(foo))는 foo가 유효한 타입을 가지면 true를 반환해요.

기본 형태 (Basic Forms)
is ( Type )

Type이 의미적으로 올바르면 조건이 만족돼요. Type은 어쨌든 문법적으로는 올바르지 않으면 안 돼요.

pragma(msg, is(5)); // error
pragma(msg, is([][])); // error
int i;
static assert(is(int));
static assert(is(typeof(i))); // same

static assert(!is(Undefined));
static assert(!is(typeof(int))); // int is not an expression
static assert(!is(i)); // i is a value

alias Func = int(int); // function type
static assert(is(Func));
static assert(!is(Func[])); // fails as an array of functions is not allowed
is ( Type : TypeSpecialization )

Type이 의미적으로 올바르고 TypeSpecialization과 같거나 그것으로 암시적으로 변환될 수 있으면 조건이 만족돼요. TypeSpecialization은 Type만 허용돼요.

alias Bar = short;
static assert(is(Bar : int)); // short implicitly converts to int
static assert(!is(Bar : string));
is ( Type == TypeSpecialization )

TypeSpecialization이 타입이면, Type이 의미적으로 올바르고 TypeSpecialization과 같은 타입일 때 조건이 만족돼요.

alias Bar = short;
static assert(is(Bar == short));
static assert(!is(Bar == int));

TypeSpecialization이 TypeCtor이면 Type이 그 TypeCtor일 때 조건이 만족돼요:

static assert(is(const int == const));
static assert(is(const int[] == const));
static assert(!is(const(int)[] == const)); // head is mutable
static assert(!is(immutable int == const));

TypeSpecialization이 struct, union, class, interface, enum, __vector, function, delegate, module, package 중 하나이면 Type이 그런 종류일 때 조건이 만족돼요.

Object o;
static assert(!is(o == class)); // `o` is not a type
static assert(is(Object == class));
static assert(is(ModuleInfo == struct));
static assert(!is(int == class));

void f();
static assert(!is(f == function)); // `f` is not a type
static assert(is(typeof(f) == function));
static assert(!is(typeof(&f) == function)); // function pointer is not a function

modulepackage 형태는 다른 형태와 달리 Type이 타입이 아니라 심볼일 때 만족돼요. 대신 isModuleisPackage __traits를 사용해야 해요. 패키지 모듈은 패키지이자 모듈로 간주돼요.

TypeSpecialization은 다음 키워드 중 하나일 수도 있어요:

keyword condition
super true if Type is a class or interface
return true if Type is a function, delegate or function pointer
__parameters true if Type is a function, delegate or function pointer
class C {}
static assert(is(C == super));

void foo(int i);
static assert(!is(foo == return));
static assert(is(typeof(foo) == return));
static assert(is(typeof(foo) == __parameters));

참고: Traits 참조.

식별자 형태 (Identifier Forms)

조건이 만족되면 Identifier는 결과 타입의 alias로 선언돼요. Identifier 형태는 IsExpression이 StaticIfCondition이나 StaticAssert의 첫 번째 인자에 나타날 때만 쓸 수 있어요.

is ( Type Identifier )

Type이 의미적으로 올바르면 조건이 만족돼요. 그렇다면 Identifier는 Type의 alias로 선언돼요.

struct S
{
    int i, j;
}
static assert(is(typeof(S.i) T) && T.sizeof == 4);
alias Bar = short;

void foo()
{
    static if (is(Bar T))
        alias S = T;
    else
        alias S = long;

    pragma(msg, S); // short

    // if T was defined, it remains in scope
    if (is(T))
        pragma(msg, T); // short

    //if (is(Bar U)) {} // error, cannot declare U here
}
is ( Type Identifier : TypeSpecialization )

TypeSpecialization이 타입이면, Type이 의미적으로 올바르고 TypeSpecialization과 같거나 그것으로 암시적으로 변환될 수 있을 때 조건이 만족돼요. Identifier는 TypeSpecialization의 alias로 선언돼요.

alias Bar = int;

static if (is(Bar T : int))
    alias S = T;
else
    alias S = long;

static assert(is(S == int));

TypeSpecialization이 Identifier를 포함한 타입 패턴이면, Type 또는 그것이 암시적으로 변환할 수 있는 타입을 기반으로 Identifier의 타입 추론이 시도돼요. 타입 패턴이 일치할 때에만 조건이 만족돼요.

struct S
{
    long* i;
    alias i this; // S converts to long*
}

static if (is(S U : U*)) // S is matched against the pattern U*
{
    U u;
}
static assert(is(U == long));

Identifier의 타입이 결정되는 방식은 TemplateTypeParameterSpecialization이 템플릿 매개변수 타입을 결정하는 방식과 유사해요.

is ( Type Identifier == TypeSpecialization )

TypeSpecialization이 타입이면, Type이 의미적으로 올바르고 TypeSpecialization과 같은 타입일 때 조건이 만족돼요. Identifier는 TypeSpecialization의 alias로 선언돼요.

const x = 5;

static if (is(typeof(x) T == const int))   // satisfied, T is now defined
    alias S = T;

static assert(is(T)); // T is in scope
pragma(msg, T); // const int

TypeSpecialization이 Identifier를 포함한 타입 패턴이면, Type을 기반으로 Identifier의 타입 추론이 시도돼요. 타입 패턴이 일치할 때에만 조건이 만족돼요.

alias Foo = long*;

static if (is(Foo U == U*)) // Foo is matched against the pattern U*
{
    U u;
}
static assert(is(U == long));

TypeSpecialization이 is(Type == Keyword) 형태의 유효한 키워드이면 조건이 같은 방식으로 만족돼요. Identifier는 다음과 같이 설정돼요:

keyword alias type for Identifier
struct Type
union Type
class Type
interface Type
super TypeSeq of base classes and interfaces
enum the base type of the enum
__vector the static array type of the vector
function TypeSeq of the function parameter types. For C- and D-style variadic functions, only the non-variadic parameters are included. For typesafe variadic functions, the ... is ignored.
delegate the function type of the delegate
return the return type of the function, delegate, or function pointer
__parameters the parameter sequence of a function, delegate, or function pointer. This includes the parameter types, names, and default values.
const Type
immutable Type
inout Type
shared Type
module the module
package the package
enum E : byte { Emember }

static if (is(E V == enum))    // satisfied, E is an enum
    V v;                       // v is declared to be a byte

static assert(is(V == byte));
매개변수 목록 형태 (Parameter List Forms)
is ( Type : TypeSpecialization , TemplateParameterList )
is ( Type == TypeSpecialization , TemplateParameterList )
is ( Type Identifier : TypeSpecialization , TemplateParameterList )
is ( Type Identifier == TypeSpecialization , TemplateParameterList )

TypeSpecialization이 Type으로 파싱될 때 다음이 패턴 매칭될 수 있어요:

TemplateParameterList는 일치된 패턴의 부분에 기반한 심볼들을 선언해요. 이는 암시적 템플릿 매개변수가 일치되는 방식과 유사해요(타입 매개변수 특수화 참조). TemplateDeclaration과 마찬가지로, 각 선언된 TemplateParameter는 특수화를 가질 수 있어요.

타입 템플릿 인스턴스화 일치 (Matching a Type Template Instantiation)

다음을 사용해야 해요:

struct Tuple(T...)
{
    // ...
}
alias Tup2 = Tuple!(int, string);

// `Template!Args` is the pattern
static if (is(Tup2 : Template!Args, alias Template, Args...))
{
    static assert(__traits(isSame, Template, Tuple));
    static assert(is(Template!(int, string) == Tup2)); // same struct
}
static assert(is(Args[0] == int));
static assert(is(Args[1] == string));

TypeSpecialization이 alias 템플릿 인스턴스일 때 Type은 일치될 수 없어요:

struct S(T) {}
alias A(T) = S!T;

static assert(is(A!int : S!T, T));
//static assert(!is(A!int : A!T, T));
파생 데이터 타입 일치 (Matching a Derived Data Type)

예: 연관 배열 일치

  • V는 AA 값 타입으로 선언
  • K는 AA 키 타입으로 선언
alias AA = long[string];

// V[K] is the pattern
// K must be convertible to string
static if (is(AA V : V[K], K : string))
{
    pragma(msg, V);  // long
    pragma(msg, K);  // string
}

// no match because B is not convertible to int
static assert(!is(AA A : A[B], B : int));

예: static 배열 일치

static if (is(int[10] E : E[len], size_t len)) // E[len] is the pattern
{
    static assert(len == 10);
}
static assert(is(E == int));

// no match, len should be 10
static assert(!is(int[10] X : X[len], size_t len : 5));

Rvalue 식 (Rvalue Expression)

RvalueExpression:
    __rvalue ( AssignExpression )

RvalueExpression은 내장된 AssignExpression이 rvalue인지 lvalue인지와 관계없이 그것을 rvalue로 취급하게 해요.

오버로딩 (Overloading)

함수 인자에 대해 ref와 비-ref 매개변수 오버로드가 모두 있으면, rvalue는 비-ref 매개변수에 선호되어 일치되고 lvalue는 ref 매개변수에 선호되어 일치돼요. RvalueExpression은 비-ref 매개변수와 선호되어 일치돼요.

rvalue 매개변수에 일치된 인자의 의미 (Semantics of Arguments Matched to Rvalue Parameters)

rvalue 함수 인자는 호출된 함수가 소유해요. 따라서 lvalue가 rvalue 함수 매개변수에 일치되면, 함수에 전달하기 위해 lvalue의 복사본이 만들어져요. 함수는 결론에서 매개변수의 소멸자(있다면)를 호출해요. rvalue 인자는 이미 고유하다고 가정되므로 복사되지 않고, 함수 결론에서 역시 소멸돼요.

매개변수가 rvalue에서 비롯되었든 lvalue의 복사본이든 호출된 함수의 의미는 같아요. 이는 RvalueExpression 인자가 함수 반환 시 그 식을 소멸시킨다는 것을 의미해요. lvalue 식을 계속 사용하려는 시도는 유효하지 않아요. 컴파일러는 lvalue가 함수에 전달된 후의 사용을 항상 감지하지는 못하므로, 객체의 소멸자가 객체의 내용을 초기 값 또는 적어도 두 번 이상 소멸될 수 있는 무해한 값으로 재설정해야 해요.

import core.stdc.stdlib;

struct S
{
    ubyte* p;

    ~this()
    {
        free(p);
        // add `p = null;` here to prevent double free
    }
}

void aggh(S s)
{
    // destructor of `s` called here, freeing `s.p`
}

void oops()
{
    S s;
    s.p = cast(ubyte*)malloc(10);
    aggh(__rvalue(s));
    // destructor of `s` called at end of scope, double-freeing `s.p`
}

RvalueExpression은 이동 생성자와 이동 대입을 사용하게 해줘요.

__rvalue 함수 속성 (__rvalue Function Attribute)

__rvalue 키워드는 함수 속성으로도 허용돼요. 이것은 함수의 반환 값을 RvalueExpression으로 취급하게 해요. 이 속성은 참조로 반환하는 함수에만 허용돼요.

struct S
{
    int* p;
    this(S rhs) { p = rhs.p; rhs.p = null; }
    this(ref S) { assert(0); }
}
ref S move(return ref S s) __rvalue
{
    return s;
}

S s;
s.p = new int(5);
// construct `t` by calling S's move constructor
S t = move(s); // call lowered to `__rvalue(move(s))`
assert(s.p is null);
assert(*t.p == 5);

특수 키워드 (Special Keywords)

SpecialKeyword:
    __FILE__
    __FILE_FULL_PATH__
    __MODULE__
    __LINE__
    __FUNCTION__
    __PRETTY_FUNCTION__

__FILE____LINE__은 인스턴스화 지점의 소스 파일 이름과 줄 번호로 확장돼요. 소스 파일의 경로는 컴파일러에 맡겨져요.

__FILE_FULL_PATH__는 인스턴스화 지점의 절대 소스 파일 이름으로 확장돼요.

__MODULE__은 인스턴스화 지점의 모듈 이름으로 확장돼요.

__FUNCTION__은 인스턴스화 지점의 함수의 완전 정규화된 이름으로 확장돼요.

__PRETTY_FUNCTION____FUNCTION__과 유사하지만 함수 반환 타입, 매개변수 타입, 속성도 확장해요.

예:

module test;
import std.stdio;

void test(string file = __FILE__, size_t line = __LINE__,
        string mod = __MODULE__, string func = __FUNCTION__,
        string pretty = __PRETTY_FUNCTION__,
        string fileFullPath = __FILE_FULL_PATH__)
{
    writefln("file: '%s', line: '%s', module: '%s',\nfunction: '%s', " ~
        "pretty function: '%s',\nfile full path: '%s'",
        file, line, mod, func, pretty, fileFullPath);
}

int main(string[] args)
{
    test();
    return 0;
}

파일이 /example/test.d에 있다고 가정하면, 이 코드는 출력해요:

file: 'test.d', line: '13', module: 'test',
function: 'test.main', pretty function: 'int test.main(string[] args)',
file full path: '/example/test.d'

경고(Warning): 현재 함수의 심볼을 얻으려고 mixin(__FUNCTION__)을 사용하지 마세요. 이것은 프로그래머들이 현재 함수에 대한 introspection을 위해 해당 심볼을 얻으려 할 때 흔히 시도하는 것으로 보여요. D가 현재 그 심볼을 얻는 직접적인 방법이 없기 때문이에요. 그러나 mixin(__FUNCTION__)을 사용하면 함수의 심볼이 이름으로 조회되는데, 이는 심볼 조회와 함께 오는 다양한 규칙의 적용을 받아 여러 문제를 일으킬 수 있어요. 그런 문제 중 하나는 함수가 오버로드된 경우 그 결과가 현재 함수인지 여부와 무관하게 첫 번째 오버로드가 된다는 점이에요.

D가 현재 함수의 심볼을 직접 얻는 방법이 없음을 감안할 때, 그것을 하는 가장 좋은 방법은 함수 안의 심볼의 부모 심볼을 얻는 것이에요. 심볼 조회 규칙을 둘러싼 문제들을 피할 수 있기 때문이에요. 다른 심볼에 의존하지 않는 예로는 __traits(parent {})이 있어요. 이것은 익명 중첩 함수를 선언하는데, 그 부모가 현재 함수예요. 따라서 그 부모를 얻으면 현재 함수의 심볼을 얻을 수 있어요.

결합성과 교환성 (Associativity and Commutativity)

구현은, 그 실행 스레드 안에서 관찰 가능한 차이가 없을 때, 산술 결합성·교환성 규칙에 따라 식의 평가를 재배열할 수 있어요.

이 규칙은 부동소수점 식의 결합적·교환적 재배열을 배제해요.

더 알아보기