コンストラクタおよびその他の特殊関数

この章では構造体のみに焦点を当てているが、ここで説明するトピックは、ほとんどの場合、クラスにも適用される。違いについては、後の章で説明する。

構造体には、その型の基本的な操作を定義する4つのメンバー関数が特別にある。

さらに、新しく書くコードでは使用しないことが推奨される、レガシー関数もある。

これらの基本的な操作は、構造体では自動的に処理される。しかし、必要に応じて、異なる実装を提供するために、手動で定義することも可能だ。

コンストラクタ

コンストラクタの役割は、メンバーに適切な値を割り当てて、オブジェクトを使用できるように準備することだ。

コンストラクタは、前の章ですでに使用している。型名が関数のように使用される場合、実際にはコンストラクタが呼び出される。これは、次の行の右側で確認できる。

auto busArrival = TimeOfDay(8, 30);
D

同様に、次の行の右側ではクラスオブジェクトが構築されている:

auto variable = new SomeClass();
D

括弧内に指定されている引数は、コンストラクタのパラメータに対応している。例えば、上記の8と30は、TimeOfDayコンストラクタのパラメータとして渡されている。

これまで見てきたさまざまなオブジェクトの構築構文に加えて、constimmutable、およびsharedオブジェクトは、型コンストラクタ構文を使用して構築することもできる(例:immutable(S)(2))。(sharedキーワードについては、後の章で説明する。)

例えば、以下の3つの変数はすべてimmutableだが、変数aの構築は、変数bおよびcの構築とは意味的に異なる。

/* よりよく知られた構文; 変更可能な型の
 * 不変の変数: */
immutable a = S(1);

/* 型コンストラクタの構文; 不変の型の
 * 変数: */
auto b = immutable(S)(2);

/* 'b'と同じ意味 */
immutable c = immutable(S)(3);
D
コンストラクタの構文

他の関数とは異なり、コンストラクタには戻り値はない。コンストラクタの名前は、常にthisである。

struct SomeStruct {
    // ...

    this(/* コンストラクタのパラメータ */) {
        // ... オブジェクトを使用可能にする準備を行う操作 ...
    }
}
D

コンストラクタのパラメータには、有用で一貫性のあるオブジェクトを作成するために必要な情報が含まれる。

コンパイラが生成する自動コンストラクタ

これまで見てきた構造体はすべて、コンパイラによって自動的に生成されたコンストラクタを利用している。自動コンストラクタは、パラメータの値を、指定された順にメンバーに割り当てる。

.init構造体の章で覚えているように、末尾のメンバの初期値は指定する必要はない。指定されていないメンバは、それぞれの型のデフォルト値で初期化される。メンバの.init値は、そのメンバの定義で、=演算子の後に指定することができる。

struct Test {
    int member = 42;
}
D

また、パラメータ数の可変の章で説明した デフォルトのパラメータ値の機能も考慮すると、次のstructの自動コンストラクタは、次のthis()と同等になると考えられる。

struct Test {
    char   c;
    int    i;
    double d;

    /* コンパイラによって生成される
     * 自動コンストラクタと同等(注釈:これは説明のためだけのものだ;
     * 以下のコンストラクタは、Test()としてオブジェクトをデフォルト構築する場合、
     * 実際には呼び出されない。) */
    this(in char   c_parameter = char.init,
         in int    i_parameter = int.init,
         in double d_parameter = double.init) {
        c = c_parameter;
        i = i_parameter;
        d = d_parameter;
    }
}
D

ほとんどの構造体では、コンパイラが生成するコンストラクタで十分だ。オブジェクトを構築するには、各メンバーに適切な値を指定するだけで済む。

メンバーへのアクセスthis.

パラメーターとメンバーを混同しないように、上記のパラメーター名には_parameterが接尾辞として付加されている。そうしないとコンパイルエラーが発生する:

struct Test {
    char   c;
    int    i;
    double d;

    this(in char   c = char.init,
         in int    i = int.init,
         in double d = double.init) {
        // 'in'パラメータをそれ自身に割り当てようとしています!
        c = c;    // ← コンパイルエラー
        i = i;
        d = d;
    }
}
D

理由は、cだけではパラメーターを意味し、メンバーを意味しないため、上記のパラメーターはinとして定義されているため、変更できないからだ:

エラー: 変数deneme.Test.this.cはconstを変更できない

解決策は、メンバー名の前にthis.を付けることだ。メンバー関数内では、thisは"このオブジェクト"を意味し、this.cは"このオブジェクトの c メンバー"を意味する。

this(in char   c = char.init,
     in int    i = int.init,
     in double d = double.init) {
    this.c = c;
    this.i = i;
    this.d = d;
}
D

これで、cはパラメータを意味し、this.cはメンバーを意味するようになり、コードはコンパイルされ、期待どおりに動作する。メンバーcは、パラメータcの値で初期化される。

ユーザー定義のコンストラクタ

コンパイラが生成するコンストラクタの動作を説明した。このコンストラクタはほとんどのケースに適しているため、手動でコンストラクタを定義する必要はない。

しかし、オブジェクトの構築に、各メンバーに順番に値を代入するよりも複雑な操作が必要な場合もある。例として、前の章で説明したDurationを考えてみよう。

struct Duration {
    int minute;
}
D

この単一メンバー構造体には、コンパイラが生成するコンストラクタで十分だ。

time.decrement(Duration(12));
D

このコンストラクタは分単位の期間を受け取るため、プログラマーは時々計算を行う必要がある:

// 23時間18分前
time.decrement(Duration(23 * 60 + 18));

// 22時間20分後
time.increment(Duration(22 * 60 + 20));
D

これらの計算を不要にするため、2つのパラメーターを受け取り、自動的に計算を行うDurationコンストラクターを設計できる:

struct Duration {
    int minute;

    this(int hour, int minute) {
        this.minute = hour * 60 + minute;
    }
}
D

時間と分は個別のパラメータになったため、ユーザーは自分で計算を行う必要がなく、値を指定するだけで済む。

// 23時間18分前
time.decrement(Duration(23, 18));

// 22時間20分後
time.increment(Duration(22, 20));
D
メンバーへの最初の代入はコンストラクタ内で行われる

コンストラクタでメンバーの値を設定する場合、各メンバーへの最初の代入は特別に扱われる。そのメンバーの.init値に新しい値を代入する代わりに、最初の代入によってそのメンバーが実際に構築される。そのメンバーへのそれ以降の代入は、通常の代入操作として扱われる。

この特別な動作は、immutableおよびconstメンバーを、実行時にのみ知られる値で実際に構築するために必要だ。そうしないと、immutableおよびconst変数への代入は許可されないため、これらのメンバーを望ましい値に設定することができなくなる。

次のプログラムは、immutableメンバーに対して代入操作が一度だけ許可される方法を示している。

struct S {
    int m;
    immutable int i;

    this(int m, int i) {
        this.m = m;     // ← construction
        this.m = 42;    // ← 割り当て (変更可能なメンバーに対して可能)

        this.i = i;     // ← construction
        this.i = i;     // ← コンパイルエラー
    }
}

void main() {
    auto s = S(1, 2);
}
D
special_functions.1
ユーザー定義のコンストラクタは、コンパイラによって生成されたコンストラクタを無効にする

プログラマによって定義されたコンストラクタは、コンパイラによって生成されたコンストラクタの一部を無効にする。オブジェクトは、デフォルトのパラメータ値によって構築できなくなる。例えば、単一のパラメータによってDurationを構築しようとすると、コンパイルエラーになる。

time.decrement(Duration(12));    // ← コンパイルエラー
D

このコンパイルエラーは、プログラマのコンストラクタが単一のパラメータを受け取らないため、コンパイラによって生成されたコンストラクタが無効になっていることが原因だ。

解決策の1つは、単一のパラメーターを取る別のコンストラクターを提供してコンストラクターをオーバーロードすることだ:

struct Duration {
    int minute;

    this(int hour, int minute) {
        this.minute = hour * 60 + minute;
    }

    this(int minute) {
        this.minute = minute;
    }
}
D

ユーザー定義のコンストラクタは、{ }構文によるオブジェクトの構築も無効にする。

Duration duration = { 5 };    // ← コンパイルエラー
D

パラメーターを指定せずに初期化することは依然として有効である:

auto d = Duration();    // コンパイルできる
D

その理由は、Dでは、すべての型の.init値がコンパイル時に知られている必要があるためだ。上記のdの値は、Durationの初期値と同じである。

assert(d == Duration.init);
D
static opCallデフォルトコンストラクタの代わりに

すべての型の初期値はコンパイル時に知られていなければならないため、デフォルトコンストラクタを明示的に定義することはできない。

その型のオブジェクトが構築されるたびに、いくつかの情報を出力しようとする次のコンストラクタを考えてみよう。

struct Test {
    this() {    // ← コンパイルエラー
        writeln("A Test object is being constructed.");
    }
}
D

コンパイラの出力:

エラー: 構造体用のデフォルトコンストラクタdeneme.Deneme.thisは、
@disable指定があり、本体がない場合にのみ使用できる

注釈:後の章で、クラスのデフォルトコンストラクタを定義する方法について説明する。

この問題を回避するには、パラメータのないstatic opCall()を使用して、パラメータを指定せずにオブジェクトを構築することができる。これは、型の.init値には影響しないことに注意しよう。

これを機能させるには、static opCall()がその構造体型のオブジェクトを構築して返す必要がある。

import std.stdio;

struct Test {
    static Test opCall() {
        writeln("A Test object is being constructed.");
        Test test;
        return test;
    }
}

void main() {
    auto test = Test();
}
D
special_functions.2

main()内のTest()の呼び出しは、static opCall()を実行する:

テストオブジェクトが構築されている。

static opCall()内でTest()を記述することはできないことに注意。その構文はstatic opCall()を再度実行し、無限再帰を引き起こす。

static Test opCall() {
    writeln("A Test object is being constructed.");
    return Test();    // ← 'static opCall()'を再度呼び出す
}
D

出力:

テストオブジェクトが構築されている。
テストオブジェクトが構築されている。
テストオブジェクトが構築されている。
...    ← 同じメッセージが繰り返さる
他のコンストラクタの呼び出し

コンストラクタは、コードの重複を避けるために他のコンストラクタを呼び出すことができる。Durationはこの機能の有用性を示すには単純すぎるが、次の単一パラメータのコンストラクタは 2 パラメータのコンストラクタを活用している:

this(int hour, int minute) {
    this.minute = hour * 60 + minute;
}

this(int minute) {
    this(0, minute);    // 他のコンストラクタを呼び出す
}
D

分値のみを受け取るコンストラクタは、時間の値として0を渡して他のコンストラクタを呼び出す。

警告:上記のDurationのコンストラクタには設計上の欠陥がある。単一のパラメータでオブジェクトが構築される場合、意図が明確でないためだ:

// 10時間か10分か?
auto travelDuration = Duration(10);
D

ドキュメントや構造体のコードを読むことで、このパラメータが実際には"10分"を意味することを判断することは可能だが、2つのパラメータを持つコンストラクタの最初のパラメータは時間であるため、一貫性がない。

このような設計ミスはバグの原因となるため、絶対に避ける必要がある。

コンストラクタ修飾子

通常は、変更可能constimmutable、およびsharedオブジェクトには同じコンストラクタが使われる。

import std.stdio;

struct S {
    this(int i) {
        writeln("Constructing an object");
    }
}

void main() {
    auto m = S(1);
    const c = S(2);
    immutable i = S(3);
    shared s = S(4);
}
D
special_functions.3

意味的には、これらの式の右辺で構築されるオブジェクトはすべて変更可能で、変数だけが型修飾子が異なる。これらすべてに同じコンストラクタが使われる。

オブジェクトの構築
オブジェクトの構築
オブジェクトの構築
オブジェクトの構築

結果のオブジェクトの修飾子に応じて、一部のメンバーは別の方法で初期化する必要がある場合や、初期化する必要がない場合がある。例えば、immutableオブジェクトのメンバーは、そのオブジェクトの存続期間中、変更することはできないため、変更可能なメンバーを初期化しないままにしておくと、プログラムのパフォーマンスが向上する場合がある。

修飾子が異なるオブジェクトに対しては、修飾子付きのコンストラクタを異なる形で定義することができる:

import std.stdio;

struct S {
    this(int i) {
        writeln("Constructing an object");
    }

    this(int i) const {
        writeln("Constructing a const object");
    }

    this(int i) immutable {
        writeln("Constructing an immutable object");
    }

    // '共有'キーワードについては、後の章で説明する。
    this(int i) shared {
        writeln("Constructing a shared object");
    }
}

void main() {
    auto m = S(1);
    const c = S(2);
    immutable i = S(3);
    shared s = S(4);
}
D
special_functions.4

ただし、前述のように、右側の式はすべて意味的に変更可能であるため、これらのオブジェクトは、変更可能なオブジェクトのコンストラクタで構築される。

オブジェクトの構築
オブジェクトの構築    ← constコンストラクタではない
オブジェクトの構築    ← 不変コンストラクタではない
オブジェクトの構築    ← 共有コンストラクタではない

修飾子付きコンストラクタを利用するには、型コンストラクタ構文を使用する必要がある。(型コンストラクタという用語は、オブジェクトコンストラクタと混同しないように。型コンストラクタは、オブジェクトではなく型に関連している。) この構文は、修飾子と既存の型を組み合わせて、別の型を作成する。例えば、immutable(S)は、immutableSから作成された修飾子付き型である。

auto m = S(1);
auto c = const(S)(2);
auto i = immutable(S)(3);
auto s = shared(S)(4);
D

この場合、右側の式にあるオブジェクトは、それぞれmutableconstimmutablesharedと異なる。その結果、各オブジェクトは、それに対応するコンストラクタで構築される。

オブジェクトの構築
constオブジェクトの構築
不変オブジェクトの構築
共有オブジェクトの構築

予想通り、上記の変数はすべてautoキーワードで定義されているため、それぞれmutableconstimmutablesharedと正しく推論される。

コンストラクタパラメータの不変性

不変性の章では、参照型のパラメータをconstまたはimmutableのどちらで定義すべきかを決定するのは容易ではないことを見てきた。コンストラクタのパラメータについても同じ考慮事項が適用されるが、コンストラクタのパラメータには通常immutableを選択した方が良い。

その理由は、パラメーターを後で使用されるメンバーに代入することが一般的だからだ。パラメーターがimmutableでない場合、メンバーが使用されるまでに元の変数が変更されない保証はない。

ファイル名をパラメータとするコンストラクタを考えてみよう。ファイル名は、後で学生の成績を書き込むときに使用される。不変性に関する章のガイドラインに従って、より有用にするために、コンストラクタのパラメータはconst char[]と定義されていると仮定しよう。

import std.stdio;

struct Student {
    const char[] fileName;
    int[] grades;

    this(const char[] fileName) {
        this.fileName = fileName;
    }

    void save() {
        auto file = File(fileName.idup, "w");
        file.writeln("The grades of the student:");
        file.writeln(grades);
    }

    // ...
}

void main() {
    char[] fileName;
    fileName ~= "student_grades";

    auto student = Student(fileName);

    // ...

    /* ファイル名変数(fileName)が後で意図せず
     * 変更された場合(ここではすべての文字が
     * 'A'に設定されている): */
    fileName[] = 'A';

    // ...

    /* 成績が間違ったファイルに書き込まれる: */
    student.save();
}
D
special_functions.5

上記のプログラムは、学生の成績をA文字で構成されるファイル名に保存する。 "student_grades"。そのため、コンストラクタのパラメータや参照型のメンバーは、immutableと定義したほうがいい場合がある。これは、stringのようなエイリアスを使用することで、文字列の場合には簡単に実現できる。以下のコードは、変更が必要な構造体の部分を示している。

struct Student {
    string fileName;
    // ...
    this(string fileName) {
        // ...
    }
    // ...
}
D

これで、構造体のユーザーはimmutable文字列を指定する必要があり、その結果、ファイル名の混乱を防ぐことができる。

単一パラメータのコンストラクタによる型変換

単一パラメータのコンストラクタは、一種の型変換を提供するものと考えてもよい。コンストラクタのパラメータから、特定の構造体型のオブジェクトを生成する。例えば、次のコンストラクタは、stringからStudentオブジェクトを生成する。

struct Student {
    string name;

    this(string name) {
        this.name = name;
    }
}
D

to()また、 もこの動作castを変換として認識する。この例を見るために、次のsalute()関数を考えてみよう。Studentを期待している関数にstringパラメータを渡すと、当然、コンパイルエラーになる。

void salute(Student student) {
    writeln("Hello ", student.name);
}
// ...
    salute("Jane");    // ← コンパイルエラー
D

一方、以下のすべての行では、関数を呼び出す前にStudentオブジェクトが確実に構築される。

import std.conv;
// ...
    salute(Student("Jane"));
    salute(to!Student("Jean"));
    salute(cast(Student)"Jim");
D

tocastは、単一パラメーターコンストラクターを利用して、一時的なStudentオブジェクトを構築し、そのオブジェクトを引数としてsalute()を呼び出している。

デストラクタ

デストラクタには、オブジェクトのライフタイムが終了した際に実行する必要がある操作が含まれる。

コンパイラによって生成される自動デストラクタは、すべてのメンバーのデストラクタを順番に実行する。そのため、コンストラクタと同様、ほとんどの構造体ではデストラクタを定義する必要はない。

しかし、オブジェクトのライフタイムが終了したときに、特別な操作を実行する必要がある場合もある。例えば、オブジェクトが所有するオペレーティングシステムのリソースをシステムに返す必要がある場合、別のオブジェクトのメンバ関数を呼び出す必要がある場合、ネットワーク上のどこかで実行されているサーバーに、接続が切断される旨を通知する必要がある場合などだ。

デストラクタの名前は~thisで、コンストラクタと同様、戻り値の型はない。

デストラクタは自動的に実行される

デストラクタは、構造体オブジェクトのライフタイムが終了するとすぐに実行される。(newキーワードで構築されたオブジェクトの場合はそうではない。)

ライフタイムと基本操作の章で見たようにオブジェクトのライフタイムは、それが定義されているスコープを離れると終了する。構造体のライフタイムが終了するタイミングは、次の通りだ。

デストラクタの例

単純なXMLドキュメントを生成するための型を設計しよう。XML要素は、山括弧で定義される。XML要素には、データや他のXML要素が含まれる。XML要素には属性も持つことができるが、ここでは無視する。

<name>タグで開かれた要素は、必ず対応する</name>タグで閉じられるようにする:

  <class1>    ← 外側のXML要素を開く
    <grade>   ← 内側のXML要素を開く
      57      ← the data
    </grade>  ← 内側のXML要素を閉じる
  </class1>   ← 外側のXML要素を閉じる

上記の出力を生成できる構造体は、XML要素のタグと、それを出力する際に使用するインデントを格納する2つのメンバーによって設計できる。

struct XmlElement {
    string name;
    string indentation;
}
D

XML要素の開始と終了の責任をそれぞれコンストラクタとデストラクタに割り当てると、XmlElementオブジェクトのライフタイムを管理することで、希望する出力を生成することができる。例えば、コンストラクタは<tag>を出力し、デストラクタは</tag>を出力することができる。

以下のコンストラクタの定義は、開始タグを生成する:

this(string name, int level) {
    this.name = name;
    this.indentation = indentationString(level);

    writeln(indentation, '<', name, '>');
}
D

indentationString()は、次の関数だ。

import std.array;
// ...
string indentationString(int level) {
    return replicate(" ", level * 2);
}
D

この関数は、std.arrayモジュールからreplicate()を呼び出し、指定された値を指定された回数繰り返して構成される新しい文字列を作成して返す。

デストラクタはコンストラクタと同様に定義して、終了タグを生成するようにできる:

~this() {
    writeln(indentation, "</", name, '>');
}
D

以下のテストコードは、自動コンストラクタとデストラクタの呼び出しの効果を示すものだ:

import std.stdio;
import std.conv;
import std.random;
import std.array;

string indentationString(int level) {
    return replicate(" ", level * 2);
}

struct XmlElement {
    string name;
    string indentation;

    this(string name, int level) {
        this.name = name;
        this.indentation = indentationString(level);

        writeln(indentation, '<', name, '>');
    }

    ~this() {
        writeln(indentation, "</", name, '>');
    }
}

void main() {
    immutable classes = XmlElement("classes", 0);

    foreach (classId; 0 .. 2) {
        immutable classTag = "class" ~ to!string(classId);
        immutable classElement = XmlElement(classTag, 1);

        foreach (i; 0 .. 3) {
            immutable gradeElement = XmlElement("grade", 2);
            immutable randomGrade = uniform(50, 101);

            writeln(indentationString(3), randomGrade);
        }
    }
}
D
special_functions.6

XmlElementオブジェクトは、上記のプログラムでは3つの別々のスコープで作成されていることに注意。出力のXML要素の開始タグと終了タグは、XmlElementのコンストラクタとデストラクタによってのみ生成される。

<classes>
  <class0>
    <grade>
      72
    </grade>
    <grade>
      97
    </grade>
    <grade>
      90
    </grade>
  </class0>
  <class1>
    <grade>
      77
    </grade>
    <grade>
      87
    </grade>
    <grade>
      56
    </grade>
  </class1>
</classes>

<classes>要素は、classes変数によって生成される。その変数はmain()で最初に構築されるため、出力にはその構築結果が最初に含まれる。また、その変数は最後に破棄される変数でもあるため、main()を離れる際に、その破棄のためのデストラクタ呼び出しの結果が出力に最後に含まれる。

コピーコンストラクタ

コピーコンストラクタは、既存のオブジェクトのコピーとして新しいオブジェクトを作成する。

Sが構造体型であると仮定すると、オブジェクトがコピーされる場合は次の通りである。

デフォルトでは、コピーは、オブジェクトの対応するメンバーを順番にコピーすることで、コンパイラによって自動的に処理される。次の構造体定義と、existingObjectからコピーされる変数aがあると仮定しよう。

 struct S {
    int i;
    double d;
}

// ...

    auto existingObject = S();
    auto a = existingObject;    // コピーの構築
D

自動コピーコンストラクタは次の手順を実行する。

  1. a.iをコピーするexistingObject.i
  2. a.dをコピーするexistingObject.d

自動動作が適さない例としては、構造体の章で定義したStudent型がある。この型では、その型のオブジェクトのコピーに問題があった。

struct Student {
    int number;
    int[] grades;
}
D

スライスであるため、そのstructgradesメンバーは参照型である。Studentオブジェクトをコピーすると、元のオブジェクトとコピーの両方のgradesメンバーが、同じint型の実際の配列要素にアクセスできるようになる。その結果、一方のオブジェクトでグレードを変更すると、もう一方のオブジェクトでもその変更が反映される。

auto student1 = Student(1, [ 70, 90, 85 ]);

auto student2 = student1;    // コピーのコンストラクター
student2.number = 2;

student1.grades[0] += 5;     // この変更により、
                             // 2人目の生徒の成績も変更される:
assert(student2.grades[0] == 75);
D

このような混乱を避けるため、2つ目のオブジェクトのgradesメンバーの要素は、そのオブジェクトにのみ属する独立した要素でなければならない。このような特別なコピー動作は、コピーコンストラクタで実装されている。

コンストラクタであるため、コピーコンストラクタの名前もthisであり、戻り値の型はない。そのパラメータの型は、構造体と同じ型であり、refとして定義する必要がある。コピーのソースオブジェクトは変更してはならないため、パラメータにはconst(またはinout)を指定するのが適切だ。thisキーワードを補完するために、パラメータにthatという名前を付けて、"このオブジェクトはあのオブジェクトからコピーされている"ことを示すと便利である。

struct Student {
    int number;
    int[] grades;

    this(ref const(Student) that) {
        this.number = that.number;
        this.grades = that.grades.dup;
    }
}
D

このコピーコンストラクタはメンバーを1つずつコピーし、特にgradesの要素は.dupでコピーされるように注意する。その結果、新しいオブジェクトは配列要素の独自のコピーを取得する。

注釈:上記の"メンバーへの最初の代入は構築である"のセクションで説明したように、これらの代入操作は実際にはメンバーのコピー構築である。

最初のオブジェクトを通じて変更を加えても、2番目のオブジェクトには影響しなくなる:

student1.grades[0] += 5;
assert(student2.grades[0] == 70);
D

コードの可読性は低下するが、上記のコードのように構造体の型をStudentのように繰り返す代わりに、すべての構造体に対してパラメータ型をtypeof(this)と一般的に記述することができる。

this(ref const(typeof(this)) that) {
    // ...
}
D
Postblit

PostblitはDのレガシー機能で、使用は推奨されてない。新しく書くコードでは、代わりにコピーコンストラクタを使うべきだ。Postblitは下位互換性のために引き続き使用できるが、コピーコンストラクタとは互換性がない。ある型に対してpostblitが定義されている場合、コピーコンストラクタは無効になる。

Dでのオブジェクトのコピーのレガシーな方法は、2つのステップからなる:

  1. 既存のオブジェクトのメンバーを新しいオブジェクトにビットごとにコピーする。このステップは、ブロック転送の略でblitと呼ばれる。
  2. 新しいオブジェクトに追加の調整を行う。このステップはポストブリットと呼ばれる。

postblitの名前もthisで、戻り値の型はない。他のコンストラクタと区別するために、そのパラメータリストにはキーワードthisが含まれる。

this(this) {
    // ...
}
D

コピーコンストラクタとの主な違いは、既存のオブジェクトのメンバは、postblitの実行が開始される時点で既に新しいオブジェクトのメンバにコピー(blitted)されている点だ。さらに、postblitは新しいオブジェクトのメンバのみを使用して実行されるため、thatオブジェクトは存在しない。そのため、必要な(そして可能な)のは、新しいオブジェクトの調整のみだ。

Student構造体のポストブリット関数は、次のように記述できる。

struct Student {
    int number;
    int[] grades;

    this(this) {
        // この時点で'number'と'grades'はすでにコピーされている。
        // 単に要素のコピーを作成するだけで済む:
        grades = grades.dup;
    }
}
D
代入演算子

代入は、既存のオブジェクトに新しい値を与えることだ。

returnTripDuration = tripDuration;  // 代入
D

代入は他の特殊操作よりも複雑だ。なぜなら、実際には2つの操作の組み合わせだからだ:

しかし、この2つのステップをこの順序で実行すると、コピーが成功するかどうかがわかる前に元のオブジェクトが破棄されてしまうため、リスクがある。そうしないと、コピー操作中に例外がスローされ、左側のオブジェクトが完全に破棄され、コピーも完了しないという不整合な状態になってしまう可能性がある。

そのため、コンパイラが生成する代入演算子は、次の手順を適用して安全に動作する。

  1. 右側のオブジェクトを一時オブジェクトにコピーする

    これは、代入操作の実際のコピー作業である。左側のオブジェクトにはまだ変更がないため、このコピー操作中に例外がスローされても、左側のオブジェクトはそのまま残る。

  2. 左辺のオブジェクトを破壊する

    これは代入操作のもう一方の部分だ。

  3. 一時オブジェクトを左側のオブジェクトに転送する

    このステップ中または後にポストブリットやデストラクタは実行されない。その結果、左側のオブジェクトは一時オブジェクトと同等になる。

上記のステップが完了すると、一時オブジェクトは消え、右側のオブジェクトとそのコピー(つまり左側のオブジェクト)のみが残る。

コンパイラが生成する代入演算子は、ほとんどの場合に適しているが、プログラマが定義することもできる。その場合は、潜在的な例外を考慮し、例外がスローされた場合でも動作する代入演算子を記述すること。

代入演算子の構文は次の通りだ:

例として、代入演算子がメッセージを出力する単純なDuration構造体を考えてみよう。

struct Duration {
    int minute;

    Duration opAssign(Duration rhs) {
        writefln("minute is being changed from %s to %s",
                 this.minute, rhs.minute);

        this.minute = rhs.minute;

        return this;
    }
}
// ...
    auto duration = Duration(100);
    duration = Duration(200);          // 代入
D

出力:

分は100から200に変更される
他の型からの代入

structの型とは異なる型の値を代入したい場合がある。例えば、右辺にDurationオブジェクトを必要とする代わりに、整数から代入できると便利だ。

duration = 300;
D

これは、intをパラメーターとする別の代入演算子を定義することで可能になる:

struct Duration {
    int minute;

    Duration opAssign(Duration rhs) {
        writefln("minute is being changed from %s to %s",
                 this.minute, rhs.minute);

        this.minute = rhs.minute;

        return this;
    }

    Duration opAssign(int minute) {
        writefln("minute is being replaced by an int");

        this.minute = minute;

        return this;
    }
}
// ...
    duration = Duration(200);
    duration = 300;
D

出力:

分は100から200に変更される
分はintに置き換えられる

注釈:便利だが、異なる型を互いに代入すると、混乱やバグの原因になることがある。

メンバー関数の無効化

@disableとして宣言された関数は使用できない。

型のメンバーに適切なデフォルト値がない場合、そのデフォルトコンストラクタを無効にすることができる。例えば、次の型では、ファイル名が空であることは不適切だ。

struct Archive {
    string fileName;
}
D

残念ながら、コンパイラが生成するデフォルトコンストラクタは、fileNameを空に初期化してしまう:

auto archive = Archive();    // ← fileNameメンバーが空である
D

デフォルトコンストラクタは、@disableと宣言することで明示的に無効にすることができ、オブジェクトは他のコンストラクタのいずれかで構築されるようになる。無効にした関数には本体を指定する必要はない。

struct Archive {
    string fileName;

    @disable this();             // ← 呼び出せない

    this(string fileName) {      // ← 呼び出せる
        // ...
    }
}

// ...

    auto archive = Archive();    // ← コンパイルエラー
D

この場合、コンパイラはthis()の呼び出しを許可しない:

エラー: コンストラクタdeneme.Archive.thisは、@disableで注釈されているため
呼び出すことができない

Archiveのオブジェクトは、他のコンストラクタのいずれか、またはその.init値で明示的に構築する必要がある。

auto a = Archive("records");    // ← コンパイルできる
auto b = Archive.init;          // ← コンパイルできる
D

コピーコンストラクタ、postblit関数、および代入演算子も無効にすることができる。

struct Archive {
// ...

    // コピーコンストラクタを無効にする
    @disable this(ref const(typeof(this)));

    // postblitを無効にする
    @disable this(this);

    // 代入演算子を無効にする
    @disable typeof(this) opAssign(ref const(typeof(this)));
}

// ...

    auto a = Archive("records");
    auto b = a;                     // ← コンパイルエラー
    b = a;                          // ← コンパイルエラー
D

コピーコンストラクタとポストブリットを無効にすると、デストラクタが1回だけ実行すべき操作を実行する場合に役立つ。このような型のオブジェクトをコピーすると、複数のコピーに対してデストラクタが実行され、バグが発生する可能性がある。

例えば、次のデストラクタは、ロギングに使用するファイルに最後の"Finishing"というメッセージを書き込むことを意図している。

import std.stdio;
import std.datetime;

struct Logger {
    File file;

    this(File file) {
        this.file = file;
        log("Started");
    }

    ~this() {
        log("Finishing");    // ← 最後のメッセージとなる予定
    }

    void log(string message) {
        file.writefln("%s %s", Clock.currTime(), message);
    }
}

void main() {
    auto logger = Logger(stdout);

    logger.log("Working inside main");
    logger.log("Calling foo");
    foo(logger);
    logger.log("Back to main");
}

void foo(Logger logger) {
    logger.log("Working inside foo");
}
D
special_functions.7

プログラムの出力は、最終メッセージが複数回表示されるため、意図したとおりに動作していないことを示している:

2022-01-03 22:21:24.3143894開始した
2022-01-03 22:21:24.3144467main内で実行中
2022-01-03 22:21:24.3144628fooを呼び出し中
2022-01-03 22:21:24.3144767foo内で実行中
2022-01-03 22:21:24.3144906終了中
2022-01-03 22:21:24.3145035mainに戻った
2022-01-03 22:21:24.3145155終了中

この問題は、複数のLoggerオブジェクトが構築され、それぞれに対してデストラクタが実行されるために発生している。意図しない"Finishing"メッセージが早期に表示される原因となっているオブジェクトは、fooのパラメータである。このオブジェクトは、値でコピーされるため、コピーされる。

このような場合、最も簡単な解決策は、コピーと代入を完全に無効にすることだ。

struct Logger {
    @disable this(this);
    @disable this(ref const(typeof(this)));
    @disable Logger opAssign(ref const(typeof(this)));

    // ...
}
D

Loggerがコピーできなくなったため、fooのパラメーターを参照で受け取るように変更する必要がある:

void foo(ref Logger logger) {
     // ...
}
D
要約