英語版
このページの英語版を見る

C++とのインターフェイス

この文書では、C++と直接インターフェイスする方法を説明する。

C++コードと間接的にインターフェイスすることも可能であり、 C言語インターフェイスまたは COMインターフェイスを介して行うことができる。

基本的な考え方

C++と100%の互換性を持つということは、多かれ少なかれ Dに完全な機能を持つC++コンパイラのフロントエンドを追加することを意味する。 逸話的な証拠によると、そのようなものを書くには 10人年単位のプロジェクトが最低限必要であり、本質的には そのような機能を持つDコンパイラは実装不可能である。 C++と連携しようとする他の言語も同じ問題に直面しており、 解決策は以下の通りである。

  1. COMインターフェースをサポートする(ただし、これはWindowsでのみ動作する)。
  2. C++コードの周りにCラッパーを苦労して構築する。
  3. SWIGなどの自動化ツールを使用してCラッパーを構築する。
  4. C++コードを他の言語で再実装する。
  5. あきらめる。

Dは、いくつかのささやかな調整で問題の大部分を解決できるという現実的なアプローチを取る。

プラットフォームとコンパイラのサポート

グローバル関数

C++のグローバル関数(名前空間内のものも含む)は、 Dで宣言および呼び出し、またはDで定義しC++で呼び出すことができる。

DからC++グローバル関数を呼び出す

C++ソースファイルに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コードでは、fooは 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);
}

2つのファイルをコンパイルする。1つ目はC++コンパイラで、 2つ目はDコンパイラで、それらをリンクし、 実行すると、以下のような結果が得られる。

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

ここでいくつかのことが起こっている。

C++からDのグローバル関数を呼び出す

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++シンボルは、 Dからアクセスすることができる。名前空間は extern (C++)LinkageAttributeに追加することができる。

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

void main()
{
    N.foo(1, 2, 3);   // fooはC++の名前空間'N'にある
}

C++では、同一ファイルおよび複数のファイルで同一の名前空間を開くことができる。 Dでは、次のようにして行うことができる。

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

文字列のタプルまたは空のタプルに解決する任意の式が受け入れられる。 式が空のタプルに解決される場合、それはextern (C++)

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

または複数のファイルで、それらを複数のモジュールで構成されるパッケージにまとめることで、

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

ファイルns/a.d:

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

ファイルns/b.d:

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

ファイルns/package.d:

module ns;
public import a, b;

次に、外部C++宣言を含むパッケージを次のようにインポートする。

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

extern (C++, ns) リンク属性は、これらの宣言の ABI(名前のマングリングと呼び出し規約)のみに影響する点に注意すること。 それらをインポートすると、通常の D モジュールインポートセマンティクスに従う。

あるいは、文字列以外の形式を使用してスコープを導入することもできる。 囲むモジュールは、すでに名前空間で宣言されたシンボルに対してスコープを提供していることに注意。 この形式では、同じモジュール内で同じ名前空間を閉じてから再度開くことはできない。つまり、

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; } }

。さらに、エイリアスを使用してシンボルの衝突を回避することもできる。

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);

クラス

C++クラスは、classstructinterfaceの宣言にextern (C++)属性を使用することで、Dで宣言できる。extern (C++) インターフェイスには、Dインターフェイスと同じ制限がある。つまり、 多重継承は、1つのベースクラスにのみメンバフィールドを持つことができる範囲でサポートされている。

extern (C++) 構造体は仮想関数をサポートしていないが、 C++の値型をマッピングするために使用できる。

D リンケージのクラスやインターフェースとは異なり、extern (C++) クラスやインターフェースはObject をルートとしておらず、typeid と併用することはできない。

Dの構造体とクラスは異なるセマンティクスを持ち、一方、C++の構造体とクラスは基本的に同じである。 クラスは基本的に同じである。D構造体またはクラスの使用は、 C++の実装に依存し、使用されるC++キーワードには依存しない。 Dclass をC++struct にマッピングする場合は、 C++コンパイラ(特にMSVC)がC++のclassstructを マングリング時に区別することによるリンクの問題を回避するために、extern(C++, struct) を使用する。逆に、Dstruct をC++class にマッピングする場合は、extern(C++, class) を使用する。

extern(C++, class) また、 は C++ の名前空間と組み合わせることができる。extern(C++, struct)

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

DからのC++クラスの使用

以下の例は、純粋仮想関数のバインド、 派生クラスでのその実装、非仮想メンバ関数、および メンバフィールドを示している:

#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

上記の例では、コンストラクタはバインドできず、 代わりにD側で無効化されていることに注目されたい。代替策としては、 Dでコンストラクタを再実装する方法がある。詳細は、以下の「ライフタイム管理」のセクションを参照のこと。

C++からDクラスを使用する

次のような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

構造体

C++では、構造体がベース構造体から継承することが可能である。これはDではalias this を使用して行われる。

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

struct Derived
{
    Base base;       // これを最初のフィールドにする
    alias base this;

    ... members ...
}

C++とDの両方において、構造体がフィールドをゼロ個だけ持つ場合、その構造体のサイズは 1バイトのままとなる。しかし、C++においてフィールドをゼロ個だけ持つ構造体がベース構造体として使用される場合、そのサイズはゼロとなる( 「空のベース最適化」と呼ばれる )。 Dでこの挙動をエミュレートするには2つの方法がある。 1つ目は、ベースへの偽装参照を返す関数への参照を転送する方法である。

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 ...
}

2つ目は、テンプレートミックスインを利用する方法である。

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

struct Base
{
    mixin BaseMembers!();
}

struct Derived
{
    mixin BaseMembers!();

    ... members ...
}

テンプレートミックスインは、宣言ではなくインスタンス化のコンテキストで評価されることに注意すること。 これが問題となる場合は、テンプレートミックスインで ローカルインポートを使用するか、メンバ関数で実際の関数に転送するようにする ことができる。

C++ テンプレート

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); } }
// 以下の2つの関数は、printThreeNextの必要なインスタンスが // このコードモジュールによって提供されることを保証する 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

関数のオーバーロード

C++とDでは、関数のオーバーロードに関するルールが異なる。 Dソースコードでは、extern (C++) 関数を呼び出す場合でも、 Dのオーバーロードルールに従う。

メモリの割り当て

C++コードでは、::operator new() および::operator delete() への呼び出しにより、メモリを明示的に管理する。 Dのnew 演算子は、Dのガベージコレクタを使用してメモリを割り当てるため、 明示的なdeleteは必要ない。Dのnew 演算子は、 C++の::operator new および::operator delete と互換性がない。 Dのnew でメモリを割り当て、C++の::operator delete で解放しようとすると、 悲惨な失敗に終わる。

Dは、さまざまなライブラリツールを使用してメモリを明示的に管理できる。例えば、 std.experimental.allocator。さらに、core.stdc.stdlib.malloccore.stdc.stdlib.freemalloc'dバッファを期待するC++関数に直接接続するために使用できる。

D ガベージコレクタのヒープ上に割り当てられたメモリへのポインタが C++関数に渡される場合、参照されたメモリが C++関数がそのメモリを処理し終える前にDガベージコレクタによって収集されないことを確実にすることが重要である。 これは以下によって達成される。

割り当てられたメモリブロックへの内部ポインタがあれば、 GCにオブジェクトが使用中であることを知らせるのに十分である。つまり、 割り当てられたメモリの先頭へのポインタを維持する必要はない。

ガベージコレクタは、Dランタイムに登録されていないスレッドのスタックをスキャンしない。 また、Dランタイムに登録されていない共有ライブラリのデータセグメントもスキャンしない。

データ型の互換性

DとC++の型の同等性
D typeC++ type
void void
byte signed char
ubyte unsigned char
char char (Dでは文字は符号なし)
core.stdc.stddef.wchar_t wchar_t
short short
ushort unsigned short
int int
uint unsigned
long long 64ビット幅の場合、それ以外はlong long
ulong unsigned long 64ビット幅の場合は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
"型"* *
ref (パラメータリストのみ) &
[ dim] [dim] 変数/フィールド宣言の場合は、 または、関数パラメータの場合はref を使用する
[ dim]* (*)[ dim]
[] いいえextern (C++) 同等品、下記参照
[] 同等品なし
function( パラメータ) (*) ( parameters)
delegate ( パラメータ) 同等なし

これらの等価性は、使用されるDおよびC++コンパイラがホストプラットフォームのコンパニオンである場合に有効である。

動的配列

これらはextern (C++) ではサポートされていない。extern (C) では、 これらは構造体テンプレートと同等である。例えば、

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

dmd -HC 次のC++宣言が生成される。

extern "C" _d_dynamicArray< const char > slice;

_d_dynamicArray 次のように生成される:

/// Dの[]配列を表す
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];
    }
};

パッキングとアラインメント

D 構造体および共用体は、C の構造体および共用体に類似している。

Cコードでは、構造体メンバのアラインメントとパッキングを、コマンドラインスイッチや、実装固有のさまざまな #pragma で調整することがよくある。Dでは、Cコンパイラのルールに対応する明示的なアラインメント属性がサポートされている。 Cコードがどのアラインメントを使用しているかを確認し、 Dの構造体宣言で明示的に設定する。

Dは標準ライブラリでビットフィールドをサポートしている。 std.bitmanip.bitfields

ライフタイム管理

C++のコンストラクタ、コピーコンストラクタ、ムーブコンストラクタ、デストラクタは Dコードから直接呼び出すことはできず、Dのコンストラクタ、ポストブライト演算子、 デストラクタはC++コードに直接エクスポートすることはできない。 これらの特殊な演算子を持つ型の相互運用は、1) クライアント言語で演算子を無効にしてホスト言語でのみ使用するか、 2)クライアント言語で演算子を忠実に再実装するかのいずれかによって可能となる 後者のアプローチでは、 目に見えるセマンティクスが両方の実装で同じであることを確認する必要があるが、 これは難しい場合があり、または、2つの言語における演算子の動作の違いにより、一部のエッジケースでは不可能な場合もある。 例えば、Dではすべてのオブジェクトが 可動であり、移動コンストラクタは存在しない。

特殊メンバ関数

DはC++の特殊メンバ関数を直接呼び出すことができず、その逆もできない。 これには、コンストラクタ、デストラクタ、変換演算子、 演算子オーバーロード、アロケータが含まれる。

実行時の型識別

Dの実行時型識別は、 C++とは全く異なる技術を使用している。 この2つは互換性がない。

例外処理

例外の相互運用性は現在も開発中である。

現時点では、C++の例外はDで捕捉したりDからスローしたりすることはできず、またDの 例外はC++で捕捉したりC++からスローしたりすることもできない。さらに、 C++のスタックフレーム内のオブジェクトは、Dの例外によってスタックが巻き戻される際に破壊されることが保証されておらず、 その逆も同様である。

計画としては、上記のすべてをサポートする予定であるが、C++コードからD例外を直接スローすることはできない (ただし、C++リンクのD関数を呼び出すことで間接的にスローすることは可能 )。

DのImmutableとConstをC++のConstと比較する

Const、Immutable 比較
FeatureDC++98
const キーワードはいはい
immutable キーワードはいいいえ
定数記法
// Functional:
//ptr to const ptr to const int
const(int*)* p;
// Postfix:
//ptr to const ptr to const int
const int *const *p;
推移的 const
// Yes:
//const ptr to const ptr to const int
const int** p;
**p = 3; // エラー
// No:
// const ptr to ptr to int
int** const p;
**p = 3;    // OK
キャストアウェイ定数
// 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
キャスト+変更
// No:
// ptr to const int
const(int)* p;
int* q = cast(int*)p;
*q = 3;   // 未定義の動作
// 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 エイリアシング
// 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);
不変/変更可能エイリアシング
// No:
void foo(immutable int* x, int* y)
{
    bar(*x); // bar(3)
    *y = 4;  // 未定義の動作
    bar(*x); // bar(??)
}
...
int i = 3;
foo(cast(immutable)&i, &i);
不変
文字列リテラルの型 immutable(char)[] const char*
文字列リテラルを非定数に 許可されない 許可されているが、非推奨