D와 C++ 연동

D와 C++ 연동 (Interfacing to C++)

이 챕터에서는 D가 C++과 직접 연동하는 방법을 다뤄요. 이름 그대로, C++로 작성된 코드를 D에서 부르고, 반대로 D로 작성된 코드를 C++에서 부를 수 있는 길을 만들어 주는 내용이죠. 외부 언어와 손을 잡는 일은 항상 까다로운데, D가 어떤 타협점을 골랐는지 차근차근 살펴볼게요.

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

본문

이 문서는 C++과 직접 연동하는 방법을 명세합니다.

C++ 코드와 간접적으로 연동하는 것도 가능한데, 이 경우에는 C 인터페이스나 COM 인터페이스를 경유하게 됩니다.

기본 아이디어 (The General Idea)

C++과 100% 호환된다는 것은, 대략적으로 말해 완전히 동작하는 C++ 컴파일러 프론트엔드를 D에 통째로 붙여 넣는 것과 같아요. 경험적으로 볼 때 그런 것을 만드는 데는 최소한 10인년(man-year)의 프로젝트가 필요해서, 실질적으로 그런 능력을 갖춘 D 컴파일러는 만들 수 없다고 봐야 합니다. C++에 붙으려는 다른 언어들도 똑같은 문제를 마주하는데, 그동안 쓰였던 해법은 이러했어요.

  1. COM 인터페이스를 지원한다 (하지만 Windows에서만 동작해요).
  2. C++ 코드 주변에 힘들게 C 래퍼(wrapper)를 만든다.
  3. SWIG 같은 자동화 도구로 C 래퍼를 만든다.
  4. 다른 언어로 C++ 코드를 다시 구현한다.
  5. 포기한다.

D는 여기서 실용적인 접근을 택합니다. 몇 가지 적당한 양보만으로도 문제의 상당 부분을 해결할 수 있다고 보는 거죠. 그 양보가 바로 이 세 가지예요.

  • C++ 이름 맹글링(name mangling) 규칙을 맞춘다
  • C++ 함수 호출 규약(calling convention)을 맞춘다
  • 단일 상속(single inheritance)에 대한 C++ 가상 함수 테이블(vtable) 레이아웃을 맞춘다

플랫폼·컴파일러 지원 (Platform-Compiler Support)

  • Windows에서는 D 코드를 MSVC로 컴파일된 바이너리에 링크하는 것을 권장해요.
  • Linux에서는 GCC와 Clang으로 컴파일된 바이너리 양쪽 모두에 링크하는 것을 지원합니다.
  • MacOS에서는 Clang으로 컴파일된 바이너리에 링크하는 것을 권장해요.

전역 함수 (Global Functions)

네임스페이스(namespace) 안에 있는 것까지 포함해 C++ 전역 함수는 D에서 선언하고 호출할 수 있고, 반대로 D에서 정의해서 C++에서 호출할 수도 있어요.

D에서 C++ 전역 함수 호출하기 (Calling C++ Global Functions from D)

C++ 소스 파일에 다음과 같은 함수가 있다고 해볼게요.

#include <iostream>

using namespace std;

int foo(int i, int j, int k)
{
    cout << "i = " << i << endl;
    cout << "j = " << j << endl;
    cout << "k = " << k << endl;

    return 7;
}

그에 대응하는 D 코드에서는 fooC++ 링키지(linkage)와 C++ 함수 호출 규약을 갖는 것으로 선언해요.

extern (C++) int foo(int i, int j, int k);

그러면 이제 D 코드 안에서 그냥 호출하면 됩니다.

extern (C++) int foo(int i, int j, int k);

void main()
{
    foo(1, 2, 3);
}

두 파일을 컴파일하고 — 첫 번째는 C++ 컴파일러로, 두 번째는 D 컴파일러로 — 서로 링크해서 실행하면 이런 결과가 나와요.

> g++ -c foo.cpp
> dmd bar.d foo.o -L-lstdc++ && ./bar
i = 1
j = 2
k = 3

여기서 여러 가지가 벌어지고 있는데, 핵심만 짚어볼게요.

  • D는 C++ 함수 이름이 어떻게 "맹글링"되는지, 그리고 올바른 C++ 함수 호출/반환 시퀀스가 무엇인지 이해합니다.
  • 모듈(module)은 C++에 없기 때문에, 전역 네임스페이스에 있는 C++ 링키지 함수는 각각 프로그램 전체에서 유일해야 해요.
  • D에는 __cdecl, __far, __stdcall, __declspec 같은 비표준 C++ 확장이 없습니다.
  • D에는 volatile 타입 한정자가 없어요.
  • D의 문자열은 0으로 종료되지 않습니다. 자세한 내용은 "데이터 타입 호환성(Data Type Compatibility)"을 참고하세요. 다만 D의 문자열 리터럴은 0으로 종료돼요.

C++에서 D 전역 함수 호출하기 (Calling Global D Functions From C++)

D 함수를 C++에서 부를 수 있게 하려면 그 함수에 C++ 링키지를 붙여주면 됩니다.

import std.stdio;

extern (C++) int foo(int i, int j, int k)
{
    writefln("i = %s", i);
    writefln("j = %s", j);
    writefln("k = %s", k);
    return 1;
}

extern (C++) void bar();

void main()
{
    bar();
}

C++ 쪽에서는 이렇게 생겼어요.

int foo(int i, int j, int k);

void bar()
{
    foo(6, 7, 8);
}

컴파일·링크·실행하면 이런 출력이 나옵니다.

> dmd -c foo.d
> g++ bar.cpp foo.o -lphobos2 -pthread -o bar && ./bar
i = 6
j = 7
k = 8

C++ 네임스페이스 (C++ Namespaces)

네임스페이스 안에 있는 C++ 심볼도 D에서 접근할 수 있어요. extern (C++) LinkageAttribute네임스페이스를 추가하면 됩니다.

extern (C++, N) int foo(int i, int j, int k);

void main()
{
    N.foo(1, 2, 3);   // foo is in C++ namespace 'N'
}

C++은 같은 파일과 여러 파일에서 같은 네임스페이스를 열 수 있는데, D에서는 다음과 같이 할 수 있어요.

module ns;
extern (C++, `ns`)
{
    int foo() { return 1; }
}

튜플(tuple)의 문자열 또는 빈 튜플로 평가되는 어떤 표현식이든 받아들여집니다. 표현식이 빈 튜플로 평가되면 extern (C++)과 동등해요.

extern(C++, (expression))
{
    int bar() { return 2; }
}

여러 파일로 나눠서는, 여러 모듈로 이루어진 패키지(package)로 구성해서 처리합니다.

ns/
|-- a.d
|-- b.d
|-- package.d

File ns/a.d:

module a; extern (C++, `ns`) { int foo() { return 1; } }

File ns/b.d:

module b; extern (C++, `ns`) { int bar() { return 2; } }

File ns/package.d:

module ns;
public import a, b;

그리고 extern C++ 선언을 담고 있는 패키지를 다음과 같이 임포트하면 돼요.

import ns;
static assert(foo() == 1 && bar() == 2);

여기서 짚고 넘어갈 점은, extern (C++, ns) 링키지 속성은 이 선언들의 ABI(이름 맹글링과 호출 규약)에만 영향을 준다는 거예요. 임포트 자체는 평소와 같은 D 모듈 임포트 시맨틱을 따릅니다.

다른 방법으로는, 문자열이 아닌 형태를 써서 스코프(scope)를 도입할 수 있어요. 이때 주의할 점은 둘러싸고 있는 모듈이 이미 네임스페이스에 선언된 심볼의 스코프를 제공한다는 것이에요. 그러니까 이 형태는 같은 모듈 안에서 같은 네임스페이스를 닫고 다시 여는 것을 허용하지 않습니다. 즉, 다음과 같은 것은 동작하고

module a; extern (C++, ns1) { int foo() { return 1; } }
module b; extern (C++, ns1) { int bar() { return 2; } }
import a, b;
static assert(foo() == 1 && bar() == 2);

이런 것은 동작하지 않습니다.

extern (C++, ns1) { int foo() { return 1; } }
extern (C++, ns1) { int bar() { return 2; } }

추가로, 별칭(alias)을 쓰면 심볼 충돌을 피할 수 있어요.

module a; extern (C++, ns) { int foo() { return 1; } }
module b; extern (C++, ns) { int bar() { return 2; } }
module ns;
import a, b;
alias foo = a.ns.foo;
alias bar = b.ns.bar;
import ns;
static assert(foo() == 1 && bar() == 2);

클래스 (Classes)

C++ 클래스는 class, struct, interface 선언에 extern (C++) 속성을 붙여서 D에 선언할 수 있어요. extern (C++) 인터페이스는 D 인터페이스와 같은 제약을 갖는데, 그 말은 단 하나의 기반 클래스만 멤버 필드를 가질 수 있다는 범위 안에서 다중 상속(Multiple Inheritance)이 지원된다는 뜻입니다.

extern (C++) struct는 가상 함수를 지원하지 않지만, C++의 값 타입(value type)을 매핑하는 데 쓸 수 있어요.

D 링키지를 가진 클래스·인터페이스와 달리, extern (C++) 클래스와 인터페이스는 Object에 뿌리를 두지 않기 때문에 typeid에 사용할 수 없습니다.

한 가지 꼭 알아둬야 할 차이가 있는데, D에서 struct와 class의 시맨틱은 서로 다르지만 C++에서 struct와 class는 기본적으로 같다는 점이에요. D에서 struct로 쓸지 class로 쓸지는 C++ 쪽의 키워드가 아니라 C++ 구현 방식에 따라 결정됩니다.

  • C++ 구현에 가상 멤버 함수가 하나 이상(상속받은 멤버 함수 포함) 있으면, D에서는 extern(C++) class로 선언해야 해요.
  • C++ 구현에 가상 멤버 함수가 전혀 없으면(상속받은 것 포함), D에서는 extern(C++) struct로 선언해야 합니다.

D 클래스를 C++ struct에 매핑할 때는 extern(C++, struct)를 써서, 맹글링할 때 C++의 class와 struct를 구분하는 C++ 컴파일러(특히 MSVC)에서 생기는 링크 문제를 피하세요. 반대로, D struct를 C++ class에 매핑할 때는 extern(C++, class)를 씁니다.

extern(C++, class)extern(C++, struct)는 C++ 네임스페이스와 조합할 수 있어요.

extern (C++, struct) extern (C++, foo)
class Bar
{
}

D에서 C++ 클래스 사용하기 (Using C++ Classes From D)

다음 예제는 순수 가상 함수(pure virtual function), 파생 클래스에서의 그 구현, 비가상 멤버 함수, 멤버 필드를 바인딩하는 방법을 보여줘요.

#include <iostream>

using namespace std;

class Base
{
    public:
        virtual void print3i(int a, int b, int c) = 0;
};

class Derived : public Base
{
    public:
        int field;
        Derived(int field) : field(field) {}

        void print3i(int a, int b, int c)
        {
            cout << "a = " << a << endl;
            cout << "b = " << b << endl;
            cout << "c = " << c << endl;
        }

        int mul(int factor);
};

int Derived::mul(int factor)
{
    return field * factor;
}

Derived *createInstance(int i)
{
    return new Derived(i);
}

void deleteInstance(Derived *&d)
{
    delete d;
    d = 0;
}

이걸 D 코드에서는 이렇게 쓸 수 있어요.

extern(C++)
{
    abstract class Base
    {
        void print3i(int a, int b, int c);
    }

    class Derived : Base
    {
        int field;
        @disable this();
        override void print3i(int a, int b, int c);
        final int mul(int factor);
    }

    Derived createInstance(int i);
    void deleteInstance(ref Derived d);
}

void main()
{
    import std.stdio;

    auto d1 = createInstance(5);
    writeln(d1.field);
    writeln(d1.mul(4));

    Base b1 = d1;
    b1.print3i(1, 2, 3);

    deleteInstance(d1);
    assert(d1 is null);

    auto d2 = createInstance(42);
    writeln(d2.field);

    deleteInstance(d2);
    assert(d2 is null);
}

컴파일·링크·실행하면 이런 출력이 나와요.

> g++ base.cpp
> dmd main.d base.o -L-lstdc++ && ./main
5
20
a = 1
b = 2
c = 3
42

위 예제에서 눈여겨볼 점은, 생성자(constructor)는 바인딩할 수 없어서 D 쪽에서 @disable로 막아둔 것입니다. 대안으로는 생성자를 D에서 다시 구현하는 방법도 있어요. 자세한 내용은 아래의 수명 관리(lifetime management) 섹션을 참고하세요.

C++에서 D 클래스 사용하기 (Using D Classes From C++)

다음과 같은 D 코드가 있다고 해볼게요.

extern (C++) int callE(E);

extern (C++) interface E
{
    int bar(int i, int j, int k);
}

class F : E
{
    extern (C++) int bar(int i, int j, int k)
    {
        import std.stdio : writefln;
        writefln("i = %s", i);
        writefln("j = %s", j);
        writefln("k = %s", k);
        return 8;
    }
}

void main()
{
    F f = new F();
    callE(f);
}

이에 접근하는 C++ 코드는 이렇게 생겼어요.

class E
{
  public:
    virtual int bar(int i, int j, int k);
};


int callE(E *e)
{
    return e->bar(11, 12, 13);
}

> dmd -c base.d
> g++ klass.cpp base.o -lphobos2 -pthread -o klass && ./klass
i = 11
j = 12
k = 13

구조체 (Structs)

C++에서는 struct가 기반 struct를 상속할 수 있어요. D에서는 이것을 alias this로 처리합니다.

struct Base { ... members ... };

struct Derived
{
    Base base;       // make it the first field
    alias base this;

    ... members ...
}

C++과 D 둘 다에서, 필드가 하나도 없는 struct라도 크기는 여전히 1바이트예요. 그런데 C++에서 필드가 없는 struct를 기반 struct로 쓰면 그 크기는 0이 됩니다. 이를 빈 기반 최적화(Empty Base Optimization)라고 해요. D에서 이 동작을 흉내 내는 방법은 두 가지가 있어요. 첫 번째는 기반을 가짜 레퍼런스로 반환하는 함수로 참조를 전달하는 방법입니다.

struct Base { ... members ... };

struct DerivedStruct
{
    static if (Base.tupleof.length > 0)
        Base base;
    else
        ref inout(Base) base() inout
        {
            return *cast(inout(Base)*)&this;
        }
    alias base this;

    ... members ...
}

두 번째는 템플릿 믹스인(template mixin)을 이용하는 방법이에요.

mixin template BaseMembers()
{
    void memberFunction() { ... }
}

struct Base
{
    mixin BaseMembers!();
}

struct Derived
{
    mixin BaseMembers!();

    ... members ...
}

여기서 주의할 점은, 템플릿 믹스인은 선언된 문맥이 아니라 인스턴스화된(instantiation) 문맥에서 평가된다는 거예요. 만약 이것이 문제가 된다면, 템플릿 믹스인이 로컬 임포트를 쓰거나, 멤버 함수가 실제 함수로 전달(forward)되게 하면 됩니다.

C++ 템플릿 (C++ Templates)

C++ 함수 템플릿과 타입 템플릿은 함수 또는 타입 템플릿 선언에 extern (C++) 속성을 붙여 바인딩할 수 있어요.

이때 반드시 기억해야 할 것이 있는데, D 코드에서 사용하는 모든 인스턴스화는 그 인스턴스화를 담고 있는 C++ 오브젝트 코드나 공유 라이브러리를 링크해서 제공받아야 한다는 점이에요.

예를 들어 볼게요.

#include <iostream>

template<class T>
struct Foo
{
    private:
    T field;

    public:
    Foo(T t) : field(t) {}
    T get();
    void set(T t);
};

template<class T>
T Foo<T>::get()
{
    return field;
}

template<class T>
void Foo<T>::set(T t)
{
    field = t;
}

Foo<int> makeIntFoo(int i)
{
    return Foo<int>(i);
}

Foo<char> makeCharFoo(char c)
{
    return Foo<char>(c);
}

template<class T>
void increment(Foo<T> &foo)
{
    foo.set(foo.get() + 1);
}

template<class T>
void printThreeNext(Foo<T> foo)
{
    for(size_t i = 0; i < 3; ++i)
    {
        std::cout << foo.get() << std::endl;
        increment(foo);
    }
}

// The following two functions ensure that the required instantiations of
// printThreeNext are provided by this code module
void printThreeNexti(Foo<int> foo)
{
    printThreeNext(foo);
}

void printThreeNextc(Foo<char> foo)
{
    printThreeNext(foo);
}
extern(C++):
struct Foo(T)
{
    private:
    T field;

    public:
    @disable this();
    T get();
    void set(T t);
}

Foo!int makeIntFoo(int i);
Foo!char makeCharFoo(char c);
void increment(T)(ref Foo!T foo);
void printThreeNext(T)(Foo!T foo);

extern(D) void main()
{
    auto i = makeIntFoo(42);
    assert(i.get() == 42);
    i.set(1);
    increment(i);
    assert(i.get() == 2);

    auto c = makeCharFoo('a');
    increment(c);
    assert(c.get() == 'b');

    c.set('A');
    printThreeNext(c);
}

컴파일·링크·실행하면 이런 출력이 나옵니다.

> g++ -c template.cpp
> dmd main.d template.o -L-lstdc++ && ./main
A
B
C

함수 오버로딩 (Function Overloading)

C++과 D는 함수 오버로딩 규칙이 서로 달라요. D 소스 코드는, 설령 extern (C++) 함수를 호출하더라도, 여전히 D의 오버로딩 규칙을 따릅니다.

메모리 할당 (Memory Allocation)

C++ 코드는 ::operator new()::operator delete() 호출로 메모리를 명시적으로 관리해요. 반면 D의 new 연산자는 D 가비지 컬렉터(GC)로 메모리를 할당하므로 명시적인 delete가 필요 없습니다. D의 new 연산자는 C++의 ::operator new·::operator delete와 호환되지 않아요. D의 new로 할당한 메모리를 C++ ::operator delete로 해제하려고 하면 끔찍한 실패를 겪게 됩니다.

D도 다양한 라이브러리 도구로 메모리를 명시적으로 관리할 수 있는데, 예컨대 std.experimental.allocator가 있어요. 추가로, core.stdc.stdlib.malloccore.stdc.stdlib.free를 직접 사용해서 malloc된 버퍼를 기대하는 C++ 함수에 연결할 수 있습니다.

D 가비지 컬렉터 힙에서 할당한 메모리 포인터를 C++ 함수에 넘긴다면, 그 C++ 함수가 처리를 끝내기 전에 참조하는 메모리가 D GC에 수집되지 않도록 보장하는 것이 중요해요. 그 방법은 이러합니다.

  • std.experimental.allocatorcore.stdc.stdlib.malloc로 데이터의 복사본을 만들고, 그 복사본을 대신 넘긴다.
  • 스택(파라미터나 자동 변수로)에 그 포인터를 남겨둔다. 가비지 컬렉터가 스택을 스캔하기 때문이에요.
  • 정적 데이터 세그먼트(static data segment)에 그 포인터를 남겨둔다. 가비지 컬렉터가 정적 데이터 세그먼트를 스캔하기 때문이에요.
  • core.memory.GC.addRootcore.memory.GC.addRange 함수로 그 포인터를 가비지 컬렉터에 등록한다.

할당된 메모리 블록에 대한 **내부 포인터(interior pointer)**만 있어도 GC는 객체가 사용 중임을 알 수 있어요. 즉, 할당된 메모리의 시작 지점을 가리키는 포인터를 유지할 필요는 없다는 뜻입니다.

가비지 컬렉터는 D 런타임에 등록되지 않은 스레드의 스택은 스캔하지 않고, D 런타임에 등록되지 않은 공유 라이브러리의 데이터 세그먼트도 스캔하지 않습니다.

데이터 타입 호환성 (Data Type Compatibility)

D와 C++ 타입 등가(equivalence)

D 타입 | C++ 타입

void | void

byte | signed char

ubyte | unsigned char

char | char (D에서 char는 unsigned)

core.stdc.stddef.wchar_t | wchar_t

short | short

ushort | unsigned short

int | int

uint | unsigned

long | 64비트면 long, 아니면 long long

ulong | 64비트면 unsigned long, 아니면 unsigned long long

core.stdc.config.cpp_long | long

core.stdc.config.cpp_ulong | unsigned long

float | float

double | double

real | long double

extern (C++) struct | 가상 멤버 함수가 없는 struct 또는 class

extern (C++) class | 가상 멤버 함수가 있는 struct 또는 class

extern (C++) interface | 멤버 필드가 없는 struct 또는 class

union | union

enum | enum

type * | type *

ref type (파라미터 목록에서만) | type &

type**[dim]** | 변수/필드 선언에서는 type**[dim]**, 함수 파라미터면 ref 사용

type**[dim]*** | type*(*)[dim]**

type [] | extern (C++) 등가 없음, 아래 참고

type**[type]** | 등가 없음

type function(parameters) | type(*)(parameters)

type delegate(parameters) | 등가 없음

이 등가 관계는 사용하는 D 컴파일러와 C++ 컴파일러가 호스트 플랫폼의 동반자(companion)일 때 성립합니다.

동적 배열 (Dynamic Arrays)

동적 배열은 extern (C++)에서 지원되지 않아요. extern (C)에서는 struct 템플릿과 동등합니다. 예를 들면:

extern (C) const(char)[] slice;

dmd -HC는 다음과 같은 C++ 선언을 생성해요.

extern "C" _d_dynamicArray< const char > slice;

_d_dynamicArray는 다음과 같이 생성됩니다.

/// Represents a D [] array
template<typename T>
struct _d_dynamicArray final
{
    size_t length;
    T *ptr;

    _d_dynamicArray() : length(0), ptr(NULL) { }

    _d_dynamicArray(size_t length_in, T *ptr_in)
        : length(length_in), ptr(ptr_in) { }

    T& operator[](const size_t idx) {
        assert(idx < length);
        return ptr[idx];
    }

    const T& operator[](const size_t idx) const {
        assert(idx < length);
        return ptr[idx];
    }
};

패킹과 정렬 (Packing and Alignment)

D struct와 union은 C의 그것과 유사해요.

C 코드는 종종 명령줄 스위치나 각종 구현별 #pragma로 struct 멤버의 정렬과 패킹을 조정합니다. D는 C 컴파일러의 규칙에 대응하는 명시적 정렬 속성을 지원해요. C 코드가 어떤 정렬을 쓰고 있는지 확인해서, D struct 선언에도 똑같이 명시적으로 설정해 주세요.

D는 표준 라이브러리에서 비트필드(bitfield)를 지원합니다. std.bitmanip.bitfields를 보세요.

수명 관리 (Lifetime Management)

C++ 생성자, 복사 생성자, 이동 생성자(move constructor), 소멸자는 D 코드에서 직접 호출할 수 없고, D 생성자, postblit 연산자, 소멸자도 C++ 코드로 직접 내보낼 수 없어요. 이런 특수 연산자를 가진 타입들의 상호운용은 다음 두 방법 중 하나로 가능합니다. 1) 클라이언트 언어에서 그 연산자를 비활성화하고 호스트 언어에서만 사용한다, 또는 2) 클라이언트 언어에서 그 연산자를 충실히 다시 구현한다. 후자의 경우 두 구현 사이에 관찰 가능한 시맨틱이 같게 유지되도록 신경 써야 하는데, 두 언어에서 연산자가 동작하는 방식이 달라서 어렵거나, 일부 극단적인 경우에는 불가능할 수도 있어요. 예를 들어 D에서는 모든 객체가 이동 가능하고 이동 생성자가 없습니다.

연산자 오버로딩 (Operator Overloading)

D의 연산자 오버로딩은 주로 단항·이항 연산자 오버로딩을 다루는데, 증감 연산자, 인덱스, 슬라이스, 캐스트(cast), 함수 호출, 비교 연산자 등을 포함해요. C++에 존재하는 이런 연산자의 오버로드들은 D 연산자와 그에 대응하는 C++ 연산자가 시맨틱상 일치한다면 D에서 직접 호출할 수 있습니다. 아래는 할당·인덱스·함수 호출 연산자 오버로드가 D와 C++ 사이에서 상호 운용되는 간단한 사용 예제예요.

foo.cpp 파일에서:

#include <iostream>

class A
{
public:
    int a;
    int b;
    int buffer[3];
    A()
    {
        a = 0;
        b = 0;
    }
    A(int a, int b);
    A& operator=(const A& obj); //overloading the assignment operator
    void operator()(int a, int b, int c); // overloading the call operator
    int& operator[](int value); // overloading the index operator
    void printbuffer();
};

A::A(int a, int b)
{
    this->a = a;
    this->b = b;
}

A& A::operator=(const A& obj)
{
    a = obj.a;
    b = obj.b;
    return *this;
}

void A::operator()(int a, int b, int c)
{
    std::cout << "value of a is "<< a << std::endl;
    std::cout << "value of b is "<< b << std::endl;
    std::cout << "value of c is "<< c << std::endl;
}

int& A::operator[](int value)
{
    return buffer[value];
}

void A::printbuffer()
{
    for (int i = 0; i < 3; i++)
    {
        std::cout << "the buffer numbers of index "<< i << " is "<< buffer[i] << std:: endl;
    }
}

bar.d 파일에서:

import std.stdio;

extern(C++) struct A
{
    int a;
    int b;
    int[3] buffer;
    this(int a, int b);
    ref A opAssign(ref const A); // links with A::operator=
    void opCall(int a, int b, int c); // links with A::operator()
    ref int opIndex(int value); // links with A::operator[]
    void printbuffer();
}

void main()
{
    auto obj1 = A(1, 3);
    auto obj2 = A(13, 43);
    obj2 = obj1; // opAssign called here
    writeln("obj2's a is ", obj2.a);
    writeln("obj2's b is ", obj2.b);
    obj2(20, 40, 60);
    obj2[0] = 30;
    obj2[1] = 60;
    obj2[2] = 90;
    obj2.printbuffer();
}

컴파일·링크·실행하면 이런 출력이 나옵니다.

> g++ -c foo.cpp
> dmd bar.d foo.o -L-lstdc++ && ./bar
obj2's a is 1
obj2's b is 3
value of a is 20
value of b is 40
value of c is 60
the buffer numbers of index 0 is 30
the buffer numbers of index 1 is 60
the buffer numbers of index 2 is 90

런타임 타입 식별 (Runtime Type Identification)

D의 런타임 타입 식별(RTTI)은 C++과 완전히 다른 기법을 사용해요. 두 방식은 서로 호환되지 않습니다.

예외 처리 (Exception Handling)

예외 상호운용은 아직 진행 중(work in progress)인 작업입니다.

현재로서는, C++ 예외를 D에서 잡거나 던질 수 없고, D 예외를 C++에서 잡거나 던질 수도 없어요. 추가로, D 예외가 스택을 풀(unwind) 때 C++ 스택 프레임 안의 객체가 파괴된다는 보장이 없고, 그 반대의 경우도 마찬가지입니다.

계획은 위의 모든 것을 지원하는 것이되, C++ 코드에서 D 예외를 직접 던지는 것만 빼고 지원하는 것입니다 (다만 C++ 링키지를 가진 D 함수를 호출하는 방식으로는 간접적으로 던질 수 있게 될 거예요).

D의 Immutable·Const와 C++의 Const 비교 (Comparing D Immutable and Const with C++ Const)

Const·Immutable 비교 **기능** | **D** | **C++98**

const 키워드 | Yes | Yes

immutable 키워드 | Yes | No

const 표기법 |

// Functional:
//ptr to const ptr to const int
const(int*)* p;

|

// Postfix:
//ptr to const ptr to const int
const int *const *p;

전이적 const (transitive const) |

// Yes:
//const ptr to const ptr to const int
const int** p;
**p = 3; // error

|

// No:
// const ptr to ptr to int
int** const p;
**p = 3;    // ok

const 떼어내기 (cast away const) |

// Yes:
// ptr to const int
const(int)* p;
int* q = cast(int*)p; // ok

|

// Yes:
// ptr to const int
const int* p;
int* q = const_cast<int*>p; //ok

cast 후 변형 (cast+mutate) |

// No:
// ptr to const int
const(int)* p;
int* q = cast(int*)p;
*q = 3;   // undefined behavior

|

// Yes:
// ptr to const int
const int* p;
int* q = const_cast<int*>p;
*q = 3;   // ok

오버로딩 |

// Yes:
void foo(int x);
void foo(const int x);  //ok

|

// No:
void foo(int x);
void foo(const int x);  //error

const/mutable 앨리어싱 (aliasing) |

// Yes:
void foo(const int* x, int* y)
{
    bar(*x); // bar(3)
    *y = 4;
    bar(*x); // bar(4)
}
...
int i = 3;
foo(&i, &i);

|

// Yes:
void foo(const int* x, int* y)
{
    bar(*x); // bar(3)
    *y = 4;
    bar(*x); // bar(4)
}
...
int i = 3;
foo(&i, &i);

immutable/mutable 앨리어싱 |

// No:
void foo(immutable int* x, int* y)
{
    bar(*x); // bar(3)
    *y = 4;  // undefined behavior
    bar(*x); // bar(??)
}
...
int i = 3;
foo(cast(immutable)&i, &i);

| immutable 없음

문자열 리터럴의 타입 | immutable(char)[] | const char*

문자열 리터럴을 non-const로 | 허용 안 됨 | 허용하지만 deprecated

더 알아보기 (Learn more)