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

C++プログラマーのためのD言語プログラミング

経験豊富なC++プログラマーは、自然に使えるようになるイディオムやテクニックを蓄積している。 新しい言語を学習する際、それらの イディオムが快適すぎて、新しい言語で同等の処理を行う方法がわからないことがある。 そこで、ここでは一般的なC++テクニックと、それに対応する Dでの処理方法を紹介する。

参照:CプログラマーのためのD言語でのプログラミング

コンストラクタの定義

C++の方法

コンストラクタはクラスと同じ名前を持つ。
class Foo
{
    Foo(int x);
};

Dの方法

コンストラクタは、this キーワードで定義される。
class Foo
{
    this(int x) { }
}
Dでの使用法を反映している。

基本クラスの初期化

C++の方法

ベースコンストラクタは、ベース初期化子の構文を使用して呼び出される。
class A { A() {... } };
class B : A
{
    B(int x)
        : A()   // 基本コンストラクタを呼び出す
    {
        ...
    }
};

Dの方法

基本クラスのコンストラクタは、super 構文で呼び出される。
class A { this() { ... } }
class B : A
{
    this(int x)
    {
        ...
        super();    // 基本コンストラクタを呼び出す
        ...
    }
}
C++よりも優れているのは、ベースクラスのコンストラクタの呼び出しが派生クラスのコンストラクタ内の任意の場所に柔軟に配置できる点である。 Dでは、コンストラクタが別のコンストラクタを呼び出すことも可能である。
class A
{
    int a;
    int b;
    this() { a = 7; b = foo(); }
    this(int x)
    {
        this();
        a = x;
    }
}
また、コンストラクタが呼び出される前にメンバーを定数で初期化することもできるため、上記の例は 以下のように同等に記述できる。
class A
{
    int a = 7;
    int b;
    this() { b = foo(); }
    this(int x)
    {
        this();
        a = x;
    }
}

構造体を比較する

C++の方法

C++では、構造体の代入はシンプルで便利な方法で定義されているが、
struct A x, y;
...
x = y;
構造体の比較については定義されていない。そのため、2つの構造体インスタンスを比較して等価であるかどうかを判断するには、
#include <string.h>

struct A x, y;

inline bool operator==(const A& x, const A& y)
{
    return (memcmp(&x, &y, sizeof(struct A)) == 0);
}
...
if (x == y)
    ...

演算子オーバーロードは比較が必要なすべての構造体に対して行う必要があることに 注意し、そのオーバーロード演算子の実装は、型チェックに関する言語のサポートを一切受けない。 C++の方法には、さらに問題がある。 (x == y) を調べただけでは、実際に何が起こっているのかわからない。

memcmp() での operator==() の実装には厄介なバグが潜んでいる。 構造体のレイアウトは、アラインメントの関係で「穴」が生じる可能性がある。 C++ はこれらの穴に値が割り当てられることを保証しないため、 異なる2つの構造体インスタンスが各メンバに同じ値を持つことがあり得るが、 穴に異なるガベージが含まれるため、比較結果は異なる。

この問題に対処するために、operator==() を実装してメンバごとに比較を行うことができる。 しかし残念ながら、これは信頼性に欠ける。なぜなら、(1) メンバが構造体定義に追加された場合、 operator==() に追加し忘れる可能性があること、および、(2) 浮動小数点の nan 値は、ビットパターンが一致していても比較結果が異なる

C++には堅牢なソリューションがない。

Dの方法

Dは明白で、単純な方法でそれを実現する。
A x, y;
...
if (x == y)
    ...

新しい型定義型を作成する

C++のやり方

C++における型定義は弱いものであり、つまり、実際には新しい型を導入するものではない。 コンパイラは型定義と その基底型を区別しない。
#define HANDLE_INIT     ((Handle)(-1))
typedef void *Handle;
void foo(void *);
void bar(Handle);

Handle h = HANDLE_INIT;
foo(h);         // コーディングバグが見つからない
bar(h);         // OK
C++の解決策は、 新しい型で型チェックとオーバーロードを行うことを唯一の目的とするダミーの構造体を作成することである。
#define HANDLE_INIT     ((void *)(-1))
struct Handle
{
    void *ptr;

    // デフォルトの初期化子
    Handle() { ptr = HANDLE_INIT; }

    Handle(int i) { ptr = (void *)i; }

    // 基本型への変換
    operator void*() { return ptr; }
};
void bar(Handle);

Handle h;
bar(h);
h = func();
if (h != HANDLE_INIT)
    ...

Dの方法

上記のような慣用的な構文は必要ない。 次のようにすればよい。 std.typecons.Typedef:
alias Handle = Typedef!(void*, cast(void*)-1);
void bar(Handle);

Handle h;
bar(h);
h = func();
if (h != Handle.init)
    ...
alias のように単なる記号とは異なり、 std.typecons.Typedef 2つの型が同等とみなされないようにします。 デフォルトの初期化子が std.typecons.Typedef 基本となる型の値として指定できる。
"value"値"

Friends

C++の方法

2つのクラスが密接に関連しているが、継承関係にあるわけではない場合、 互いのプライベートメンバにアクセスする必要がある。これは、 宣言を使用して行う。 friend宣言を使用する:
class A
{
  private:
    int a;

  public:
    int foo(B *j);
    friend class B;
    friend int abc(A *);
};

class B
{
  private:
    int b;

  public:
    int bar(A *j);
    friend class A;
};

int A::foo(B *j) { return j->b; }
int B::bar(A *j) { return j->a; }

int abc(A *p) { return p->a; }

Dの方法

Dでは、friend アクセスは同じモジュールのメンバーであることの暗黙的なものである。 密接に関連するクラスは同じモジュールにあるべきであるという理屈は通っている。 そのため、暗黙的にfriend アクセスを他のモジュールメンバーに付与することで 問題をうまく解決できる。
module X;

class A
{
  private:
    static int a;

  public:
    int foo(B j) { return j.b; }
}

class B
{
  private:
    static int b;

  public:
    int bar(A j) { return j.a; }
}

int abc(A p) { return p.a; }
private 属性により、他のモジュールが メンバーにアクセスできなくなる。

演算子オーバーロード

C++の方法

新しい算術データ型を作成する構造体が与えられた場合、 比較演算子をオーバーロードして、 整数と比較できるようにすると便利である。
struct A
{
    int operator <  (int i);
    int operator <= (int i);
    int operator >  (int i);
    int operator >= (int i);
};

int operator <  (int i, A &a) { return a >  i; }
int operator <= (int i, A &a) { return a >= i; }
int operator >  (int i, A &a) { return a <  i; }
int operator >= (int i, A &a) { return a <= i; }
合計8つの関数が必要である。

Dの方法

Dは、比較演算子がすべて基本的に互いに関連していることを認識している。 そのため、必要な関数は1つだけである。
struct A
{
    int opCmp(int i);
}

コンパイラは、 <、<=、>、および>= 演算子をすべて自動的にcmp 関数の観点で解釈し、 左オペランドがオブジェクト参照ではない場合も処理する。

同様の賢明なルールは、他の演算子オーバーロードにも適用され、 Dでの演算子オーバーロードの使用をはるかに面倒でなく、エラーを起こしにくいものにしている。同じ効果を得るために書く必要のあるコードははるかに少ない。


名前空間を使用する宣言

C++の方法

C++におけるusing宣言は、 名前空間スコープから現在のスコープに名前を移動するために使用される。
namespace foo
{
    int x;
}
using foo::x;

Dの方法

Dでは、名前空間や#includeファイルの代わりにモジュールを使用し、 エイリアス宣言がusing宣言の代わりとなる。
/** foo.dモジュール **/
module foo;
int x;

/** 別のモジュール **/
import foo;
alias x = foo.x;
エイリアスは単一目的のusing宣言よりもはるかに柔軟である。 エイリアスはシンボルの名前変更、テンプレートメンバーの参照、 ネストされたクラス型の参照など、さまざまな用途に使用できる。

RAII(リソース取得は初期化である)

C++の方法

C++では、メモリなどのリソースはすべて明示的に処理する必要がある。 デストラクタは自動的に呼び出されるため、 RAIIはリソース解放コードをデストラクタに配置することで実装される。
class File
{
    Handle *h;

    ~File()
    {
        h->release();
    }
};

Dの方法

リソース解放の問題の大部分は、単純に メモリの追跡と解放を行うことである。Dでは、これはガベージコレクタによって自動的に処理される。 2番目に良く使用されるリソースはセマフォとロックであり、 これらはDのsynchronized宣言と文によって自動的に処理される。

残りのRAIIの問題は、structによって処理される。struct は、スコープ外に出るとデストラクタが実行される。

struct File
{
    Handle h;

    ~this()
    {
        h.release();
    }
}

void test()
{
    if (...)
    {
        auto f = File();
        ...
    } // f.~this()は、スローされた例外によってスコープが終了した場合でも、
      // 終了ブレースで実行される。
}

class通常、ガベージコレクタによって管理されるが、これは RAIIには適していない。classによる決定論的な破棄が必要な場合は、 を使用できる std.typecons.scoped(これも ガベージコレクタが管理するヒープではなく、スタック上にclass を割り当てる)。

ScopeGuardStatementも参照のこと。 現在のスコープを離れる際に任意の文を実行できる、より一般的な メカニズムである。


プロパティ

C++の方法

フィールドを定義する際には、 オブジェクト指向の get関数とset関数を一緒に定義するのが一般的である。
class Abc
{
  public:
    void setProperty(int newproperty) { property = newproperty; }
    int getProperty() { return property; }

  private:
    int property;
};

Abc a;
a.setProperty(3);
int x = a.getProperty();
これらはすべてかなりの量のタイピングを必要とし、 getProperty()とsetProperty()の呼び出しで コードが埋め尽くされるため、コードが読みにくくなる傾向がある。

Dの方法

プロパティは通常のフィールド構文で取得および設定できるが、 実際には、get および set は代わりにメソッドを呼び出す。
class Abc
{
    // 設定
    @property void property(int newproperty) { myprop = newproperty; }

    // 取得
    @property int property() { return myprop; }

  private:
    int myprop;
}
これは次のように使用される。
Abc a = new Abc;
a.property = 3;
int x = a.property;
このように、Dではプロパティは単純なフィールド名のように扱われる。 プロパティは、最初は単純なフィールド名であってもよいが、 後に そのプロパティの取得や設定に"関数"呼び出しが必要になった場合でも、 コードを修正する必要はない。 これは、派生クラスがgetやsetプロパティをオーバーライドする必要があるかもしれないという「万が一」に備えて、getやsetプロパティを定義するという煩雑な作業を回避するものである。 派生クラスがそれらをオーバーライドする必要がある場合に備えて、 また、データフィールドを持たないインターフェースクラスを データフィールドを持つかのように構文上動作させる ことも可能である。

再帰的テンプレート

C++の方法

テンプレートの高度な使用法としては、再帰的に展開し、 特殊化によって終了させるというものがある。階乗を計算するテンプレートは 次のようになる。
template<int n> class factorial
{
  public:
    enum { result = n * factorial<n - 1>::result };
};

template<> class factorial<1>
{
  public:
    enum { result = 1 };
};

void test()
{
    printf("%d\n", factorial<4>::result); // 24を表示する
}

Dの方法

Dバージョンも同様だが、よりシンプルで、 同一名テンプレート- 単一テンプレートメンバーを包含する名前空間への昇格 :
template factorial(int n)
{
    enum factorial = n * .factorial!(n-1);
}

template factorial(int n : 1)
{
    enum factorial = 1;
}

void test()
{
    writeln(factorial!(4));  // 24を表示する
}
列挙型テンプレート構文を使用することで、テンプレートブロックを短くすることができる。
enum factorial(int n) = n * .factorial!(n-1);

enum factorial(int n : 1) = 1;

メタテンプレート

問題:符号付き整数型で、少なくともnbitsのサイズを持つ typedef を作成する。

C++の方法

この例は、 カルロ・ペスチョ博士が 「テンプレート・メタプログラミング:この新しいテクニックでパラメータ化された整数をポータブルにする」で書いたものを簡略化し、一部修正したものである。

C++には、テンプレートパラメータに基づく式の結果に基づいて条件付きコンパイルを行う方法はない ため、 すべての制御フローは、テンプレート引数とさまざまな明示的なテンプレート特殊化とのパターンマッチングに従う。

C++では、テンプレートパラメータに基づく式の結果に基づいて条件付きコンパイルを行う方法はないため、 すべての制御フローは、テンプレート引数とさまざまな明示的なテンプレート特殊化のパターンマッチングに従う。 さらに悪いことに、「以下」のような関係に基づいてテンプレート特殊化を行う方法もないため、 この例では、テンプレートが再帰的に展開され、 特殊化が一致するまで、テンプレート値の引数を毎回1ずつ増やしながら、 テンプレートを再帰的に展開する巧妙なテクニックが使用されている。 一致しない場合は、役に立たない再帰コンパイラのスタックオーバーフローや内部エラー、あるいはせいぜい奇妙な構文エラーが発生する。

また、テンプレート型定義の不足を補うために、プリプロセッサマクロも必要となる。
#include <limits.h>

template< int nbits > struct Integer
{
    typedef Integer< nbits + 1 > :: int_type int_type ;
};

struct Integer< 8 >
{
    typedef signed char int_type ;
};

struct Integer< 16 >
{
    typedef short int_type ;
};

struct Integer< 32 >
{
    typedef long int_type ;
};

struct Integer< 64 >
{
    typedef long long int_type ;
};

// 必要なサイズがサポートされていない場合、
// メタプログラムは内部エラーが通知されるか、
// INT_MAXに達するまでカウンタを増やす。INT_MAXの特殊化では
// int_typeを定義しないため、常
// にコンパイルエラーが発生する。
struct Integer< INT_MAX >
{
};

// 構文糖
#define Integer( nbits ) Integer< nbits > :: int_type

#include <stdio.h>

int main()
{
    Integer( 8 ) i ;
    Integer( 16 ) j ;
    Integer( 29 ) k ;
    Integer( 64 ) l ;
    printf("%d %d %d %d\n",
        sizeof(i), sizeof(j), sizeof(k), sizeof(l));
    return 0 ;
}

C++ Boost による方法

このバージョンでは、C++ Boost ライブラリを使用している。 これは David Abrahams 氏による提供である。
#include <boost/mpl/if.hpp>
#include <boost/mpl/assert.hpp>

template <int nbits> struct Integer
    : mpl::if_c<(nbits <= 8), signed char
    , mpl::if_c<(nbits <= 16), short
    , mpl::if_c<(nbits <= 32), long
    , long long>::type >::type >
{
    BOOST_MPL_ASSERT_RELATION(nbits, <=, 64);
}

#include <stdio.h>

int main()
{
    Integer< 8 > i ;
    Integer< 16 > j ;
    Integer< 29 > k ;
    Integer< 64 > l ;
    printf("%d %d %d %d\n", sizeof(i), sizeof(j), sizeof(k), sizeof(l));
    return 0 ;
}

Dの方法

Dバージョンも再帰的テンプレートで書くことができるが、 より良い方法がある。 C++の例とは異なり、この例は 何が起こっているかを把握するのがかなり容易である。 コンパイルも速く、失敗した場合は意味のあるコンパイル時のメッセージを表示する。
import std.stdio;

template Integer(int nbits)
{
    static if (nbits <= 8)
        alias Integer = byte;
    else static if (nbits <= 16)
        alias Integer = short;
    else static if (nbits <= 32)
        alias Integer = int;
    else static if (nbits <= 64)
        alias Integer = long;
    else
        static assert(0);
}

int main()
{
    Integer!(8) i ;
    Integer!(16) j ;
    Integer!(29) k ;
    Integer!(64) l ;
    writefln("%d %d %d %d", i.sizeof, j.sizeof, k.sizeof, l.sizeof);
    return 0;
}

型トレイト

型トレイトとは、別の言い方をすれば、 コンパイル時に型の特性を特定できることを指す。

C++の方法

以下のテンプレートは、 C++ Templates: The Complete Guide(C++ テンプレート:完全ガイド)、David Vandevoorde、Nicolai M. Josuttis著、 353ページから引用したもので、テンプレートの引数の型が 関数であるかどうかを判断するものである。
template<typename T> class IsFunctionT
{
  private:
    typedef char One;
    typedef struct { char a[2]; } Two;
    template<typename U> static One test(...);
    template<typename U> static Two test(U (*)[1]);
  public:
    enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 };
};

void test()
{
    typedef int (fp)(int);

    assert(IsFunctionT<fp>::Yes == 1);
}
このテンプレートは、SFINAE(Substitution Failure Is Not An Error)の原則に基づいている。 なぜそれが機能するのかは、かなり高度なテンプレートに関するトピックである。

Dの方法

SFINAE(Substitution Failure Is Not An Error) は、テンプレート引数パターンマッチングに頼らずともDで実現できる。
template IsFunctionT(T)
{
    static if ( is(T[]) )
        const int IsFunctionT = 0;
    else
        const int IsFunctionT = 1;
}

void test()
{
    alias int fp(int);

    assert(IsFunctionT!(fp) == 1);
}
型が関数であるかどうかを調べる作業には、 テンプレートはまったく必要なく、また、無効な関数型の配列を作成しようとするようなごまかしも必要ない。 IsExpression式で直接テストできる。
void test()
{
    alias int fp(int);

    assert( is(fp == function) );
}

インターフェース

C++のインターフェイスは、インターフェイスを実装するクラスの動作や機能について記述するために使用される。 インターフェイスの宣言されたメソッドの特定の実装を決定することなく、

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

「C++インターフェイスは抽象クラスによって実装される。 抽象クラスは インターフェイスメソッドを純粋仮想関数として定義する。 インターフェイスを実装するクラスは 抽象クラスを継承し、その純粋仮想関数を実装する。」
class Interface {
  public:
    virtual int method() = 0;
};

class FirstVariant : public Interface {
  public:
    int method() {
      ...
    }
};

class SecondVariant : public Interface {
  public:
    int method() {
      ...
    }
};
インターフェイスは次のように使用される。
FirstVariant FirstVariant;
SecondVariant SecondVariant;

FirstVariant.method();
SecondVariant.method();

D言語におけるインターフェース

Dでは、Alexandrescu 2010, p. 212; Cehreli 2017, p. 347 に示されているインターフェースは、interface キーワードを使用して実装される。このキーワードは、主に未実装のメソッド宣言を含むクラスを導入する (非静的メソッド定義および非静的データメンバーは許可されない)。実装クラスは1つ以上のインターフェースを継承し、 インターフェースのメソッドを実装する。
interface Interface {
  int method();
}

class FirstVariant : Interface {
  int method() {
    ...
  }
}

class SecondVariant : Interface {
  int method() {
    ...
  }
}
インターフェースは次のように使用される。
FirstVariant FirstVariant = new FirstVariant();
SecondVariant SecondVariant = new SecondVariant();

FirstVariant.method();
SecondVariant.method();

D言語における参照

D言語には、"C++言語のスタイル"の参照という概念は存在しない。引数は参照によって渡すことができる。そのため、ref というキーワードが存在するが、"フリー"な参照は言語には存在しない。 foreach ループ変数のref は、概念的にはループ本体へのパラメータと考えることもできる。(opApply ベースの反復処理の場合、ループ本体は実際には関数に変換される。「プレーン」な反復処理の場合、コンパイラのASTには内部的に特別なref 変数が存在するが、言語からは見えない。) 以下のコードでは、d2gallery[0] の値のコピーであり、参照ではない。
module test;

void main()
{
    struct Data
    {
        int id;
    }

    import std.container.array : Array;
    Array!Data gallery;

    Data d1;
    gallery.insertBack(d1);

    auto d2 = gallery[0];
    d2.id = 1;
    assert(d2.id == gallery[0].id, "neither ref nor pointer");
}
このような場合、参照を取得しようとするのではなく、ポインタを取得する。結局のところ、D言語では-> 演算子を使用する必要はない。これは構文的にありがたい。
    auto d2 = &gallery[0];
xml-ph-0000@deepl.internalxml-ph-0000@deepl.internal