Expressions
Expressions (식)
D 언어에서 **식(Expression)**은 연산자와 피연산자가 모여 하나의 계산을 나타내는 단위예요. 어떤 값이 계산되고, 그 값은 다시 대입되거나 검사되거나 무시될 수 있죠. 식은 그 과정에서 부수 효과(side effect)를 일으키기도 합니다. 이 장에서는 식의 문법, 평가 순서, 그리고 의미를 하나씩 정리해 볼게요.
본문
문법부터 살펴볼게요. 모든 식은 궁극적으로 CommaExpression(콤마 식)으로 귀결됩니다.
Expression:
CommaExpression
식은 값을 계산하는 데 쓰여요. 그 결과로 얻어진 값은 대입되거나, 검사되거나, 무시될 수 있죠. 식은 부수 효과를 일으킬 수도 있어요.
정의와 용어 (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()는 이 식의 부분식으로 파싱되지 않으므로, g() * 2의 전체 식이 될 수 없습니다.
정의 자체는 단순하지만, 함수 리터럴과 관련해선 미묘한 점이 몇 가지 있습니다. 다음을 볼게요.
return (() => x + f())() * g();
이 코드에서 f()의 전체 식은 x + f()예요. return에 전달된 표현식이 아니죠. 왜냐하면 x + f()의 부모는 식 타입이 아니라 함수 리터럴 타입이기 때문입니다.
lvalue (좌변값)
다음에 나열한 식들만, 그리고 오직 이것들만 lvalue 식(lvalue expression), 줄여서 lvalue라고 불러요.
struct·union멤버 함수 안의this- 변수, 함수 이름, 또는 참조(reference)로 반환하는 함수의 호출
.(PostfixExpression)과 모듈 범위 연산자(Module Scope Operator)의 결과 — 단, 점(dot)의 가장 오른쪽이 변수, 필드(직접 필드든 정적 필드든), 함수 이름, 또는 참조로 반환하는 함수의 호출일 때- 다음 식들의 결과:
- 내장 단항 연산자인
+(lvalue에 적용될 때),*,++(전위만),--(전위만) - 내장 인덱싱 연산자
[](슬라이싱 연산자는 제외) - 내장 대입 연산자, 즉
=,+=,*=,/=,%=,&=,|=,^=,~=,<<=,>>=,>>>=,^^= - 사용자 정의 연산자 — 단, 낮춤(lowering) 결과로 호출된 함수가 참조로 반환할 때만
- 조건식 연산자
e ? e1 : e2— 아래 상황에서만:e1과e2가 같은 타입의 lvalue일 때; 또는e1·e2중 하나가 타입T의 lvalue이고, 다른 하나가T의 lvalue로 변환해 주는alias this를 가질 때
mixin식 — 단,mixin에 전달된 인자(들)를 컴파일한 결과가 lvalue일 때만- lvalue에 적용된
cast(U)식 — 단,T*이U*로 암묵적으로 변환될 때 - lvalue에 적용된
cast(*TypeCtors*opt)
- 내장 단항 연산자인
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)**이 있다면 이렇게 정의돼요. fullexpr의 부분식이면서 AndAndExpression(&&) 또는 OrOrExpression(||)이고, 동시에 expr이 그 부분식이 되는 최단 길이의 부분식 scexpr이 바로 그것입니다. 예를 들어 볼게요.
((f() * 2 && g()) + 1) || h()
위에서 부분식 f() * 2의 최소 단락 식은 f() * 2 && g()예요. 그런데 이번엔 이걸 볼게요.
(f() && g()) + h()
이 코드에서 부분식 h()는 최소 단락 식이 존재하지 않아요. h()를 감싸는 ||나 &&가 없기 때문이죠.
평가 순서 (Order Of Evaluation)
모범 사례: 평가 순서가 잘 정의돼 있다 해도, 그 순서에 의존하도록 코드를 작성하는 건 바람직하지 않아요.
증가와 감소 (Increment and Decrement)
내장 전위 단항 식 ++와 --는 대입으로 낮춰(rewrite) 평가됩니다.
| 식 | 동등한 표현 |
|---|---|
++expr |
((expr) += 1) |
--expr |
((expr) -= 1) |
따라서 전위 ++·--의 결과는 부수 효과가 적용된 이후의 lvalue예요.
내장 후위 단항 식 ++와 --는 람다 호출로 낮춰 평가됩니다.
| 식 | 동등한 표현 |
|---|---|
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);
OrOrExpression과 AndAndExpression은 왼쪽 피연산자를 먼저 평가합니다. 그다음 OrOrExpression은 왼쪽이 0이 아닌 값으로 평가되지 않을 때만 오른쪽을 평가하고, AndAndExpression은 왼쪽이 0이 아닌 값으로 평가될 때만 오른쪽을 평가해요.
구현 정의(Implementation Defined):
AssignExpression의 피연산자 평가 순서.
조건식 (Conditional Expressions)
ConditionalExpression은 왼쪽 피연산자를 먼저 평가해요. 결과가 0이 아니면 두 번째 피연산자를, 그렇지 않으면 세 번째 피연산자를 평가합니다.
함수 호출 (Function Calls)
extern(D) 링키지(기본값)를 가진 함수 호출은 다음 순서로 평가됩니다.
- 필요하다면, 호출할 함수의 주소를 평가한다(예: 계산된 함수 포인터나 delegate의 경우).
- 인자를 왼쪽에서 오른쪽으로 평가한다.
- 실행이 함수로 전달된다.
함수 포인터를 호출하는 예시예요.
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());
구현 정의:
extern(D)이외의 링키지를 가진 함수의 인자 평가 순서.
임시 값의 수명 (Lifetime of Temporaries)
식과 문은 rvalue를 만들거나 소비할 수 있어요. 이런 값들을 **임시 값(temporaries)**이라고 부르고, 이름도 가시적인 스코프도 없습니다. 그 수명은 이 절에서 설명하는 규칙에 따라 자동으로 관리됩니다.
임시 값을 산출하는 각 평가에서, 그 임시 값의 수명은 평소처럼 이름 있는 값을 식으로 초기화해서 만드는 것과 마찬가지로 평가 시점에 시작돼요.
임시 값의 수명 종료는 흔한 스코프 규칙을 따르지 않고 다음과 같이 정의됩니다.
- 만약:
- 전체 식에 최소 단락 식
expr이 있고; - 그 임시 값이
&&또는||연산자의 오른쪽에서 만들어졌으며; - 그 오른쪽이 평가된다면;
- 임시 값 소멸자(destructor)는 오른쪽 식이 평가되어
bool로 변환된 직후에 평가된다. 소멸은 생성 순서의 역순으로 진행된다.
- 전체 식에 최소 단락 식
- 그 외 모든 경우, 함수를 호출하기 위해 만들어진 임시 값들은 전체 식의 끝까지 미뤄진다. 소멸 순서는 생성 순서의 역순이다.
식의 어떤 부분식이 예외를 던지면, 그 부분식의 평가 시점까지 만들어진 모든 임시 값은 위 규칙대로 파괴됩니다. 아직 생성되지 않은 임시 값에는 소멸자 호출이 이뤄지지 않아요.
참고: 이 규칙들에 깔린 직관은 이래요. 임시 값의 소멸자는 전체 식의 끝까지, 그리고 생성 순서의 역순으로 미뤄진다. 단, &&와 ||의 오른쪽은 더 큰 식의 일부라도 그 자체가 하나의 전체 식으로 간주한다는 예외가 있죠.
참고: ConditionalExpression인 e1 ? e2 : e3는 조건부로 식을 평가하지만 특별한 경우가 아니에요. e1과 e2·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): 피연산자 중 하나가 참조 타입이고 다음 중 하나일 때:
- 피연산자들이 부분적으로 겹치는 저장 공간을 가질 때
- 피연산자들의 저장 공간이 정확히 겹치지만 타입이 다를 때
구현 정의: 피연산자 중 어느 것도 참조 타입이 아니고 다음 중 하나일 때:
- 피연산자들이 부분적으로 겹치는 저장 공간을 가질 때
- 피연산자들의 저장 공간이 정확히 겹치지만 타입이 다를 때
단순 대입 식 (Simple Assignment Expression)
연산자가 =이면 단순 대입입니다.
- 왼쪽 피연산자가
opAssign을 정의한 struct라면, 그 동작은 오버로드된 함수에 의해 정의된다. - 왼쪽·오른쪽 피연산자가 같은 struct 타입이고 그 struct 타입이 Postblit을 가진다면, 복사 동작은 Struct Postblit에 설명된 대로다.
- lvalue가 동적 배열의
.length속성이라면, 동작은 Setting Dynamic Array Length에 설명된 대로다. - 왼쪽 피연산자가 슬라이스 식이라면, 동작은 Array Copying과 Array Filling에 설명된 대로다.
- lvalue가 배열이라면, 동작은 Array Assignment에 설명된 대로다.
- lvalue가 사용자 정의 속성이라면, 동작은 Property Functions에 설명된 대로다.
그 외에는 오른쪽 피연산자가 왼쪽 피연산자의 타입으로 암묵적으로 변환되어 대입됩니다.
대입 연산자 식 (Assignment Operator Expressions)
내장 타입의 인자에 대해, a op= b 같은 대입 연산자 식은 의미상 다음과 동등합니다.
a = cast(typeof(a))(a op b)
단, 다음 차이가 있어요.
- 피연산자
a는 한 번만 평가된다. op를 오버로드하는 것과op=를 오버로드하는 것은 다른 함수를 쓴다.>>>=의 왼쪽 피연산자는 시프트 전에 Integer Promotions를 거치지 않는다.
좁히기(narrowing) 변환은 허용되고, 잘라내기(truncating) 변환은 오류가 됩니다.
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 식 (OrOr Expressions)
OrOrExpression:
AndAndExpression
OrOrExpression || AndAndExpression
OrOrExpression의 결과 타입은 bool이에요. 단, 오른쪽 피연산자가 void 타입이면 그때는 결과 타입이 void가 됩니다.
OrOrExpression은 왼쪽 피연산자를 평가해요.
- 왼쪽 피연산자를
bool로 변환한 값이true라면, 오른쪽 피연산자는 평가되지 않는다.OrOrExpression의 결과 타입이bool이면 식의 결과는true가 된다. - 왼쪽 피연산자가
false라면 오른쪽 피연산자가 평가된다.OrOrExpression의 결과 타입이bool이면 식의 결과는 오른쪽 피연산자를bool로 변환한 값이 된다.
AndAnd 식 (AndAnd Expressions)
AndAndExpression:
OrExpression
AndAndExpression && OrExpression
AndAndExpression의 결과 타입은 bool이에요. 단, 오른쪽 피연산자가 void 타입이면 그때는 결과 타입이 void가 됩니다.
AndAndExpression은 왼쪽 피연산자를 평가해요.
- 왼쪽 피연산자를
bool로 변환한 값이false라면, 오른쪽 피연산자는 평가되지 않는다.AndAndExpression의 결과 타입이bool이면 식의 결과는false가 된다. - 왼쪽 피연산자가
true라면 오른쪽 피연산자가 평가된다.AndAndExpression의 결과 타입이bool이면 식의 결과는 오른쪽 피연산자를bool로 변환한 값이 된다.
비트 단위 식 (Bitwise Expressions)
비트 단위 식은 피연산자에 대해 비트 단위 연산(bitwise operation)을 수행해요. 피연산자는 정수 타입이어야 합니다. 먼저 Usual Arithmetic Conversions(평소 산술 변환)이 일어나고, 그다음에 비트 단위 연산이 수행됩니다.
참고:
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 식 (Or Expressions)
OrExpression:
XorExpression
OrExpression | XorExpression
피연산자들이 서로 OR 됩니다.
Xor 식 (Xor Expressions)
XorExpression:
AndExpression
XorExpression ^ AndExpression
피연산자들이 서로 XOR 됩니다.
And 식 (And Expressions)
AndExpression:
CmpExpression
AndExpression & CmpExpression
피연산자들이 서로 AND 됩니다.
비교 식 (Compare Expressions)
CmpExpression:
EqualExpression
IdentityExpression
RelExpression
InExpression
ShiftExpression
동등 식 (Equality Expressions)
EqualExpression:
ShiftExpression == ShiftExpression
ShiftExpression != ShiftExpression
동등 식은 두 피연산자가 같은지(==) 다른지(!=)를 비교합니다. 결과 타입은 bool이에요.
부등(inequality)은 동등(equality)의 논리 부정으로 정의됩니다.
- 피연산자가 정수 값이면, 비교 전에 Usual Arithmetic Conversions를 적용해 공통 타입으로 만든다. 동등은 공통 타입의 비트 패턴이 정확히 일치하는 것으로 정의된다.
- 피연산자가 포인터면, 동등은 피연산자의 비트 패턴이 정확히 일치하는 것으로 정의된다. 두 타입이 일치하거나, 하나가
typeof(null)이어야 한다. float,double,real값이면, 비교 전에 Usual Arithmetic Conversions를 적용해 공통 타입으로 만든다.-0과+0은 같다고 간주된다. 피연산자 중 하나라도NaN이면==는false를,!=는true를 반환한다. 그 외에는 공통 타입의 비트 패턴을 비교한다.- 정적 배열과 동적 배열에 대해, 동등은 두 배열의 길이가 일치하고 각 원소가 서로 같다고 비교되는 것으로 정의된다. 원소 타입은 공통 타입이 있어야 한다.
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
클래스·구조체 동등 (Class & Struct Equality)
클래스 참조에 대해 a == b는 .object.opEquals(a, b)로 다시 쓰이며, 이는 null도 처리합니다. 이는 두 객체의 내용을 비교하기 위한 것이지만, 그러려면 적절한 opEquals 메서드 오버라이드가 정의돼 있어야 해요. 루트 Object 클래스가 제공하는 기본 opEquals는 is 연산자와 동등합니다.
구조체 객체에 대해 (a == b)는 a.opEquals(b)로 다시 쓰이고, 그것이 실패하면 b.opEquals(a)로 다시 써져요.
클래스 참조와 구조체 객체 모두에서 (a != b)는 !(a == b)로 다시 쓰입니다.
opEquals에 대한 자세한 내용은 Struct Equality를 참고하세요.
구조체 객체에서 동등은 opEquals() 멤버 함수의 결과를 의미해요. opEquals()가 제공되지 않으면 하나가 생성됩니다. 동등은 대응하는 객체 필드들의 모든 동등 결과의 논리곱(logical product)으로 정의됩니다.
struct S
{
int i = 4;
string s = "four";
}
S s;
assert(s == S());
s.s = "foul";
assert(s != S());
구현 정의: 구조체 객체의 정렬 공백(alignment gaps) 내용.
겹치는 필드가 있다면(unions에서 발생), 기본 동등은 겹치는 각 필드를 비교합니다.
모범 사례: opEquals()는 겹치는 필드 중 어느 것이 유효한 데이터를 담는지 고려할 수 있어요. opEquals()는 부동소수점 NaN 값이 항상 같지 않다고 비교되는 기본 동작을 재정의할 수 있고요. 다음 상황에서 memcmp()로 opEquals()를 구현할 때는 조심해야 합니다.
- 정렬 공백이 있을 때
- 어떤 필드든
opEquals()가 있을 때 NaN또는-0값을 담을 수 있는 부동소수점 필드가 있을 때
동일성 식 (Identity Expressions)
IdentityExpression:
ShiftExpression is ShiftExpression
ShiftExpression ! is ShiftExpression
is 연산자는 식 값의 동일성(identity)을 비교해요. 동일하지 않음을 비교하려면 e1 !is e2를 쓰면 됩니다. 결과 타입은 bool이고, 피연산자는 비교 전에 Usual Arithmetic Conversions로 공통 타입이 됩니다.
클래스/인터페이스 객체에 대해 동일성은 객체 참조가 같다는 것으로 정의됩니다. 클래스 참조는 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
}
구조체 객체와 부동소수점 값에 대해 동일성은 피연산자의 비트가 같다는 것으로 정의됩니다.
정적 배열·동적 배열에 대해 두 배열의 동일성은 두 배열이 같은 메모리 위치를 참조하고 같은 수의 원소를 담을 때 성립합니다.
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:
is를 정적 배열을 주소와 길이로 비교하는 데 쓰는 것은 권장되지 않아요. 대신 슬라이스 연산자를 써서 배열 슬라이스를 비교하세요. 예:a1[] is a2[].
다른 피연산자 타입에 대해 동일성은 동등과 같은 것으로 정의됩니다.
is 동일성 연산자는 오버로드할 수 없어요.
관계 식 (Relational Expressions)
RelExpression:
ShiftExpression < ShiftExpression
ShiftExpression <= ShiftExpression
ShiftExpression > ShiftExpression
ShiftExpression >= ShiftExpression
먼저 피연산자에 Usual Arithmetic Conversions가 적용됩니다. 관계 식의 결과 타입은 bool이에요.
두 피연산자가 모두 포인터라면, 호환 가능한 타입을 가리켜야 합니다. 또한 같은 메모리 객체, 또는 그 메모리 객체의 바로 다음 위치를 가리켜야 해요.
배열 비교 (Array Comparisons)
정적 배열·동적 배열에 대해 CmpExpression의 결과는 배열에서 처음으로 서로 다른 원소에 연산자를 적용한 결과입니다. 두 배열이 서로 같게 비교되는데 길이가 다르다면, 더 짧은 배열이 더 긴 배열보다 "작다"고 비교됩니다.
정수 비교 (Integer Comparisons)
정수 비교는 두 피연산자가 모두 정수 타입일 때 일어납니다.
| 연산자 | 관계 |
|---|---|
< |
less (작다) |
> |
greater (크다) |
<= |
less or equal (작거나 같다) |
>= |
greater or equal (크거나 같다) |
== |
equal (같다) |
!= |
not equal (같지 않다) |
<, <=, > 또는 >= 식에서 한 피연산자가 signed이고 다른 하나가 unsigned인 것은 오류예요. cast를 써서 두 피연산자를 모두 signed 또는 모두 unsigned로 만들어야 합니다.
부동소수점 비교 (Floating Point Comparisons)
피연산자 중 하나 또는 둘 다 부동소수점이라면 부동소수점 비교가 수행됩니다.
CmpExpression은 NaN 피연산자를 가질 수 있어요. 피연산자 중 하나라도 NaN이면 부동소수점 비교 연산은 다음과 같이 반환합니다.
| 연산자 | 관계 | 반환 |
|---|---|---|
< |
less (작다) | false |
> |
greater (크다) | false |
<= |
less or equal | false |
>= |
greater or equal | false |
== |
equal | false |
!= |
unordered, less, or greater | true |
모범 사례: IdentityExpression으로 T.nan을 검사할 수는 있지만, 런타임에 만들어지는 NaN은 다른 부동소수점 값들도 있어요. 모든 것을 처리하려면 std.math.traits.isNaN을 쓰세요.
클래스·구조체 비교 (Class and Struct Comparisons)
구조체 객체에 대해 RelExpression은 먼저 일치하는 opCmp 메서드 호출을 평가하는 비교를 수행해요.
클래스 참조에 대해 RelExpression은 먼저 다음 중 하나인 int로 평가되는 비교를 수행합니다.
- 두 객체 참조가 같다면
0 - 왼쪽 식이
null이면-1 - 오른쪽 식이
null이면1 - 일치하는
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
}
둘째로, 클래스·구조체 객체에 대해 평가된 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
피연산자는 정수 타입이어야 하고 Integer Promotions를 거칩니다. 결과 타입은 승격 후 왼쪽 피연산자의 타입이에요. 결과 값은 비트를 오른쪽 피연산자의 값만큼 시프트한 결과입니다.
<<은 왼쪽 시프트.>>은 부호 있는(signed) 오른쪽 시프트.>>>은 부호 없는(unsigned) 오른쪽 시프트.
구현 정의: 음수 값만큼, 또는 시프트 대상 크기와 같거나 그보다 많은 비트만큼 시프트한 결과는 정의되지 않는다. 시프트 양이 컴파일 타임에 알려지면, 이렇게 하는 것은 컴파일 오류를 낸다.
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)
가산 연산 +와 -의 경우:
피연산자가 정수 타입이면 Usual Arithmetic Conversions를 거치고, 그다음 Usual Arithmetic Conversions를 사용해 공통 타입으로 만듭니다.
두 피연산자가 모두 정수 타입이고 계산에서 오버플로나 언더플로가 발생하면, 감싸기(wrapping)가 일어나요. 예:
uint.max + 1 == uint.minuint.min - 1 == uint.maxint.max + 1 == int.minint.min - 1 == int.max
피연산자 중 하나가 부동소수점 타입이면, 다른 하나가 암묵적으로 부동소수점으로 변환되고 Usual Arithmetic Conversions로 공통 타입이 됩니다.
부동소수점 피연산자에 대한 덧셈 식은 결합적(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은 컨테이너의 데이터를 다른 데이터와 이어붙여 새로운 컨테이너를 만들어요.
동적 배열에 대해, 다른 피연산자는 다른 배열이거나 배열의 원소 타입으로 암묵적으로 변환되는 단일 값이어야 합니다. Array Concatenation을 참고하세요.
곱셈 식 (Mul Expressions)
MulExpression:
UnaryExpression
MulExpression * UnaryExpression
MulExpression / UnaryExpression
MulExpression % UnaryExpression
피연산자는 산술 타입이어야 하고 Usual Arithmetic Conversions를 거칩니다.
정수 피연산자에 대해 *, /, %는 각각 곱셈, 나눗셈, 나머지 연산에 해당해요. 곱셈의 경우 오버플로는 무시되고 정수 타입에 맞게 잘려 들어갑니다.
나눗셈 (Division)
/와 % 연산자의 정수 피연산자에 대해 몫은 0을 향해 반올림되고, 나머지는 피제수(dividend)와 같은 부호를 가집니다.
다음 나눗셈·나머지 정수 피연산자들은:
- 분모가 0일 때
- 부호 있는
int.min이 분자이고-1이 분모일 때 - 부호 있는
long.min이 분자이고-1L이 분모일 때
컴파일 타임 실행(Compile Time Execution) 중에 만나면 불법입니다.
미정의 동작: 런타임에 만나면 나타난다.
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
| 연산자 | 설명 |
|---|---|
& |
lvalue의 메모리 주소를 취한다 — pointers 참고 |
++ |
사용 전에 증가 — 평가 순서 참고 |
-- |
사용 전에 감소 |
* |
역참조/간접 — 보통 포인터용 |
- |
음수 |
+ |
양수 |
! |
논리 NOT |
단항 -와 + 연산 전에 평소의 Integer Promotions가 수행됩니다.
보수 식 (Complement Expressions)
ComplementExpression:
~ UnaryExpression
ComplementExpression은 정수 타입(bool 제외)에 대해 동작해요. 값의 모든 비트가 보수됩니다. 보수 연산 전에 평소 Integer Promotions가 수행됩니다.
삭제 식 (Delete Expressions)
DeleteExpression:
delete UnaryExpression
Legacy:
delete는 제거되었다. 가능하면destroy를 쓰고, 최후의 수단으로core.memory.__delete를 사용하세요.
UnaryExpression이 클래스 객체 참조이고 그 클래스에 소멸자가 있으면, 그 객체 인스턴스에 대한 소멸자가 호출됩니다.
다음으로, UnaryExpression이 클래스 객체 참조이거나 struct 인스턴스에 대한 포인터이고, 그 클래스나 struct가 오버로드된 operator delete를 가진다면, 그 클래스 객체 인스턴스나 struct 인스턴스에 대한 operator delete가 호출됩니다.
그 외에는 가비지 컬렉터가 호출되어 클래스 인스턴스나 struct 인스턴스에 할당된 메모리를 즉시 해제합니다.
UnaryExpression이 포인터나 동적 배열이면, 가비지 컬렉터가 호출되어 메모리를 즉시 해제합니다.
delete가 수행된 후 포인터, 동적 배열, 또는 참조는 null로 설정됩니다. 삭제 후 다른 참조를 통해 그 데이터를 참조하려는 시도는 미정의 동작을 일으켜요.
UnaryExpression이 스택에 할당된 변수라면, 그 인스턴스에 대한 클래스 소멸자(있으면)가 호출됩니다. 가비지 컬렉터는 호출되지 않아요.
미정의 동작:
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)
기본 타입에 대한 암묵적 변환이 수행될 수 없는 상황에서는, cast를 사용해 타입 시스템이 메모리 영역의 재해석을 받아들이도록 강제할 수 있어요.
그런 시나리오의 예로 더 넓은 타입을 더 좁은 타입에 저장하려는 경우가 있어요.
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);
참고: Casting Integers.
클래스 참조 (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의 인스턴스인지 판별하려면 cast를 쓰세요.
if (cast(B) o)
{
// o is an instance of B
}
else
{
// o is not an instance of B
}
포인터 타입을 클래스 타입으로, 또는 그 반대로 캐스팅하는 것은 타입 페인트(type paint, 즉 재해석 캐스트)로 수행됩니다.
포인터 (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(더 넓은)의 포인터로 캐스팅할 때, A의 크기를 넘어선 메모리 접근을 시도하면 미정의 동작이 됩니다.
char c = 'a';
int *p = cast(int*) (&c);
writeln(*p);
포인터를 기본 데이터 타입으로 캐스팅할 수도 있어요. 흔한 관례는 포인터를 int 값으로 캐스팅한 뒤 그 주소를 출력하는 것이에요.
import core.stdc.stdlib;
int *p = cast(int*) malloc(int.sizeof);
int a = cast(int) p;
writeln(a);
배열 (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);
미정의 동작: 어떤 원소가 0이나 1이 아닌 바이트 표현을 가질 때 비-리터럴 배열을
bool[]로 캐스팅하는 것.
참고: 배열 리터럴 캐스팅(Casting array literals).
정적으로 알려진 길이의 슬라이스는 각 데이터의 바이트 수가 일치할 때 정적 배열 타입으로 캐스팅될 수 있어요.
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
}
참고: 슬라이스에서 정적 배열로의 변환(Slice conversion to static array).
정적 배열 (Static Arrays)
정적 배열을 다른 정적 배열로 캐스팅하는 것은 배열 길이 곱하기 원소 크기가 일치할 때만 수행되며, 불일치는 불법이에요. 캐스트는 타입 페인트(재해석 캐스트)로 수행되고 배열의 내용은 바뀌지 않습니다.
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
*/
기본 타입은 크기가 같을 때 단일 원소 정적 배열 타입으로 캐스팅될 수 있어요. 반대로 단일 원소 정적 배열도 같은 크기의 기본 타입으로 캐스팅될 수 있습니다. 캐스트는 타입 페인트(재해석 캐스트)로 수행돼요.
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)
부동소수점 리터럴을 한 타입에서 다른 타입으로 캐스팅하면 그 타입이 바뀌지만, 내부적으로는 상수 폴딩(constant folding)을 위해 전체 정밀도로 유지됩니다.
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
}
부동소수점 값을 정수 타입으로 캐스팅하는 것은 잘라내기(truncation)로 정수로 변환하는 것과 같아요. 부동소수점 값이 정수 타입 범위를 벗어나면 캐스트는 잘못된 결과를 만들어 냅니다(이것은 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);
}
구조체 (Structs)
어떤 식 e는 struct 타입 S로 캐스팅될 수 있어요.
- 컴파일러는 PostfixExpression인
S(e)를 시도한다. 이것이 실패하면: e가 struct 또는 정적 배열 인스턴스일 때, 그 데이터는 목적지 타입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 속성이 같은 결과를 각각 줄 때 정적 배열 타입으로 캐스팅될 수 있어요.
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에서는 "has 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 블록에 들어온 이후 통과한 적절한 scope(exit)/scope(failure)를 실행한 뒤, 적절한 catch 절에서 계속하도록 합니다. (try-statement의 catch 절)
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이에요.
모범 사례: 프로그램 버그를 보고하고 프로그램을 중단하려면 Error보다 Assert Expressions를 쓰세요.
거듭제곱 식 (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
| 연산 | 설명 |
|---|---|
.*Identifier* |
다음 중 하나: 타입 또는 식의 속성(property) 접근 / 모듈·패키지·집계 타입·인스턴스·enum·템플릿 인스턴스의 멤버 접근 / 집계 타입 인스턴스에 대한 포인터 역참조 후 멤버 접근 / UFCS를 사용한 자유 함수 호출 |
.*NewExpression* |
중첩 클래스(nested class) 인스턴스화 |
++ |
사용 후 증가 — 평가 순서 참고 |
-- |
사용 후 감소 |
(args) |
다음 중 하나: 선택적 인자를 가진 식 호출 / 사용자 정의 타입에서 정적 opCall 호출 / 선택적 인자를 가진 타입 생성 |
*IndexOperation* |
단일 원소 선택 |
*SliceOperation* |
일련의 원소 선택 |
후위 인자 목록 (Postfix Argument Lists)
ArgumentList:
AssignExpression
AssignExpression ,
AssignExpression , ArgumentList
NamedArgumentList:
NamedArgument
NamedArgument ,
NamedArgument , NamedArgumentList
NamedArgument:
Identifier : AssignExpression
AssignExpression
호출 가능한 식 (Callable Expressions)
호출 가능한 식은 괄호 안의 이름 있는 인자 목록 앞에 올 수 있어요. 다음 식들이 호출될 수 있습니다.
- 함수
- 함수 포인터
- delegate
opCall을 정의한 집계 타입 인스턴스
void f(int, int);
void g()
{
f(5, 6);
(&f)(5, 6);
}
인자를 매개변수에 맞추기 (Matching Arguments to Parameters)
NamedArgumentList의 인자는 다음과 같이 함수 매개변수에 맞춰집니다.
- 첫 번째 인자에 이름이 없으면 첫 번째 함수 매개변수에 할당된다.
- 이름 있는 인자는 같은 이름을 가진 함수 매개변수에 할당된다. 그런 매개변수가 없으면 오류다.
- 이름 없는 인자는 앞선 인자가 할당된 매개변수에 상대적인 다음 매개변수에 할당된다. 그런 매개변수가 없으면(즉 앞선 인자가 마지막 매개변수에 할당하면) 오류다.
- 매개변수에 두 번 이상 할당하는 것은 오류다.
- 매개변수에 인자를 할당하지 않는 것도 오류다. 단, 매개변수가 기본 인자(Default Argument)를 가질 때는 예외.
인자 목록으로 타입 생성하기 (Constructing a Type with an Argument List)
타입이 인자 목록 앞에 올 수 있어요. 다음을 참고하세요.
- Struct Literals
- Struct Constructors
- 내장 스칼라 타입의 균일 생성 구문(Uniform construction syntax for built-in scalar types)
인덱스 연산 (Index Operations)
IndexOperation:
[ ArgumentList ]
기본 PostfixExpression이 평가됩니다. 특수 변수 $가 선언되고 기본 PostfixExpression의 원소 수(가능할 때)로 설정돼요. ArgumentList 평가를 위해 새로운 선언 스코프가 만들어지고 $는 그 스코프에서만 나타납니다.
- PostfixExpression이 정적 또는 동적 배열 타입의 식이면, 인덱싱의 결과는 배열에서 ArgumentList로 평가된 정수
i번째 원소의 lvalue다. 배열 인덱싱 참고. - PostfixExpression이 포인터
p이면, 결과는*(p + i)다(Pointer Arithmetic 참고). - 기본 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이 정적·동적 배열
a이면, 슬라이스의 결과는 원소a[i]부터a[j-1]까지(포함)를 참조하는 동적 배열이며,i·j는 각각 첫 번째·두 번째 AssignExpression에서 평가된 정수다. 배열 슬라이싱 참고. - 기본 PostfixExpression이 포인터
p이면, 결과는 원소p[i]부터p[j-1]까지(포함)를 참조하는 동적 배열이며,i·j는 각각 첫 번째·두 번째 AssignExpression에서 평가된 정수다. - 기본 PostfixExpression이 ValueSeq이면, 슬라이스의 결과는 정적 상수로 평가되어야 하는 상한·하한에서 형성된 새 ValueSeq다. 그 경계가 범위를 벗어나면 오류다.
첫 번째 AssignExpression은 슬라이스의 포함(inclusive) 하한으로, 두 번째 AssignExpression은 배제(exclusive) 상한으로 취해져요. 식의 결과는 PostfixExpression의 원소들 중 슬라이스입니다.
[ ] 형식을 쓰면 슬라이스는 기본 PostfixExpression의 모든 원소가 돼요. 기본 식은 포인터일 수 없습니다.
슬라이스 연산자는 오버로드될 수 있어요. 둘 이상의 Slice를 사용하는 것은 연산자 오버로딩에만 지원됩니다.
SliceOperation은 수정 가능한 lvalue가 아니에요.
슬라이스에서 정적 배열로의 변환 (Slice Conversion to Static Array)
슬라이스 경계를 컴파일 타임에 알 수 있으면, 슬라이스 식은 정적 배열 lvalue로 암묵적으로 변환될 수 있습니다. 예:
arr[a .. b] // typed T[]
a와 b가 모두 정수(상수 폴딩 가능)라면, 슬라이스 식은 T[b - a] 타입의 정적 배열로 변환될 수 있어요.
참고: 정적 배열은 슬라이스에서도 할당될 수 있는데, 이때 길이가 일치하는지 런타임 검사를 수행합니다.
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* |
모듈 범위 연산자(Module Scope Operator) |
$ |
인덱싱/슬라이싱되는 객체의 원소 수 |
(*Type*).*Identifier* |
타입의 속성 또는 타입의 정적 멤버 접근 |
*FundamentalType*(arg) |
선택적 인자를 가진 스칼라 타입의 균일 생성 |
(*Type*)(args) |
선택적 인자를 가진 타입 생성 |
(*Expression*) |
식 평가 — 부분식으로 유용 |
this
생성자 또는 비-정적 멤버 함수 안에서 this는 함수가 호출된 객체에 대한 참조로 해석돼요.
typeof(this)는 집계 타입 정의 안의 어디에서나 유효합니다. 클래스 멤버 함수가 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 대입은 허용되지 않아요.
참고:
- 위임 생성자(Delegating Constructors)
template this매개변수
super
super는 this와 동일하되, this의 기본 클래스로 캐스팅된 것입니다. 기본 클래스가 없으면 오류예요. (기본 클래스가 없는 유일한 extern(D) 클래스는 Object인데, extern(C++) 클래스는 지정하지 않으면 기본 클래스가 없다는 점을 유의하세요.) 멤버 함수가 super에 대한 명시적 참조로 호출되면 비-가상 호출이 이뤄져요.
super에 대한 대입은 허용되지 않습니다.
참고: 기본 클래스 생성(Base Class Construction).
null
null은 포인터, 함수 포인터, delegate, 동적 배열, 연관 배열, 클래스 객체에 대한 null 값을 나타냅니다. 아직 타입으로 캐스팅되지 않았다면 단수 타입 typeof(null)이 주어지고, 그것을 포인터·함수 포인터·delegate 등의 null 값으로 변환하는 것은 정확(exact) 변환이에요. 타입으로 캐스팅된 뒤에는 그런 변환은 암묵적이지만 더 이상 정확하지는 않습니다.
문자열 리터럴 (String Literals)
StringLiteral 문법 참고.
문자열 리터럴은 읽기 전용이에요. StringPostfix가 없는 문자열 리터럴은 다음 타입들(같은 가중치) 중 어느 것으로든 암묵적으로 변환될 수 있습니다.
| 타입 |
|---|
immutable(char)* |
immutable(wchar)* |
immutable(dchar)* |
immutable(char)[] |
immutable(wchar)[] |
immutable(dchar)[] |
미정의 동작: 문자열 리터럴에 쓰는 것. 이는
@safe코드에서 허용되지 않는다.
기본적으로 문자열 리터럴은 동적 배열로 타입이 정해지지만, 원소 수는 컴파일 타임에 알려져요. 그래서 모든 문자열 리터럴은 immutable 정적 배열로 암묵적으로 변환될 수 있습니다.
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
}
문자열 리터럴은 같거나 더 긴 길이의 정적 배열 rvalue로 변환됩니다. 추가 원소들은 0으로 채워져요. 문자열 리터럴은 같은 길이의 정적 배열 lvalue로도 변환될 수 있습니다.
문자열 리터럴에는 '\0'이 덧붙여져서, null로 끝나는 const char* 문자열을 기대하는 C·C++ 함수에 넘기기 쉽게 해 줍니다. '\0'은 문자열 리터럴의 .length 속성에는 포함되지 않아요.
문자열 리터럴의 이어붙이기(concatenation)는 ~ 연산자가 필요하고 컴파일 타임에 해결됩니다. 중간 연산자 없이 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진수 문자열의 빅 엔디언(big endian) 바이트 순서가 가정됩니다.
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
기본적으로 배열 리터럴은 동적 배열로 타입이 정해지지만, 원소 수는 컴파일 타임에 알려져 있어요. 따라서 배열 리터럴은 같은 길이의 정적 배열로 암묵적으로 변환될 수 있습니다.
int[2] sa = [1, 2]; // OK
int[2] sb = [1]; // error
참고: 정적으로 알려진 슬라이스 길이로 동적 배열을 슬라이싱하는 것도 정적 배열로의 변환을 허용합니다.
어떤 ArrayMemberInitialization이 ValueSeq이면, 그 ValueSeq의 원소들이 시퀀스 대신에 식으로 삽입됩니다.
GC 할당 (GC Allocation)
탈출하는 배열 리터럴은 항상 메모리 관리 힙(GC 힙)에 할당됩니다. 따라서 함수에서 안전하게 반환될 수 있어요.
int[] foo()
{
return [1, 2, 3];
}
배열 리터럴은 다음 경우에는 GC 할당되지 않아요.
- 정적 배열을 초기화하거나 대입할 때.
scope함수 매개변수의 인자일 때.scope슬라이스를 초기화할 때(-preview=dip1000필요).- EqualExpression 또는 RelExpression의 한쪽에서 사용될 때.
- 즉시 인덱싱되어 rvalue로 사용될 때.
- 원소 변수가
ref가 아닌foreach집계로 사용될 때.
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]
다시 말해, 배열 리터럴을 캐스팅하면 각 초기화 원소의 타입이 바뀝니다.
모범 사례: 원소들이 기대되는 타입으로 암묵적으로 변환될 수 있을 때 배열 리터럴을 캐스팅하는 것은 피하세요. 대신 그 타입의 변수를 선언하고 배열 리터럴로 초기화하세요. 캐스팅은 암묵적 변환보다 버그가 나기 쉽습니다.
연관 배열 리터럴 (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를 식 안에 직접 포함시킬 수 있게 해 줍니다. 짧은 함수 리터럴을 **람다(lambda)**라고 부릅니다.
- BasicTypeWithSuffixes는 함수나 delegate의 반환 타입이다 — 생략하면 본문에서 추론된다.
- ParameterWithAttributes 또는 ParameterWithMemberAttributes로 함수의 매개변수를 지정할 수 있다. 이것들이 생략되면 함수는 빈 매개변수 목록
()이 기본값이다. - 매개변수 타입은 생략될 수 있다. 추론되거나, 아니면 리터럴이 템플릿이 된다. Parameter가 BasicType으로 Declarator 없는 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);
}
참고: 함수 리터럴을 중첩 함수와 비교할 때, function 형식은 static 또는 비-중첩 함수와 유사하고, delegate 형식은 비-정적 중첩 함수와 유사해요. 즉, delegate 리터럴은 둘러싼 함수의 비-정적 지역 변수에 접근할 수 있지만, function 리터럴은 그럴 수 없습니다.
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
함수 리터럴 템플릿 매개변수는 unpacking을 사용할 수 있어요.
반환 타입 추론 (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 함수 본문이 있을 때 Parameters는 함수 리터럴에서 완전히 생략될 수 있어요.
참고: 이 형식은 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; };
모범 사례: 함수 리터럴의 최소 형식은 템플릿 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
참고: Usual Arithmetic Conversions.
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예요.
미정의 동작: Invalid State에 들어가면 프로그램의 계속 실행 동작은 정의되지 않는다.
구현 정의: 첫 번째 AssertExpression이 (런타임에) 평가되는지 여부는 보통 컴파일러 스위치로 설정된다. 평가되지 않으면 AssertExpression이 지정한 부수 효과는 일어나지 않을 수 있다. 첫 번째 AssertExpression이
false로 평가될 때의 동작도 보통 컴파일러 스위치로 설정되며, 다음 옵션을 포함할 수 있다.
- 특수 CPU 명령 실행으로 즉시 중단
- 프로그램 중단(abort)
- 해당 C 런타임 라이브러리에서 assert 실패 함수 호출
- D 런타임 라이브러리에서
AssertError예외 던지기
참고: AssertError를 던지는 것이 dmd의 기본값이며, 선택적 -checkaction=context 스위치로 첫 번째 AssertExpression에 사용된 특정 부분식을 오류 메시지에 보여줄 수 있습니다.
auto x = 4;
assert(x < 3);
사용 시, 위는 4 >= 3이라는 메시지와 함께 AssertError를 던집니다.
모범 사례:
- 후속 코드가 의존하는 부수 효과를 두 AssignExpression 어느 쪽에도 넣지 말 것.
- AssertExpression은 프로그램의 버그를 찾기 위한 것입니다. 입력이나 환경 오류를 찾는 데 쓰지 말 것.
- Assert Failure 후 정상 실행을 재개하려 들지 말 것.
컴파일 타임 평가 (Compile-time Evaluation)
첫 번째 AssignExpression이 전적으로 컴파일 타임 상수로 이루어지고 false로 평가된다면, 그것은 특수한 경우예요 — 후속 문장들이 도달할 수 없는 코드(unreachable)임을 나타냅니다. 그 평가를 위해 CTFE(Compile Time Function Execution) 호출은 시도되지 않아요. 그런 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 명령이나 그에 준하는 것을 생성할 수 있습니다.
근거: 프로그램을 중단하면 미정의 동작의 발생을 막을 수 있다.
참고: static assert.
Assert 메시지 (Assert Message)
두 번째 AssignExpression이 있으면 const(char)[] 타입으로 암묵적으로 변환되어야 해요. 있을 때, 구현은 그것을 평가하고 assert 실패 시 그 결과 메시지를 출력할 수 있습니다.
void main()
{
assert(0, "an" ~ " error message");
}
컴파일되어 실행되면, 보통 다음 메시지를 만듭니다.
[email protected](3) an error message
Mixin 식 (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진수 문자열 리터럴이 됩니다.
구현은 디렉터리 탐색(directory traversal) 보안 취약점을 피하기 위해 파일 이름을 제한할 수 있어요. 가능한 제한은 파일 이름에 어떤 경로 구성 요소도 허용하지 않는 것일 수 있습니다.
참고로, 기본적으로 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가 참조 타입(예: 클래스, 연관 배열)일 때TT가 값 타입(예: 기본 타입, 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로 정적 배열을 직접 할당하는 것은 불가능합니다(타입 alias를 통해서만).
결과는 다른 한정자로 암묵적으로 변환될 수 있는 고유(unique) 식이에요.
immutable o = new Object;
클래스 인스턴스화 (Class Instantiation)
NewExpression이 scope 저장 클래스가 있는 함수 지역 변수의 초기화자로 클래스 타입과 함께 사용되면, 그 인스턴스는 스택에 할당됩니다.
new는 중첩 클래스를 할당하는 데도 쓰일 수 있어요.
다차원 배열 (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의 Type은 만들어지는 객체의 Type과 같을 필요는 없습니다.
모범 사례: PlacementExpression에는 void의 정적 배열을 쓰는 것이 선호됩니다.
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);
}
제약:
- PlacementExpression 타입은 수정 가능하고
shared가 아니어야 한다. - Type은 연관 배열일 수 없다. 연관 배열은 GC 힙에 있도록 설계되었기 때문이다. 할당된 연관 배열의 크기는 런타임 라이브러리에서 결정되며 사용자가 설정할 수 없다.
- Placement 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은 컴파일 타임에 평가되고, 어떤 심볼/타입이 유효한 타입인지 검사하는 데 사용됩니다. 또한 다음을 할 수 있는 형식도 있습니다.
- 타입들의 동등(equivalence) 비교
- 한 타입이 다른 타입으로 암묵적으로 변환될 수 있는지 판별
- 어떤 타입이 특정한 종류인지 검사
static if 조건으로 사용될 때 IsExpression은 또한:
- Type 또는 TypeSpecialization 패턴에 대한 타입 alias를 정의
- TypeSpecialization 키워드에 기반한 새 심볼 정의
- 매개변수 목록 패턴 매칭을 사용해:
- 파생 데이터 타입에서 사용된 타입들을 추론
- 타입 템플릿 인스턴스의 템플릿과 템플릿 인자를 추론
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
다른 형식들과 달리 module과 package 형식은 Type이 타입이 아니라 심볼일 때 충족됩니다. 대신 isModule·isPackage __traits를 사용해야 해요. Package 모듈은 패키지와 모듈 둘 다로 간주됩니다.
TypeSpecialization은 다음 키워드 중 하나일 수도 있습니다.
| 키워드 | 조건 |
|---|---|
super |
Type이 클래스 또는 인터페이스이면 true |
return |
Type이 함수, delegate 또는 함수 포인터이면 true |
__parameters |
Type이 함수, delegate 또는 함수 포인터이면 true |
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는 다음과 같이 설정됩니다.
| 키워드 | Identifier를 위한 alias 타입 |
|---|---|
struct |
Type |
union |
Type |
class |
Type |
interface |
Type |
super |
기본 클래스와 인터페이스의 TypeSeq |
enum |
enum의 기본 타입 |
__vector |
벡터의 정적 배열 타입 |
function |
함수 매개변수 타입의 TypeSeq. C·D 스타일 가변 인자 함수에 대해 비-가변 인자 매개변수만 포함된다. 타입 안전 가변 인자 함수에 대해 ...은 무시된다. |
delegate |
delegate의 함수 타입 |
return |
함수, delegate 또는 함수 포인터의 반환 타입 |
__parameters |
함수, delegate 또는 함수 포인터의 매개변수 시퀀스. 매개변수 타입, 이름, 기본값을 포함한다. |
const |
Type |
immutable |
Type |
inout |
Type |
shared |
Type |
module |
모듈 |
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는 특수화를 가질 수 있습니다.
타입 템플릿 인스턴스화 매칭
- TemplateAliasParameter는 템플릿 심볼을 매칭하는 데 사용해야 한다.
- TemplateSequenceParameter는 0개 이상의 TemplateArgument를 매칭하는 데 사용된다.
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));
파생 데이터 타입 매칭
예: 연관 배열 매칭
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));
예: 정적 배열 매칭
E는 배열 원소 타입으로 선언된다- TemplateValueParameter는 배열 길이를 매칭하는 데 사용된다
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은 이동 생성자(move constructor)와 이동 대입(move assignment)의 사용을 가능하게 합니다.
__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__은 인스턴스화 지점의 함수의 정규화된 이름(fully qualified name)으로 확장됩니다.
__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'
경고: 현재 함수의 심볼을 얻기 위해 mixin(__FUNCTION__)을 쓰지 마세요. 이것은 프로그래머들이 현재 함수의 심볼을 얻어 자기 성찰(introspection)을 하려 할 때 흔히 시도하는 것처럼 보입니다. D는 현재 그 심볼을 얻는 직접적인 방법이 없기 때문이죠. 그러나 mixin(__FUNCTION__)을 쓰는 것은 함수의 심볼이 이름으로 조회된다는 뜻이고, 이는 심볼 조회에 따르는 여러 규칙에 종속되어 다양한 문제를 일으킬 수 있어요. 그런 문제 중 하나는 함수가 오버로드되어 있을 때, 그 결과가 현재 함수인지 여부와 무관하게 첫 번째 오버로드가 된다는 점입니다.
D에 현재 함수의 심볼을 직접 얻는 방법이 아직 없다는 점을 감안할 때, 가장 좋은 방법은 함수 안의 어떤 심볼의 부모 심볼을 얻는 것입니다. 이것은 심볼 조회 규칙을 둘러싼 어떤 문제도 피하게 해 주거든요. 다른 심볼에 의존하지 않는 그러한 예는 __traits(parent {})입니다. 이것은 익명의 중첩 함수를 선언하고, 그 부모는 곧 현재 함수가 됩니다. 그래서 그것의 부모를 얻으면 현재 함수의 심볼을 얻게 됩니다.
결합성과 교환성 (Associativity and Commutativity)
구현은 산술 결합성(associativity)과 교환성(commutativity) 규칙에 따라 식의 평가를 재배열할 수 있어요. 단, 그 실행 스레드 안에서 관찰 가능한 차이가 없을 때만 가능합니다.
이 규칙은 부동소수점 식의 어떤 결합적·교환적 재배열도 배제합니다.
더 알아보기 (Learn more)
이 장에서 다룬 것은 식 자체의 문법과 의미예요. 조금 더 깊게 파고 싶다면 공식 사양의 다음 주제들이 자연스럽게 이어집니다.
- 연산자 오버로딩 —
opCmp,opEquals,opBinaryRight, 사용자 정의 대입 연산자를 어떻게 정의하는지는operatoroverloading.html챕터에서 자세히 다뤄요. - 배열과 연관 배열 — 저 배열 리터럴·연관 배열 리터럴이 실제 메모리에서 어떻게 동작하는지는
arrays.html과hash-map.html에서 설명합니다. - 구조체와 클래스 —
opAssign, Postblit, 소멸자, 생성자 위임 같은 개념이struct.html과class.html에 정리돼 있어요. - 템플릿 —
is식의 템플릿 패턴 매칭을 제대로 쓰려면template.html의 IFTI와 매개변수 특수화를 함께 보는 게 좋아요. - 속성과 한정자 —
@safe,const·immutable·inout·shared같은 타입 한정자가attribute.html과const3.html에 자세히 나와 있습니다. - 예외 처리 —
throw식과try/catch는exception-safe.html챕터에서 예외 안전성과 함께 다룹니다.