연산자 오버로딩

연산자 오버로딩 (Operator Overloading)

D에서 클래스나 구조체 인스턴스를 피연산자로 가진 연산자를 어떻게 오버로딩하는지 다루는 페이지입니다. 단항·이항 연산자, 캐스트, 비교, 함수 호출, 할당, 인덱싱·슬라이싱, 전달(opDispatch)까지 D의 연산자 재작성 규칙을 정리했어요.

출처: Operator Overloading — D Language Specification

본문

연산자 오버로딩은 피연산자가 클래스나 구조체 객체인 연산자를 특별히 이름 붙여진 멤버에 대한 호출로 재작성함으로써 달성됩니다. 추가 문법은 사용되지 않습니다.

단항 연산자 오버로딩 (Unary Operator Overloading)

연산자 재작성
-e e.opUnary!("-")()
+e e.opUnary!("+")()
~e e.opUnary!("~")()
*e e.opUnary!("*")()
++e e.opUnary!("++")()
--e e.opUnary!("--")()

예를 들어, 구조체 S에 대해 -(부정) 연산자만 오버로딩하려면:

struct S
{
 int m;

 int opUnary(string s)() if (s == "-")
 {
 return -m;
 }
}

void main()
{
 S s = {2};
 assert(-s == -2);
}

참고: 위 opUnary는 템플릿 매개변수 특수화를 사용해서도 선언할 수 있습니다:

 int opUnary(string s : "-")()

후위 증가 e++와 후위 감소 e-- 연산자 (Postincrement e++ and Postdecrement e-- Operators)

이들은 직접 오버로딩할 수 없지만, ++e--e 전위 연산자의 관점에서 재작성됩니다:

후위 연산자 재작성
e-- (auto t = e, e.opUnary!"--", t)
e++ (auto t = e, e.opUnary!"++", t)

인덱스 단항 연산자 오버로딩 (Overloading Index Unary Operators)

인덱싱을 오버로딩할 수 있습니다. 인덱스 표현식에 대한 단항 연산도 독립적으로 오버로딩할 수 있습니다. 이는 다차원 인덱싱에서도 동작합니다.

연산자 재작성
-a[b1, b2, ... bn] a.opIndexUnary!("-")(b1, b2, ... bn)
+a[b1, b2, ... bn] a.opIndexUnary!("+")(b1, b2, ... bn)
~a[b1, b2, ... bn] a.opIndexUnary!("~")(b1, b2, ... bn)
*a[b1, b2, ... bn] a.opIndexUnary!("*")(b1, b2, ... bn)
++a[b1, b2, ... bn] a.opIndexUnary!("++")(b1, b2, ... bn)
--a[b1, b2, ... bn] a.opIndexUnary!("--")(b1, b2, ... bn)
struct S
{
 private int[] a;

 void opIndexUnary(string s: "++")(size_t i) { ++a[i]; }
}

S s = {[4]};
++s[0];
assert(s.a[0] == 5);

슬라이스 단항 연산자 오버로딩 (Overloading Slice Unary Operators)

슬라이싱을 오버로딩할 수 있습니다. 슬라이스에 대한 단항 연산도 독립적으로 오버로딩할 수 있습니다. opIndexUnary는 전체 슬라이스에 대해 함수 인자 없이, 또는 슬라이스의 시작과 끝 인덱스에 대해 두 인자로 정의됩니다.

연산자 재작성
-a[i..j] a.opIndexUnary!("-")(a.opSlice(i, j))
+a[i..j] a.opIndexUnary!("+")(a.opSlice(i, j))
~a[i..j] a.opIndexUnary!("~")(a.opSlice(i, j))
*a[i..j] a.opIndexUnary!("*")(a.opSlice(i, j))
++a[i..j] a.opIndexUnary!("++")(a.opSlice(i, j))
--a[i..j] a.opIndexUnary!("--")(a.opSlice(i, j))
-a[ ] a.opIndexUnary!("-")()
+a[ ] a.opIndexUnary!("+")()
~a[ ] a.opIndexUnary!("~")()
*a[ ] a.opIndexUnary!("*")()
++a[ ] a.opIndexUnary!("++")()
--a[ ] a.opIndexUnary!("--")()
struct S
{
 private int[] a;

 void opIndexUnary(string s: "--")() { --a[]; }
}

S s = {[1, 2]};
--s[];
assert(s.a == [0, 1]);

레거시 (Legacy): 하위 호환을 위해, 위 재작성이 컴파일되지 않고 opSliceUnary가 정의되어 있다면 각각 a.opSliceUnary!(op)(i, j)a.opSliceUnary!(op) 재작성이 대신 시도됩니다.

캐스트 연산자 오버로딩 (Cast Operator Overloading)

한 타입이 다른 타입으로 캐스트되는 방법을 정의하려면 opCast 템플릿 메서드를 정의합니다:

연산자 재작성
cast(type) e e.opCast!(type)()

opCast는 불리언 연산(다음 섹션 참고)의 경우를 제외하고 항상 명시적 캐스트 표현식에서만 사용됩니다.

struct S
{
 void* mem;

 bool opCast(T)()
 if (is(T == bool)) => mem !is null;
}

S s = S(new int);
auto b = cast(bool) s;
assert(b);
//b = s; // error

opCast의 반환 타입이 캐스트의 타입 매개변수와 다르면, 결과는 타입으로 암시적으로 변환됩니다.

불리언 연산 (Boolean Operations)

오버로딩된 단항 연산자 목록에서 눈에 띄게 빠진 것은 ! 논리 부정 연산자입니다. 더 난해하게 빠진 것은 bool 결과로 변환하는 단항 연산자입니다. 대신 구조체의 경우 이것들은 다음으로 재작성해 다룹니다:

opCast!(bool)(e)

그래서,

if (e) => if (e.opCast!(bool))
if (!e) => if (!e.opCast!(bool))

그리고 구조체 인스턴스에 사용되는 다른 불리언 조건 표현식과 논리 연산자에 대해서도 유사합니다.

그러나 이것은 구조체 인스턴스에 대해서만 일어납니다. 클래스 참조는 클래스 참조가 null인지 아닌지 확인해 bool로 변환됩니다.

이항 연산자 오버로딩 (Binary Operator Overloading)

다음 이항 연산자를 오버로딩할 수 있습니다:

+ - * / % ^^ & | ^ << >> >>> ~ in

표현식:

a op b

은 다음 중 하나로 재작성됩니다:

a.opBinary!("op")(b)
b.opBinaryRight!("op")(a)

그리고 '더 나은' 일치를 가진 것이 선택됩니다. 둘 다 동일하게 일치하면 오류입니다. 예:

struct S
{
 int[] data;

 // this ~ rhs
 int[] opBinary(string op : "~")(int rhs)
 {
 return data ~ rhs;
 }
 // lhs ~ this
 int[] opBinaryRight(string op : "~")(int lhs)
 {
 return lhs ~ data;
 }
}

void main()
{
 auto s = S([2,3]);
 assert(s ~ 4 == [2,3,4]); // opBinary
 assert(1 ~ s == [1,2,3]); // opBinaryRight
}

여러 연산자에 대한 연산자 오버로딩을 한 번에 할 수 있습니다. 예를 들어 + 또는 - 연산자만 지원한다면:

T opBinary(string op)(T rhs)
{
 static if (op == "+") return data + rhs.data;
 else static if (op == "-") return data - rhs.data;
 else static assert(0, "Operator " ~ op ~ " not implemented");
}

모두 한꺼번에 하려면:

T opBinary(string op)(T rhs)
{
 return mixin("data " ~ op ~ " rhs.data");
}

opInopIn_r은 각각 opBinary!"in"opBinaryRight!"in"을 위해 폐기(deprecated)되었습니다.

비교 연산자 오버로딩 (Overloading the Comparison Operators)

D는 비교 연산자 ==, !=, <, <=, >=, >를 두 함수 opEqualsopCmp를 통해 오버로딩할 수 있게 합니다.

동등성과 부등성 연산자는 비교 연산자와 별개로 취급됩니다. 실질적으로 모든 사용자 정의 타입이 동등성 비교에 참여할 수 있는 반면, 오직 타입의 부분집합만이 의미 있는 순서를 가지기 때문입니다. 예를 들어 두 RGB 색 벡터가 같은지 판별하는 것은 의미가 있지만, 한 색이 다른 색보다 크다고 말하는 것은 의미가 없습니다. 색에는 순서가 없기 때문입니다. 따라서 Color 타입에 대해 opEquals는 정의하되 opCmp는 정의하지 않을 것입니다.

게다가 순서를 가진 타입에서도 순서 관계가 선형이 아닐 수 있습니다. 예를 들어 부분집합(subset) 관계로 집합에 순서를 정의할 수 있습니다. 이때 x < yxy의 (엄격한) 부분집합이면 참입니다. xy가 분리된 집합이면 x < yy < x도 성립하지 않지만, 그것이 x == y를 뜻하지는 않습니다. 따라서 opCmp만으로 동등성을 판별하는 것은 불충분합니다. 이런 이유로 opCmpRelExpression 연산자 <, <=, >=, >에만 사용됩니다. 동등성 연산자 ==!=는 항상 opEquals를 사용합니다.

따라서 opCmpopEquals가 서로 일관되게 하도록 보장하는 것은 프로그래머의 책임입니다. 구조체에 opEquals가 지정되지 않으면 컴파일러는 멤버별(member-wise) 비교를 하는 기본 버전을 제공합니다. 이것으로 충분하다면, RelExpression 연산자의 동작을 사용자 정의하기 위해 opCmp만 정의할 수도 있습니다. 하지만 그렇지 않다면, 두 종류의 비교 연산자 사이에 일관된 의미론을 보존하기 위해 opEquals의 사용자 정의 버전도 정의해야 합니다.

마지막으로, 사용자 정의 타입을 내장 연관 배열(associative array)의 키로 사용하려면, 프로그래머는 opEqualstoHash의 의미론이 일관되게 하도록 보장해야 합니다. 그렇지 않으면 연관 배열이 기대한 대로 동작하지 않을 수 있습니다.

==와 != 오버로딩 (Overloading == and !=)

a != b 형태의 표현식은 !(a == b)로 재작성됩니다.

a == b가 주어졌을 때:

  • ab가 둘 다 클래스 객체이면, 표현식은 다음과 같이 재작성됩니다:
.object.opEquals(a, b)

그 함수는 다음과 유사합니다:

bool opEquals(Object a, Object b)
{
 if (a is b) return true;
 if (a is null || b is null) return false;
 if (typeid(a) == typeid(b)) return a.opEquals(b);
 return a.opEquals(b) && b.opEquals(a);
}
  • 그 외에는 표현식 a.opEquals(b)b.opEquals(a)가 시도됩니다. 둘 다 같은 opEquals 함수로 해석되면, 표현식은 a.opEquals(b)로 재작성됩니다.
  • 하나가 다른 것보다 더 나은 일치이거나, 하나는 컴파일되고 다른 하나는 컴파일되지 않으면, 첫 번째가 선택됩니다.
  • 그 외에는 오류가 발생합니다.

클래스에 대해 Object.opEquals()를 오버라이드할 때, 클래스 멤버 함수는 Object 매개변수를 받고 그것이 호환 가능한 클래스인지 동적으로 검사해야 합니다. 예:

class C
{
 int i;

 this(int i) { this.i = i; }

 override bool opEquals(Object o)
 {
 if (auto c = cast(C) o)
 return c.i == i;
 else
 assert(0, __FUNCTION__ ~ ": Cannot compare a " ~ typeid(o).toString);
 }
}

static assert(new C(2) == new C(2));
static assert(new C(2) != new C(3));

구조체가 동등성 비교를 위해 opEquals 멤버 함수를 선언하면, 여러 형태를 가질 수 있습니다. 예:

struct S
{
 // lhs should be mutable object
 bool opEquals(const S s) { ... } // for r-values (e.g. temporaries)
 bool opEquals(ref const S s) { ... } // for l-values (e.g. variables)

 // both hand side can be const object
 bool opEquals(const S s) const { ... } // for r-values (e.g. temporaries)
}

또는 auto ref 매개변수를 가진 단일 템플릿 opEquals 함수를 선언합니다:

struct S
{
 // for l-values and r-values,
 // with converting both hand side implicitly to const
 bool opEquals()(auto ref const S s) const { ... }
}

참고: 구조체에 대한 opEquals 구현의 모범 사례는 Struct Equality를 참고하세요.

<, <=, >, >= 오버로딩 (Overloading <, <=, >, and >=)

클래스 참조는 먼저 참조로 비교됩니다. 서로 다른 객체를 가리키고 둘 다 null이 아니면, 구조체에서처럼 일치하는 opCmp 메서드를 호출해 비교합니다.

구조체 비교 연산은 다음과 같이 재작성됩니다:

비교 재작성 1 재작성 2
a < b a.opCmp(b) < 0 b.opCmp(a) > 0
a <= b a.opCmp(b) <= 0 b.opCmp(a) >= 0
a > b a.opCmp(b) > 0 b.opCmp(a) < 0
a >= b a.opCmp(b) >= 0 b.opCmp(a) <= 0

두 재작성이 시도됩니다. 하나만 컴파일되면 그게 취해집니다. 둘 다 같은 함수로 해석되면 첫 번째 재작성이 수행됩니다. 서로 다른 함수로 해석되면 가장 잘 일치하는 것이 사용됩니다. 둘 다 같은 정도로 일치하는데 서로 다른 함수라면 모호성 오류가 발생합니다.

struct B
{
 int opCmp(int)         { return -1; }
 int opCmp(ref const S) { return -1; }
 int opCmp(ref const C) { return -1; }
}

struct S
{
 int opCmp(ref const S) { return 1; }
 int opCmp(ref B)       { return 0; }
}

struct C
{
 int opCmp(ref const B) { return 0; }
}

void main()
{
 S s;
 const S cs;
 B b;
 C c;
 assert(s > s);      // s.opCmp(s) > 0
 assert(!(s < b));   // s.opCmp(b) > 0  - S.opCmp(ref B) is exact match
 assert(!(b < s));   // s.opCmp(b) < 0  - S.opCmp(ref B) is exact match
 assert(b < cs);     // b.opCmp(s) < 0  - B.opCmp(ref const S) is  exact match
 static assert(!__traits(compiles, b < c)); // both C.opCmp and B.opcmp match exactly
}

클래스에 대해 Object.opCmp()을 오버라이드할 때, 클래스 멤버 함수는 Object 매개변수를 받고 그것이 비교에 호환 가능한 클래스인지 동적으로 검사해야 합니다(opEquals를 오버라이드할 때처럼).

class C
{
 override int opCmp(Object o) { ... }
}

구조체가 opCmp 멤버 함수를 선언하면, 다음 형태를 가져야 합니다:

struct S
{
 int opCmp(ref const S s) const { ... }
}

opCmpRelExpression 연산자에만 사용된다는 점에 유의하세요. a == b 같은 표현식은 항상 opEquals를 사용합니다.

구조체에 대해 opCmp가 정의되었는데 opEquals가 없다면, 컴파일러는 멤버별 비교를 수행하는 opEquals의 기본 버전을 공급합니다. 그러나 멤버별 비교가 사용자 정의 opCmp와 일관되지 않을 수 있습니다.

적절할 때 opEquals 버전도 공급하는 것은 프로그래머에게 달려 있습니다. 그렇지 않으면 a <= b 같은 RelExpressiona == b 같은 동등성과 일관되지 않게 동작합니다.

struct S
{
 int i, j;
 int opCmp(ref const S s) const { return (i > s.i) - (i < s.i); } // ignore j
}

S a = {2, 3};
S b = {2, 1};
S c = {3, 0};
assert(a < c);
assert(a <= b);
assert(!(a < b)); // opCmp ignores j
assert(a != b);   // generated opEquals tests both i and j members

모범 사례 (Best Practices): 정수를 비교할 때 i - s.i 대신 (i > s.i) - (i < s.i)를 사용하면 오버플로를 피합니다.

함수 호출 연산자 오버로딩 (Function Call Operator Overloading)

함수 호출 연산자 ()opCall이라는 함수를 선언해 오버로딩할 수 있습니다:

struct F
{
 int opCall();
 int opCall(int x, int y, int z);
}

void main()
{
 F f;
 int i;

 i = f(); // same as i = f.opCall();
 i = f(3,4,5); // same as i = f.opCall(3,4,5);
}

이런 식으로 구조체나 클래스 객체가 함수처럼 동작할 수 있습니다.

struct Multiplier
{
 int factor;
 this(int num) { factor = num; }
 int opCall(int value) { return value * factor; }
}

void main()
{
 Multiplier m = Multiplier(10); // invoke constructor
 assert(m.factor == 10);
 int result = m(5); // invoke opCall
 assert(result == 50);
}

static opCall

static opCall도 타입 이름과 함께 함수 호출 연산자로 기대한 대로 동작합니다.

struct Double
{
 static int opCall(int x) { return x * 2; }
}

void main()
{
 int i = Double(2);
 assert(i == 4);
}

단순히 static opCall을 선언하면 구조체 리터럴 문법이 자동으로 비활성화된다는 점에 유의하세요.

구조체 생성자와 static opCall을 혼합하는 것은 허용되지 않습니다.

struct S
{
 this(int i) {}
 static S opCall() // disallowed due to constructor
 {
 return S.init;
 }
}

참고: static opCall은 인자 없는 구조체 생성자를 흉내 내는 데 사용될 수 있지만 권장되는 방법은 아닙니다. 대신 선호되는 해결책은 팩토리 함수를 사용해 구조체 인스턴스를 만드는 것입니다.

할당 연산자 오버로딩 (Assignment Operator Overloading)

왼쪽이 집계(aggregate)이고 opAssign이 그 집계의 멤버 함수이면 할당 연산자 =를 오버로딩할 수 있습니다.

구조체 타입의 경우 동일성 할당(identity assignment)과 다른 타입으로부터의 할당 모두에 대한 연산자 오버로딩이 허용됩니다.

struct S
{
 // identity assignment, allowed.
 void opAssign(S rhs);

 // not identity assignment, also allowed.
 void opAssign(int);
}
S s;
s = S(); // Rewritten to s.opAssign(S());
s = 1; // Rewritten to s.opAssign(1);

그러나 클래스 타입의 경우, 할당은 typeof(this)로 변환되지 않는 타입에서만 오버라이드할 수 있습니다.

이유 (Rationale): 모든 클래스 타입은 참조 의미론을 가지므로, 동일성 할당은 왼쪽 클래스 참조를 오른쪽 참조에 다시 바인딩합니다. 이것이 오버라이드 가능하다면 서브클래스가 기반 클래스로 암시적으로 변환되는 것과 개념적으로 충돌할 것입니다.

class C
{
 // If X is the same type as C or a type which is
 // implicitly convertible to C, then opAssign would
 // accept identity assignment, which is disallowed.
 // C opAssign(...);
 // C opAssign(X);
 // C opAssign(X, ...);
 // C opAssign(X ...);
 // C opAssign(X, U = defaultValue, etc.);

 // not an identity assignment - allowed
 void opAssign(int);
}
C c = new C();
c = new C(); // Rebinding reference
c = 1; // Rewritten to c.opAssign(1);

인덱스 할당 연산자 오버로딩 (Index Assignment Operator Overloading)

할당의 왼쪽이 구조체나 클래스 인스턴스의 인덱스 연산이면, opIndexAssign 멤버 함수를 제공해 오버로딩할 수 있습니다.

재작성
a[b1, b2, ... bn] = c a.opIndexAssign(c, b1, b2, ... bn)
struct A
{
 int opIndexAssign(int value, size_t i1, size_t i2);
}

void test()
{
 A a;
 size_t i;
 a[i,3] = 7; // same as a.opIndexAssign(7,i,3);
}

슬라이스 할당 연산자 오버로딩 (Slice Assignment Operator Overloading)

할당의 왼쪽이 구조체나 클래스 인스턴스의 슬라이스 연산이면, opSlice 함수의 반환 값을 매개변수로 받는 opIndexAssign 멤버 함수를 구현해 오버로딩할 수 있습니다.

재작성
a[] = c a.opIndexAssign(c)
a[i..j] = c a.opIndexAssign(c, a.opSlice!0(i, j))

자세한 내용은 Slice Operator Overloading를 참고하세요.

struct A
{
 int opIndexAssign(int v); // overloads a[] = v
 int opIndexAssign(int v, size_t[2] slice); // overloads a[i .. j] = v
 size_t[2] opSlice(size_t dim)(size_t i, size_t j); // overloads i .. j
}

void test()
{
 A a;
 int v;

 a[] = v; // same as a.opIndexAssign(v);
 a[3..4] = v; // same as a.opIndexAssign(v, a.opSlice!0(3,4));
}

레거시 (Legacy): 하위 호환을 위해, a[i..j]a.opIndexAssign(a.opSlice!0(i, j))로 재작성하는 것이 컴파일되지 않으면, 레거시 재작성 opSliceAssign(c, i, j)가 대신 사용됩니다.

op= 할당 연산자 오버로딩 (Op Assignment Operator Overloading)

다음 op 할당 연산자를 오버로딩할 수 있습니다:

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

표현식:

a op= b

은 다음과 같이 재작성됩니다:

a.opOpAssign!("op")(b)

예:

struct S
{
 int i;
 void opOpAssign(string op: "+")(int rhs) { i += rhs; }
}

S s = {2};
s += 3;
assert(s.i == 5);

인덱스 op= 할당 연산자 오버로딩 (Index Op Assignment Operator Overloading)

op=의 왼쪽이 구조체나 클래스 인스턴스의 인덱스 표현식이고 opIndexOpAssign이 멤버이면:

a[b1, b2, ... bn] op= c

은 다음과 같이 재작성됩니다:

a.opIndexOpAssign!("op")(c, b1, b2, ... bn)

슬라이스 op= 할당 연산자 오버로딩 (Slice Op Assignment Operator Overloading)

op=의 왼쪽이 구조체나 클래스 인스턴스의 슬라이스 표현식이고 opIndexOpAssign이 멤버이면:

a[i..j] op= c

은 다음과 같이 재작성됩니다:

a.opIndexOpAssign!("op")(c, a.opSlice(i, j))

그리고

a[] op= c

은 다음과 같이 재작성됩니다:

a.opIndexOpAssign!("op")(c)

레거시 (Legacy): 하위 호환을 위해, 위 재작성이 실패하고 opSliceOpAssign이 정의되어 있으면, 각각 a.opSliceOpAssign(c, i, j)a.opSliceOpAssign(c) 재작성이 시도됩니다.

배열 인덱싱과 슬라이싱 연산자 오버로딩 (Array Indexing and Slicing Operators Overloading)

배열 인덱싱과 슬라이싱 연산자는 opIndex, opSlice, opDollar 메서드를 구현해 오버로딩합니다. 이들은 결합되어 다차원 배열을 구현할 수 있습니다.

인덱스 연산자 오버로딩 (Index Operator Overloading)

arr[b1, b2, ... bn] 형태의 표현식은 arr.opIndex(b1, b2, ... bn)으로 변환됩니다. 예:

struct A
{
 int opIndex(size_t i1, size_t i2, size_t i3);
}

void test()
{
 A a;
 int i;
 i = a[5,6,7]; // same as i = a.opIndex(5,6,7);
}

이런 식으로 구조체나 클래스 객체가 배열처럼 동작할 수 있습니다.

인덱스 표현식이 opIndexAssign이나 opIndexOpAssign으로 재작성될 수 있으면, 그것들이 opIndex보다 선호됩니다.

슬라이스 연산자 오버로딩 (Slice Operator Overloading)

슬라이싱 연산자를 오버로딩한다는 것은 a[]a[i..j] 같은 표현식을 오버로딩하는 것을 뜻합니다. 여기서 대괄호 안의 표현식은 i..j 형태의 슬라이스 표현식을 포함합니다.

a[]를 오버로딩하려면 매개변수가 없는 opIndex를 정의하면 됩니다:

재작성
a[] a.opIndex()
struct S
{
 int[] impl;
 int[] opIndex()
 {
 return impl[];
 }
}

void main()
{
 auto s = S([1,2,3]);
 int[] t = s[]; // calls s.opIndex()
 assert(t == [1,2,3]);
}

a[i..j] 형태의 배열 슬라이싱을 오버로딩하려면 두 단계가 필요합니다. 먼저 슬라이스 인자 i..jopSlice 호출에 의해 끝점 ij를 캡슐화하는 객체로 변환됩니다. 그런 다음 이 객체가 실제 슬라이싱을 수행하기 위해 opIndex에 전달됩니다:

재작성
a[i..j] a.opIndex(a.opSlice(i, j))

아래에서 opSliceint[]를 반환하므로, opIndex가 이 타입을 받아야 합니다:

struct S
{
 int[] impl;

 int[] opSlice(size_t i, size_t j)
 {
 return impl[i..j];
 }
 int[] opIndex()(int[] slice) { return slice; }
}

void main()
{
 auto s = S([1, 2, 3]);
 int[] t = s[0..2]; // calls s.opIndex(s.opSlice(0, 2))
 assert(t == [1, 2]);
}

이 설계는 다차원 배열에서 혼합 인덱싱과 슬라이싱을 지원하기 위해 선택되었습니다. 예를 들어 arr[1, 2..3, 4] 같은 표현식을 변환하는 것입니다. 더 정확히는 arr[b1, b2, ... bn] 형태의 표현식이 arr.opIndex(c1, c2, ... cn)으로 변환됩니다. 각 인자 bi는 둘 중 하나일 수 있습니다:

  • 단일 표현식 — 이 경우 해당 인자 ci로 직접 opIndex에 전달됩니다.
  • xi..yi 형태의 슬라이스 인자 — 이 경우 opIndex의 해당 인자 ciarr.opSlice!i(xi, yi)입니다. n이 1이면 opSlice는 템플릿 매개변수가 필요하지 않습니다.

예:

재작성
arr[1, 2, 3] arr.opIndex(1, 2, 3)
arr[1..2, 3..4, 5..6] arr.opIndex(arr.opSlice!0(1,2), arr.opSlice!1(3,4), arr.opSlice!2(5,6))
arr[1, 2..3, 4] arr.opIndex(1, arr.opSlice!1(2,3), 4)

슬라이싱이 포함된 할당 연산자에 대해서도 유사한 변환이 수행됩니다. 예:

재작성
arr[1, 2..3, 4] = c arr.opIndexAssign(c, 1, arr.opSlice!1(2, 3), 4)
arr[2, 3..4] += c arr.opIndexOpAssign!"+"(c, 2, arr.opSlice!1(2, 3))

의도는 opSlice!i가 배열의 i번째 차원을 따라 인덱스 구간을 나타내는 사용자 정의 객체를 반환하는 것입니다. 이 객체는 실제 슬라이싱 연산을 수행하기 위해 opIndex에 전달됩니다.

모든 경우에서 arr는 한 번만 평가된다는 점에 유의하세요. 따라서 getArray()[1, 2..3, $-1] = c 같은 표현식은 다음의 효과를 가집니다:

auto __tmp = getArray();
__tmp.opIndexAssign(c, 1, __tmp.opSlice!1(2,3), __tmp.opDollar!2 - 1);

여기서 getArray에 대한 초기 함수 호출은 단 한 번만 실행됩니다.

레거시 (Legacy): 하위 호환을 위해, a[]a[i..j]는 각각 인자 없는 opSlice()와 두 인자를 가진 opSlice(i, j)를 구현해 오버로딩할 수도 있습니다. 이는 1차원 슬라이싱에만 적용되며, D가 다차원 배열을 완전히 지원하기 전의 것입니다. 이런 opSlice 사용은 권장되지 않습니다.

달러 연산자 오버로딩 (Dollar Operator Overloading)

배열 인덱스와 슬라이싱 연산자의 인자 안에서 $opDollar!i로 변환됩니다. 여기서 i$ 표현식이 나타나는 위치입니다. 예:

재작성
arr[$-1, $-2, 3] arr.opIndex(arr.opDollar!0 - 1, arr.opDollar!1 - 2, 3)
arr[1, 2, 3..$] arr.opIndex(1, 2, arr.opSlice!2(3, arr.opDollar!2))

의도는 opDollar!i가 배열의 i번째 차원을 따른 길이를 반환하거나, 그 차원을 따른 배열의 끝을 나타내는 사용자 정의 객체(는 opSliceopIndex가 이해)를 반환하는 것입니다.

struct Rectangle
{
 int width, height;
 int[][] impl;

 this(int w, int h)
 {
 width = w;
 height = h;
 impl = new int[][](w, h);
 }
 int opIndex(size_t i1, size_t i2)
 {
 return impl[i1][i2];
 }
 int opDollar(size_t pos)()
 {
 static if (pos==0)
 return width;
 else
 return height;
 }
}

void main()
{
 auto r = Rectangle(10,20);
 int i = r[$-1, 0]; // same as: r.opIndex(r.opDollar!0, 0),
 // which is r.opIndex(r.width-1, 0)
 int j = r[0, $-1]; // same as: r.opIndex(0, r.opDollar!1)
 // which is r.opIndex(0, r.height-1)
}

위 예가 보여주듯, opDollar에 전달되는 컴파일 타임 인자는 그것이 어느 인자에 나타나느냐에 따라 다릅니다. 첫 인자에 나타나는 $opDollar!0으로, 두 번째 인자에 나타나는 $opDollar!1로 변환됩니다. 그렇게 함으로써 다차원 배열을 구현하기 위한 $의 적절한 값을 반환할 수 있습니다.

opDollar!i$가 인덱싱 연산의 해당 위치에 나타나는 각 i에 대해 한 번만 평가된다는 점에 유의하세요. 따라서 arr[$-sqrt($), 0, $-1] 같은 표현식은 다음의 효과를 가집니다:

auto __tmp1 = arr.opDollar!0;
auto __tmp2 = arr.opDollar!2;
arr.opIndex(__tmp1 - sqrt(__tmp1), 0, __tmp2 - 1);

opIndex가 하나의 인자로만 선언되면, opDollar의 컴파일 타임 인자는 생략될 수 있습니다. 이 경우 두 개 이상의 인자를 가진 배열 인덱싱 표현식 안에서 $를 사용하는 것은 불법입니다.

완전한 예 (Complete Example)

아래 코드 예는 오버로딩된 인덱싱과 슬라이싱 연산자를 가진 2차원 배열의 간단한 구현을 보여줍니다. 사용된 다양한 구문의 설명은 뒤따르는 섹션들에서 제공됩니다.

struct Array2D(E)
{
 E[] impl;
 int stride;
 int width, height;

 this(int width, int height, E[] initialData = [])
 {
 impl = initialData;
 this.stride = this.width = width;
 this.height = height;
 impl.length = width * height;
 }

 // Index a single element, e.g., arr[0, 1]
 ref E opIndex(int i, int j) { return impl[i + stride*j]; }

 // Array slicing, e.g., arr[1..2, 1..2], arr[2, 0..$], arr[0..$, 1].
 Array2D opIndex(int[2] r1, int[2] r2)
 {
 Array2D result;

 auto startOffset = r1[0] + r2[0]*stride;
 auto endOffset = r1[1] + (r2[1] - 1)*stride;
 result.impl = this.impl[startOffset .. endOffset];

 result.stride = this.stride;
 result.width = r1[1] - r1[0];
 result.height = r2[1] - r2[0];

 return result;
 }
 auto opIndex(int[2] r1, int j) { return opIndex(r1, [j, j+1]); }
 auto opIndex(int i, int[2] r2) { return opIndex([i, i+1], r2); }

 // Support for `x..y` notation in slicing operator for the given dimension.
 int[2] opSlice(size_t dim)(int start, int end)
     if (dim >= 0 && dim < 2)
 in { assert(start >= 0 && end <= this.opDollar!dim); }
 do
 {
 return [start, end];
 }

 // Support `$` in slicing notation, e.g., arr[1..$, 0..$-1].
 @property int opDollar(size_t dim : 0)() { return width; }
 @property int opDollar(size_t dim : 1)() { return height; }
}

void main()
{
 auto arr = Array2D!int(4, 3, [
 0, 1, 2,  3,
 4, 5, 6,  7,
 8, 9, 10, 11
 ]);

 // Basic indexing
 assert(arr[0, 0] == 0);
 assert(arr[1, 0] == 1);
 assert(arr[0, 1] == 4);

 // Use of opDollar
 assert(arr[$-1, 0] == 3);
 assert(arr[0, $-1] == 8);   // Note the value of $ differs by dimension
 assert(arr[$-1, $-1] == 11);

 // Slicing
 auto slice1 = arr[1..$, 0..$];
 assert(slice1[0, 0] == 1 && slice1[1, 0] == 2  && slice1[2, 0] == 3 &&
        slice1[0, 1] == 5 && slice1[1, 1] == 6  && slice1[2, 1] == 7 &&
        slice1[0, 2] == 9 && slice1[1, 2] == 10 && slice1[2, 2] == 11);

 auto slice2 = slice1[0..2, 1..$];
 assert(slice2[0, 0] == 5 && slice2[1, 0] == 6 &&
        slice2[0, 1] == 9 && slice2[1, 1] == 10);

 // Thin slices
 auto slice3 = arr[2, 0..$];
 assert(slice3[0, 0] == 2 &&
        slice3[0, 1] == 6 &&
        slice3[0, 2] == 10);

 auto slice4 = arr[0..3, 2];
 assert(slice4[0, 0] == 8 && slice4[1, 0] == 9 && slice4[2, 0] == 10);
}

전달 (Forwarding)

클래스나 구조체에서 찾을 수 없는 멤버 이름은 해석을 위해 opDispatch라는 템플릿 함수로 전달될 수 있습니다.

import std.stdio;

struct S
{
 void opDispatch(string s, T)(T i)
 {
 writefln("S.opDispatch('%s', %s)", s, i);
 }
}

class C
{
 void opDispatch(string s)(int i)
 {
 writefln("C.opDispatch('%s', %s)", s, i);
 }
}

struct D
{
 template opDispatch(string s)
 {
 enum int opDispatch = 8;
 }
}

void main()
{
 S s;
 s.opDispatch!("hello")(7);
 s.foo(7);

 auto c = new C();
 c.foo(8);

 D d;
 writefln("d.foo = %s", d.foo);
 assert(d.foo == 8);
}

D1 스타일 연산자 오버로딩 (D1 style operator overloading)

D1 연산자 오버로딩 메커니즘은 폐기(deprecated)되었습니다.

더 알아보기

  • Functions — 함수와 템플릿 규칙.
  • Templates — 템플릿 매개변수 특수화와 mixin.
  • Structs — 구조체 비교(opEquals, opCmp) 모범 사례.