コンストラクタの詳細

初期化リスト、委譲、explicit キーワード。オブジェクト初期化の奥深さ。

コンストラクタの役割

コンストラクタは、オブジェクト作成時に呼ばれ、 メンバ変数を初期化し、必要なリソースを確保します。

効率的で安全なコンストラクタの実装は、クラス設計の基盤です。

初期化リスト(Member Initializer List)

初期化リストを使用することで、 メンバ変数をより効率的に初期化できます。

#include <iostream>
#include <string>

class Person {
private:
    std::string name;   // クラス型
    int age;            // 基本型
    const int id;       // const メンバ
    
public:
    // 初期化リストなし:非効率
    Person(const std::string& n, int a, int i) {
        name = n;  // デフォルト初期化後に代入
        age = a;
        // id = i;  // エラー! const メンバは代入不可
    }
};

// 初期化リストあり:効率的かつ安全
class GoodPerson {
private:
    std::string name;
    int age;
    const int id;
    
public:
    GoodPerson(const std::string& n, int a, int i)
        : name(n), age(a), id(i) {  // 直接初期化
        std::cout << "GoodPerson 作成: " << name << std::endl;
    }
};

int main() {
    GoodPerson p("太郎", 25, 12345);
    return 0;
}

GoodPerson 作成: 太郎

初期化リストの順序

初期化リストの実行順序は、 クラス定義での メンバ宣言順です。 リストの順序ではありません。

#include <iostream>

class Example {
private:
    int x;
    int y;
    
public:
    // 注意: 実行順序は x → y (宣言順)
    // リスト順序の y → x ではない
    Example(int a, int b) : y(b), x(a) {  // 見づらい!
        std::cout << "x=" << x << ", y=" << y << std::endl;
    }
};

// 良い実装:宣言順と合わせる
class GoodExample {
private:
    int x;
    int y;
    
public:
    GoodExample(int a, int b) : x(a), y(b) {  // 明確
        std::cout << "x=" << x << ", y=" << y << std::endl;
    }
};

int main() {
    Example e(10, 20);
    GoodExample g(10, 20);
    return 0;
}

x=10, y=20
x=10, y=20

複数のコンストラクタ(オーバーロード)

同じクラスに複数のコンストラクタを定義できます。 異なる初期化方法を提供します。

#include <iostream>
#include <string>

class Account {
private:
    std::string owner;
    double balance;
    int pin;
    
public:
    // コンストラクタ 1: すべてのパラメータ
    Account(const std::string& o, double b, int p)
        : owner(o), balance(b), pin(p) {
        std::cout << "Account 1: " << owner << " 残高=" << balance << std::endl;
    }
    
    // コンストラクタ 2: PIN なし(デフォルト 1234)
    Account(const std::string& o, double b)
        : owner(o), balance(b), pin(1234) {
        std::cout << "Account 2: " << owner << " 残高=" << balance << std::endl;
    }
    
    // コンストラクタ 3: 名義だけ(残高 0)
    explicit Account(const std::string& o)
        : owner(o), balance(0), pin(1234) {
        std::cout << "Account 3: " << owner << " (新規)" << std::endl;
    }
    
    void display() const {
        std::cout << "  " << owner << ": " << balance << "円" << std::endl;
    }
};

int main() {
    Account a1("太郎", 100000, 5678);  // コンストラクタ 1
    Account a2("花子", 50000);         // コンストラクタ 2
    Account a3("次郎");                // コンストラクタ 3
    
    a1.display();
    a2.display();
    a3.display();
    
    return 0;
}

Account 1: 太郎 残高=100000
Account 2: 花子 残高=50000
Account 3: 次郎 (新規)
太郎: 100000円
花子: 50000円
次郎: 0円

委譲コンストラクタ (Delegating Constructor - C++11)

委譲コンストラクタにより、 あるコンストラクタが別のコンストラクタを呼び出せます。 重複コードを削減します。

#include <iostream>
#include <vector>

class Vector3D {
private:
    double x, y, z;
    
public:
    // メインコンストラクタ
    Vector3D(double x_, double y_, double z_)
        : x(x_), y(y_), z(z_) {
        std::cout << "Vector3D(" << x << ", " << y << ", " << z << ")" << std::endl;
    }
    
    // 委譲: ゼロベクトル作成
    Vector3D() : Vector3D(0, 0, 0) {
        std::cout << "  (ゼロベクトル)" << std::endl;
    }
    
    // 委譲: XY平面上のベクトル
    Vector3D(double x_, double y_) : Vector3D(x_, y_, 0) {
        std::cout << "  (XY平面)" << std::endl;
    }
    
    // 委譲: 単位ベクトル
    explicit Vector3D(int axis) : Vector3D(
        axis == 0 ? 1 : 0,
        axis == 1 ? 1 : 0,
        axis == 2 ? 1 : 0) {
        std::cout << "  (単位ベクトル軸" << axis << ")" << std::endl;
    }
};

int main() {
    Vector3D v1(3, 4, 5);     // メイン
    Vector3D v2;               // ゼロベクトル
    Vector3D v3(1, 2);         // XY平面
    Vector3D v4(0);            // X軸方向単位ベクトル
    
    return 0;
}

Vector3D(3, 4, 5)
Vector3D(0, 0, 0)
(ゼロベクトル)
Vector3D(0, 0, 0)
(XY平面)
Vector3D(1, 0, 0)
(単位ベクトル軸0)

explicit キーワード

explicit キーワードは、 暗黙的な型変換を防ぎます。 意図しないコンストラクタ呼び出しを防止します。

#include <iostream>
#include <string>

class String {
private:
    std::string data;
    
public:
    // explicit なし:危険
    String(const std::string& s) : data(s) {}
    
    void print() const {
        std::cout << data << std::endl;
    }
};

// explicit あり:安全
class SafeString {
private:
    std::string data;
    
public:
    explicit SafeString(const std::string& s) : data(s) {}
    
    void print() const {
        std::cout << data << std::endl;
    }
};

void processString(const String& s) {
    std::cout << "処理中: ";
    s.print();
}

void processSafeString(const SafeString& s) {
    std::cout << "安全処理中: ";
    s.print();
}

int main() {
    // explicit なし:暗黙的変換
    processString("Hello");  // String("Hello") と自動変換
    
    // explicit あり:エラー
    // processSafeString("Hello");  // コンパイルエラー
    
    // 明示的に変換
    processSafeString(SafeString("Hello"));
    
    return 0;
}

処理中: Hello
安全処理中: Hello

デフォルトコンストラクタ

デフォルトコンストラクタは、引数がないコンストラクタです。 配列やコンテナ要素の初期化に使われます。

#include <iostream>
#include <vector>

class Counter {
private:
    int value;
    
public:
    // デフォルトコンストラクタ(引数なし)
    Counter() : value(0) {
        std::cout << "Counter 作成 (value=0)" << std::endl;
    }
    
    // 通常コンストラクタ
    Counter(int v) : value(v) {
        std::cout << "Counter 作成 (value=" << v << ")" << std::endl;
    }
    
    int getValue() const { return value; }
};

int main() {
    // デフォルトコンストラクタが呼ばれる
    Counter c1;
    Counter c2[3];  // 配列は各要素でデフォルトコンストラクタ呼び出し
    
    std::vector<Counter> v(5);  // 5 個の Counter をデフォルト初期化
    
    // 通常コンストラクタ
    Counter c3(100);
    
    return 0;
}

Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=0)
Counter 作成 (value=100)

= default と = delete (C++11)

コンストラクタを明示的に生成または削除することで、 意図を明確にします。

#include <iostream>

class Point {
private:
    int x, y;
    
public:
    // 点 (x, y) を初期化
    Point(int x_, int y_) : x(x_), y(y_) {}
    
    // デフォルトコンストラクタを明示的に用意
    Point() = default;  // = default で生成
    
    void print() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

// コピー不可なクラス
class Unique {
private:
    int id;
    
public:
    Unique(int i) : id(i) {}
    
    // コピーコンストラクタを削除
    Unique(const Unique&) = delete;
    
    // コピー代入演算子を削除
    Unique& operator=(const Unique&) = delete;
    
    int getId() const { return id; }
};

int main() {
    Point p1;       // デフォルトコンストラクタ(= default で生成)
    Point p2(3, 4);
    
    p1.print();
    p2.print();
    
    Unique u1(1);
    // Unique u2 = u1;  // エラー! = delete
    
    return 0;
}

(0, 0)
(3, 4)

メンバの in-class初期化 (C++11)

メンバ変数をクラス定義時にデフォルト値で初期化できます。 すべてのコンストラクタで使用されます。

#include <iostream>
#include <string>

class Config {
private:
    std::string name = "unnamed";  // in-class 初期化
    int port = 8080;
    bool debug = false;
    
public:
    // デフォルト値を使用
    Config() {
        std::cout << "デフォルト: " << name << ":" << port << std::endl;
    }
    
    // 一部をオーバーライド
    Config(const std::string& n) : name(n) {
        // port と debug は in-class 初期値を使用
        std::cout << "名前指定: " << name << ":" << port << std::endl;
    }
    
    // すべてをオーバーライド
    Config(const std::string& n, int p, bool d)
        : name(n), port(p), debug(d) {
        std::cout << "全指定: " << name << ":" << port << std::endl;
    }
};

int main() {
    Config c1;                        // すべてデフォルト
    Config c2("MyApp");              // 名前のみ指定
    Config c3("API", 3000, true);    // すべて指定
    
    return 0;
}

デフォルト: unnamed:8080
名前指定: MyApp:8080
全指定: API:3000

よくある誤り

誤り1: 初期化リストの順序を無視

class Bad {
private:
    int x;
    int* px;
public:
    Bad(int v) : px(new int(0)), x(v) {  // 実行順: x → px (宣言順)
        *px = x;  // px が初期化される前に x が使用される可能性
    }
};

誤り2: const メンバをコンストラクタ本体で初期化

class Bad {
private:
    const int id;
public:
    Bad(int i) {
        id = i;  // エラー! const メンバ
    }
};

// 正しい
class Good {
private:
    const int id;
public:
    Good(int i) : id(i) {}  // 初期化リストで
};

誤り3: explicit を忘れて暗黙的変換を許可

class Bad {
public:
    Bad(int size) { }  // explicit がない
};

void process(const Bad& b) { }

int main() {
    process(100);  // Bad(100) に暗黙的変換(意図不明)
}

ポイント

  • 初期化リスト:メンバを効率的に初期化(const メンバ対応)
  • 複数コンストラクタ:異なる初期化方法を提供
  • 委譲コンストラクタ:重複を削減(C++11)
  • explicit:暗黙的変換を防止
  • デフォルコンス:配列・コンテナで必須
  • in-class初期化:便利だが乱用に注意(C++11)

やってみよう

練習1: 複数のコンストラクタを持つクラスを設計

練習2: 初期化リストの順序による問題をテスト

練習3: explicit を使った暗黙的変換防止

チャレンジ: 委譲コンストラクタで重複を削減したクラス

まとめ

  • 初期化リストはコンストラクタの必須スキル
  • 複数のコンストラクタで柔軟な初期化方法を提供
  • explicit で暗黙的変換による誤りを防止
  • C++11 の機能(委譲、= default)で効率化