コンストラクタ | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要

このページでは、クラスが持つ機能の1つであるコンストラクタを取り上げます。コンストラクタは、クラスのオブジェクトを初期化するために存在するメンバ関数の一種です。すでに前のページでも登場していますが、ここではコンストラクタに関連するさまざまな機能を、まとめて紹介します(ただしこれでも全てではありません)。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



コンストラクタ

コンストラクタ (constructor)は、クラスに定義されるメンバ関数の一種です。コンストラクタは「クラス」のページでも登場しましたが、最低限のことしか説明しませんでした。このページではコンストラクタに関連するさまざまな話題を取り上げます。

コンストラクタは、クラス型のオブジェクトが作られたときに自動的に呼び出され、ほかのタイミングで呼び出すことはできません。

Color color(255, 0, 0);  // Colorクラスのオブジェクトが作られ、コンストラクタが呼ばれる

クラスにはコンストラクタが必ず存在します。プログラマーが自分でコンストラクタを定義しなかった場合には、コンパイラが自動的に作っていますあとで取り上げます)。一方、クラスでない型にはコンストラクタはありません。

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

コンストラクタを自分で用意する場合は、次のようにクラス定義の中に宣言を記述します。

class クラス名 {
    クラス名(仮引数);
};

名前はクラスと同じにしなければならず、戻り値型の指定はありません。

定義については通常のメンバ関数と同様、クラスの内側にでも外側にでも記述できます。

class クラス名 {
    クラス名(仮引数)
    {
    }
};
クラス名::クラス名(仮引数)
{
}

アクセス指定子の影響を受けるので、ごく一般的なケースでは、public にする必要があります。

コンストラクタを constメンバ関数(「クラス」のページを参照)にすることはできませんが、const修飾されたオブジェクトであってもコンストラクタは呼び出されます。const修飾されたオブジェクトに対して呼び出されたとしても、コンストラクタ内での thisポインタ(「クラス」のページを参照)が constポインタにはなることもありません。

Canvasクラスに次のようなコンストラクタを定義することによって、キャンバスの大きさの指示を受けて、初期化するようにできます。

struct Color {
    unsigned char  red;
    unsigned char  green;
    unsigned char  blue;
};

class Canvas {
public:
    // コンストラクタ
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    Canvas(unsigned int width, unsigned int height);

private:
    std::vector<std::vector<Color>>     m_pixels;
    unsigned int                        m_width;
    unsigned int                        m_height;
};

Canvas::Canvas(unsigned int width, unsigned int height)
{
    assert(1 <= width);
    assert(1 <= height);

    m_width = width;
    m_height = height;

    m_pixels.resize(m_height);
    for (auto& row : m_pixels) {
        row.resize(m_width);
    }
}

このように明示的に定義したコンストラクタのことを、ユーザー定義のコンストラクタ (user provided constructor) と呼びます。

自動生成されるコンストラクタ

ユーザー定義のコンストラクタを定義しなかったクラスには、コンパイラが自動的(暗黙的)にコンストラクタを定義します。このときに生成されるコンストラクタは、public で、仮引数がなく、本体が空のものです。

class C {
    // 明示的にコンストラクタを定義していない場合、
    // コンパイラが自動的に定義している。
};

実質、以下と同等ということになります。

class C {
public:
    C()
    {
    }
};

これを記述するのであれば、このあと取り上げる「=default」を使うといいです。

=default

=defualt」は、明示的に定義しなかった場合にコンパイラが暗黙的に定義するメンバ関数を、明示的に記述する機能です。

明示的に定義しなかった場合にコンパイラが暗黙的に定義するメンバ関数はいくつか存在し、コンストラクタはその1つです。これらを総称して、特殊なメンバ関数 (special member funtions) といいます。また、これらが =default によって明示されると、明示的にデフォルト化された関数 (explicitly-defaulted functions) と呼ばれます。

【上級】特殊なメンバ関数は、コンストラクタ、コピーコンストラクタ、ムーブコンストラクタ、コピー代入演算子、ムーブ代入演算子、デストラクタです。

記法は次のとおりです。

class クラス名 {
    関数宣言 = default;
};

暗黙的なものを明示的にするだけなので、「関数宣言」の部分は、暗黙的に定義される場合と同じになるように書く必要があります。また、本体のコードは書けません。

class C {
public:
    // 暗黙のコンストラクタには仮引数がないので、明示する場合にも仮引数はあってはならない。
    C() = default;
};

【上級】ほかに =default が持つ効果として、inline や explicit のような関数宣言に付けるキーワードを追加できる点があります。

コンストラクタの呼び出し

クラスのオブジェクトを定義するときに、コンストラクタに渡す実引数を指定する必要があります。

Canvas canvas(320, 240);

コンストラクタに仮引数がない場合は () ごと省略するか、これまでのページでもやってきたように空の {} を使います。

class C {
public:
    C() = default;
};

C c1;
C c2 {};

このように、実引数なしで呼び出せるコンストラクタのことを、デフォルトコンストラクタ (default constructor)と呼びます。

「デフォルト」という言葉のイメージから、コンパイラが自動生成したコンストラクタのことをデフォルトコンストラクタと思ってしまうかもしれませんが、そういう意味ではありません。しかし、自動生成されたコンストラクタには仮引数がないので、デフォルトコンストラクタであるといえます。

あとで解説するデフォルト実引数によって、実引数を指定せずに呼び出せるようになっている場合も、デフォルトコンストラクタとみなせます。

コンストラクタに渡す実引数がないのであれば、空の () を使ってもよさそうに思えますが、C++ の構文上の問題があってうまくいきません。

#include <iostream>

class C {
public:
    C()
    {
        std::cout << "OK\n";
    }
};

int main()
{
    C c3();    // ???
}

実行結果:

構文を解釈するルール上、C c3(); という記述は、戻り値の型が C、名前が c3、仮引数なしの関数の宣言とみなされてしまいます。そのため、このサンプルプログラムの main関数は、c3 という関数を宣言しているだけで、ほかに何もしていませんから、実行結果にも何も現れません。

この問題を避けるために有効な方法が、これまでずっと使ってきた {} です。{} はコンストラクタの引数の有無に関わらず使用できます。

#include <iostream>

class C0 {
public:
    C0()
    {
        std::cout << "C0\n";
    }
};

class C1 {
public:
    C1(int n)
    {
        std::cout << "C1\n";
    }
};

class C2 {
public:
    C2(int n1, int n2)
    {
        std::cout << "C2\n";
    }
};


int main()
{
    C0 c0 {};
    C1 c1 {100};
    C2 c2 {100, 200};
}

実行結果:

C0
C1
C2

{} を使う方法(リスト初期化)なら、クラスでない型の初期化とも記法が統一できますし、暗黙の縮小変換を検出する効果もあるので(「std::vector」のページを参照)、今後は {} による方法を使うことにします。ただし、あとで取り上げるように、{}() を使い分ける必要があるケースも存在します。


配列の要素がクラス型の場合、要素1つ1つのコンストラクタに実引数を指定することはできず、各要素はデフォルトコンストラクタを使って初期化されます。そのため、配列の要素として使いたいクラスには、デフォルトコンストラクタが必須です。

class C {
public:
    // デフォルトコンストラクタではないコンストラクタを定義。
    // 暗黙のコンストラクタは定義されない。
    C(int v)
    {
    }
};

int main()
{
    C c_array[5] {};  // コンパイルエラー。デフォルトコンストラクタが必要
}

コンストラクタ初期化子

コンストラクタの本体でデータメンバに値を代入することで、データメンバが未初期化な状態を防げそうですが、この方法では「初期化」ではなく「代入」をしていることになります。最初の値が代入されるまでのあいだに、未初期化な期間ができます。

class C {
public:
    C()
    {
        // この時点では m_value は未初期化である

        m_value = 100;  // 最初の代入
    }

private:
    int m_value;
};

m_value がクラス型であれば、クラスC のオブジェクトが作られるときに m_value の型のコンストラクタが呼び出されるため、そちらで初期化されます。

そこで、コンストラクタ初期化子 (constructor_initializer) という機能を使用します。コンストラクタ初期化子は、コンストラクタの定義のところに記述して、データメンバに与える初期化子(メンバ初期化子 (member initializer))を指定します。

コンストラクタの定義をクラス定義内に記述するなら、次のようになります。

class クラス名 {
    クラス名(仮引数の並び) :
        メンバ初期化子
        , ...
    {
    }
};

クラス定義の外側に記述するなら、次のようになります。

クラス名::クラス名(仮引数の並び) :
    メンバ初期化子
    , ...
{
}

いずれにしても、: で区切ったあと、「メンバ初期化子」を必要な数だけ書き並べる構文になっています。「メンバ初期化子」の書き方は以下のいずれかです。

データメンバ名(初期値の並び)
データメンバ名{初期値の並び}

対象のデータメンバがクラス型の場合は、「初期値の並び」にコンストラクタに渡す実引数を書き並べます。


最初のコード例は次のように書き換えられます。

class C {
public:
    C() : m_value {100}
    {
        // 本体が開始された時点で、m_value は初期化済み
    }

private:
    int m_value;
};

コンストラクタ初期化子にメンバ初期化子をどのような順番で記述しても、実際に初期化が行われる順番には関係しないことに注意してください。データメンバの初期化順は、データメンバの宣言順と決められています。混乱を避けるためにも、メンバ初期化子はデータメンバの宣言順に合わせて記述するのが無難です。

class C {
public:
    C::C() : m_value2 {200}, m_value1 {100}  // 宣言順と異なる並びにするのは紛らわしい。
                                             // ここをどう書こうと、初期化順は m_value1 -> m_value2 である。
    {
    }

private:
    int m_value1;
    int m_value2;
};

このように初期化される順序は明確に規定されているので、ほかのデータメンバの値を利用して、後続のデータメンバを初期化することは可能です。また、ほかのデータメンバを使えるということは、thisポインタを経由したアクセスをしているということです。コンストラクタの本体のコードの実行を終えるまでオブジェクトの生成は完了していませんが、thisポインタを使うこと自体は可能であることを意味しています。

#include <iostream>

class C {
public:
    C(int value) :
        m_value1 {value},
        m_value2 {m_value1 * 2},  // m_value1 のほうが先に初期化されることを利用
        m_value3 {this->m_value1 + this->m_value2}  // (わざわざ書く必要はないが)this は使える
    {
    }

    void print() const
    {
        std::cout << m_value1 << "\n"
                  << m_value2 << "\n"
                  << m_value3 << "\n";
    }

private:
    int m_value1;
    int m_value2;
    int m_value3;
};

int main()
{
    C c {100};
    c.print();
}

実行結果:

100
200
300

データメンバ宣言時の初期化

データメンバを初期化する方法には、以前に「構造体」のページで紹介した、データメンバの宣言時に与える初期化子もあります。

class クラス名 {
    型名 データメンバ名 = 定数式;
    型名 データメンバ名 初期化子リスト;
};

={} を使った構文に限られ、() を使うことはできません。また必ず定数式を用いる必要があります。

() が使えないのは、メンバ関数を宣言する構文との区別が付かなくなるためです。

【上級】静的データメンバの場合には使えないという制約もあります。

次のようにして、データメンバに初期値を与えられます。

class C {
private:
    int m_id = 50;
    std::vector<int> m_values {0, 1, 2};
};

もし、メンバ初期化子のほうでも、同じデータメンバを初期化するようにコードを書いた場合は、メンバ初期化子のほうが有効になり、宣言時の初期化子は無視されます(評価自体をしない)1

#include <iostream>

class C {
public:
    C(int value) : m_value {value}
    {
    }

    void print() const
    {
        std::cout << m_value << "\n";
    }

private:
    int m_value {50};  // メンバ初期化子が優先され、ここで指定した 50 は無視される
};

int main()
{
    C c {100};
    c.print();
}

実行結果:

100

もし、メンバ初期化子を書くためだけにコンストラクタを定義しているのであれば(つまりコンストラクタの本体のコードが空になるのであれば)、データメンバ宣言時の初期化を利用することで、コンストラクタの定義をコンパイラに任せてしまえます。この方が効率的なコードになりうるとして、推奨しているガイドラインがあります2

構造体のときには、新C++編の最低動作確認環境である Visual Studio 2015 では集成体でなくなってしまう不便さがあることを理由に、データメンバ宣言時の初期化を避ける方針にしていましたが(「構造体」のページを参照)、クラスの場合には使わない理由は特にないので、今後はクラスに限っては使用していくことにします。

オーバーロード

同じスコープ内に、同じ名前の別の関数を作ることができます。この機能を、オーバーロード (overload) と呼びます。ここでいう「関数」には、メンバ関数やコンストラクタも含まれるので、コンストラクタも複数作ることが可能です。なお、オーバーロードされている各宣言のことを、オーバーロード宣言 (overloaded declarations) と呼びます。

変数や型の場合は、同じスコープ内に同じ名前で存在するとエラーになります。

オーバーロードの方法は単純で、必要なだけ関数の宣言と定義を書けばいいだけです。関数の名前が同じであれば、オーバーロードであるとみなされます。ただし、戻り値の型だけが異なる場合など、いくつか認められないケースが存在します。

Canvasクラスに、初期状態の色を指定できるコンストラクタを追加してみます。

struct Color {
    unsigned char  red;
    unsigned char  green;
    unsigned char  blue;
};

class Canvas {
public:
    // コンストラクタ
    // 初期状態は白。
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    Canvas(unsigned int width, unsigned int height);

    // コンストラクタ
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    // color: 初期状態の色
    Canvas(unsigned int width, unsigned int height, Color color);

    // 全面を塗りつぶす
    //
    // color: 色
    void fill(Color color)

private:
    std::vector<std::vector<Color>>     m_pixels;
    unsigned int                        m_width;
    unsigned int                        m_height;
};

Canvas canvas1 {200, 200};              // 仮引数が2つのコンストラクタを使う
Canvas canvas2 {200, 200, {0, 255, 0}}; // 仮引数が3つのコンストラクタを使う

コンストラクタがオーバーロードされている場合に、どのコンストラクタを呼び出すかは、実引数の型や個数から判断されます。正確なルールは非常に難解ですが3、基本的には「もっとも自然と思えるもの」が選ばれるようになっています。どれを呼び出すべきか判断できないときにはコンパイルエラーになります。

オーバーロードされたコンストラクタの本体のコードが、似たような内容になってしまうことがあります。さきほどの Canvasクラスのコンストラクタの本体を書いてみます。

Canvas::Canvas(unsigned int width, unsigned int height) :
    m_width{width},
    m_height{height}
{
    assert(1 <= m_width);
    assert(1 <= m_height);

    m_pixels.resize(m_height);
    for (auto& row : m_pixels) {
        row.resize(m_width);
    }

    // 初期状態は白
    fill({255, 255, 255});
}

Canvas::Canvas(unsigned int width, unsigned int height, Color color) :
    m_width{width},
    m_height{height}
{
    assert(1 <= m_width);
    assert(1 <= m_height);

    m_pixels.resize(m_height);
    for (auto& row : m_pixels) {
        row.resize(m_width, color);
    }
}

void Canvas::fill(Color color)
{
    for (auto& row : m_pixels) {
        std::fill(std::begin(row), std::end(row), color);
    }
}

resizeメンバ関数の第2引数は、要素が追加される場合にその要素に与える初期値を指定できます(「多次元配列」のページを参照)。

コードの重複は保守性を悪くしたり、単に書くのが面倒であったりします。解決策の1つには、privateメンバ関数に処理を抜き出して共通化を図る方法があります。

class Canvas {
public:
    // コンストラクタ
    // 初期状態は白。
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    Canvas(unsigned int width, unsigned int height);

    // コンストラクタ
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    // color: 初期状態の色
    Canvas(unsigned int width, unsigned int height, Color color);

private:
    void initialize(unsigned int width, unsigned int height, Color color);

private:
    std::vector<std::vector<Color>>     m_pixels;
    unsigned int                        m_width;
    unsigned int                        m_height;
};

Canvas::Canvas(unsigned int width, unsigned int height)
{
    initialize(width, height, {255, 255, 255});
}

Canvas::Canvas(unsigned int width, unsigned int height, Color color)
{
    initialize(width, height, color);
}

void Canvas::initialize(unsigned int width, unsigned int height, Color color)
{
    assert(1 <= width);
    assert(1 <= height);

    m_width = width;
    m_height = height;

    m_pixels.resize(m_height);
    for (auto& row : m_pixels) {
        row.resize(m_width, color);
    }
}

この方法は、追加の関数を作成する手間があることと、追加された関数自体はコンストラクタではないため、メンバ初期化子が記述できず、データメンバに代入するかたちで初期値を与えるコードになってしまう問題があります。こうした問題は、移譲コンストラクタを使うことで解決できます。

移譲コンストラクタ(委譲コンストラクタ)

メンバ初期化子を記述するところにクラスの名前を記述すると、そのクラスのほかのコンストラクタに処理を任せる(委譲する)ことができます。

class C {
public:
    C() : C{100}  // もう1つのコンストラクタに委譲
    {}

    C(int v) : m_value {v}
    {}

private:
    int m_value;
};

委譲が3段階以上になっても構いません。

日本語の「移譲」と「委譲」は意味合いが少し違っていますが、どちらの訳語もよく見かけます。

委譲を行う場合、データメンバに対するメンバ初期化子を記述することはできなくなります。

ほかのコンストラクタに処理を委譲しているコンストラクタのことを、移譲コンストラクタ(委譲コンストラクタ) (delegating constructor) と呼びます。移譲先のコンストラクタは、ターゲットコンストラクタ (target constuctor) と呼ばれます。また、最初に呼び出されるコンストラクタのことを、主コンストラクタ (principal constructor) と呼びます。

移譲コンストラクタが呼び出されると、ターゲットコンストラクタに移譲され、そちらのメンバ初期化子が処理されます。そして、ターゲットコンストラクタの本体のコードが実行されたあとで、移譲コンストラクタの本体のコードが実行されます。


さきほどの Canvasクラスのコードを、移譲コンストラクタを使って書き換えてみます。

struct Color {
    unsigned char  red;
    unsigned char  green;
    unsigned char  blue;
};

class Canvas {
public:
    // コンストラクタ
    // 初期状態は白。
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    Canvas(unsigned int width, unsigned int height);

    // コンストラクタ
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    // color: 初期状態の色
    Canvas(unsigned int width, unsigned int height, Color color);

private:
    std::vector<std::vector<Color>>     m_pixels;
    unsigned int                        m_width;
    unsigned int                        m_height;
};

Canvas::Canvas(unsigned int width, unsigned int height) : Canvas{width, height, {255, 255, 255}}
{
}

Canvas::Canvas(unsigned int width, unsigned int height, Color color) :
    m_width{width},
    m_height{height}
{
    assert(1 <= m_width);
    assert(1 <= m_height);

    m_pixels.resize(m_height);
    for (auto& row : m_pixels) {
        row.resize(m_width, color);
    }
}

新しいメンバ関数を追加する必要がなくなり、データメンバをメンバ初期化子を使って初期化できるようにもなりました。

デフォルト実引数

さきほどの Canvasクラスのコンストラクタの例は、もう1つ実装方法の選択肢があります。コンストラクタに限らず、関数の仮引数に与える値(つまり実引数)の指定を省略する方法があり、これを利用します。この機能は、デフォルト実引数 (default arguments) と呼ばれます。

関数を宣言するときに、仮引数の名前のうしろに ‘=’ と式を続けることで指定します。

戻り値の型 関数名(仮引数の型 仮引数の名前 =);

「式」を評価した結果がその仮引数に対するデフォルトの値となり、実引数を指定せずに呼び出された場合に使用されます。もちろん明示的に実引数を与えた場合はその値が使われます。実引数を渡すという行為は実行中に行われることなので、「式」が定数式である必要はありません。

デフォルト実引数がある仮引数のうしろに、デフォルト実引数がない仮引数が続くことは許されません。

void f(int a, int b = 10, int c);  // b にデフォルト実引数があるので、c にも必要

呼び出し時に実引数を省略できるのは、末尾に近い引数に対してだけです。

void f(int a = 0, int b = 10, int c = 20);

// 先頭や途中の実引数を省略することはできない
f(, 15, 25);
f(0, , 25);

また、実引数の並びに余分が ‘,’ が残ってしまうような呼び出し方は許されません。

void f(int a = 0, int b = 10, int c = 20);

f(5, 15, );

Canvasクラスのコンストラクタは、次のように1つにまとめられます。

struct Color {
    unsigned char  red;
    unsigned char  green;
    unsigned char  blue;
};

class Canvas {
public:
    // コンストラクタ
    //
    // width: 横方向のピクセル数
    // height: 縦方向のピクセル数
    // color: 初期状態の色。省略時は白
    Canvas(unsigned int width, unsigned int height, Color color = {255, 255, 255});

private:
    std::vector<std::vector<Color>>     m_pixels;
    unsigned int                        m_width;
    unsigned int                        m_height;
};

Canvas::Canvas(unsigned int width, unsigned int height, Color color) :
    m_width{width},
    m_height{height}
{
    assert(1 <= m_width);
    assert(1 <= m_height);

    m_pixels.resize(m_height);
    for (auto& row : m_pixels) {
        row.resize(m_width, color);
    }
}

デフォルト実引数を使う場合は関数そのものが1つになるので、当然ながら実装内容も1種類だけということになります。オーバーロードを使う場合は関数が複数に分かれるので、引数の型や個数に応じて実装内容を変更できますが、関数名は同じなので、利用者が混乱しないように注意が必要です。どちらの方法を選ぶこともできるケースでは、デフォルト実引数を選ぶほうが混乱は少ないでしょう4

初期化リストコンストラクタ

仮引数が std::initializer_list(「関数を作る」のページを参照)が1つだけの(あるいは後続にデフォルト実引数が続く)コンストラクタを初期化リストコンストラクタ (initializer-list constructor) と呼びます。

初期化リストコンストラクタがあれば、任意の個数の実引数を使って、リスト初期化(「構造体」のページを参照)を行えるようになります。

#include <initializer_list>
#include <vector>

struct Color {
    unsigned char  red;
    unsigned char  green;
    unsigned char  blue;
};

class Palette {
public:
    Palette(std::initializer_list<Color> colors) : m_colors{colors}
    {
        // ...
    }

private:
    std::vector<Color>  m_colors;
};

int main()
{
    Palette palette {
        {0, 0, 0},
        {255, 0, 0},
        {255, 255, 0},
        {0, 127, 255},
        {127, 127, 127},
    };
}

実行結果:

次のようにコンストラクタがオーバーロードされていたら、次の2つのオブジェクトの定義はどちらのコンストラクタを使うでしょうか?

class Palette {
public:
    Palette(Color color);
    Palette(std::initializer_list<Color> colors);
};

int main()
{
    Palette palette0 {};
    Palette palette1 {{255, 0, 0}};
}

palette0 で渡しているものは、空の初期化リストという扱いになります。そのため、初期化リストコンストラクタが使われることになります。初期化リストコンストラクタを定義する際には、要素が空である可能性も考慮しておかなければいけないということでもあります。

palette1 のほうは、初期化リストに含まれている要素は1つだけです。この場合、仮引数が Color型1つだけのコンストラクタが適合するようにもみえるかもしれませんが、リスト初期化の構文を使っているので、初期化リストコンストラクタが優先されて呼び出されることになります。

ここにデフォルトコンストラクタが加わると、結果に変化が起こります。

class Palette {
public:
    Palette();  // デフォルトコンストラクタを追加
    Palette(Color color);
    Palette(std::initializer_list<Color> colors);
};

int main()
{
    Palette palette0 {};  // デフォルトコンストラクタを使う
    Palette palette1 {{255, 0, 0}};  // 初期化リストコンストラクタを使う
}

リスト初期化のルールでは、{} の内側が空の場合が特別扱いされており、呼び出せるデフォルトコンストラクタがあるなら、そちらが使用されることになっています5。そのため、palette0 はデフォルトコンストラクタを使うように変化します。

また、{} の内側が空でない場合には、初期化リストコンストラクタが「優先的」に使用されますが、初期化リストコンストラクタが存在しなければ、{}() とみなして、ほかの使用可能なコンストラクタを探して使おうとします6

class Palette {
public:
    Palette();
    Palette(Color color);
};

Palette palette1 {{255, 0, 0}};  // ({255, 0, 0}) とみなして、Color1つのコンストラクタを使う

このように、リスト初期化の構文を使ったからといって、必ずしも初期化リストコンストラクタが使用されるわけではありません。反対に、リスト初期化ではないのに初期化リストコンストラクタが使われることはありません。

ルールが複雑であるため、呼び出されるコンストラクタが思っていたものと違っていたという事故が起こることがあります。std::vector もこの事故を起こし得るものです。

std::vector<int> v {10, 20};  // 要素が 10 と 20 の初期化リストコンストラクタを呼ぶのか、
                              // 要素数と初期値を指定するコンストラクタを呼ぶのか?

初期化時に使う括弧を {} で統一したいと思っている人なら、要素数と初期値を指定するコンストラクタを使っているつもりで {10, 20} と書いた可能性があります。しかし、std::vector には初期化リストコンストラクタが定義されているため、呼び出されるのは初期化リストコンストラクタの方です。したがって、要素が 10 と 20 の vector が作られます。要素数が 10、それぞれの値が 20 になることを望むのなら、() を使って、初期化リストコンストラクタではないコンストラクタを使わせる必要があります。

std::vector<int> v (10, 20);  // 要素数と初期値を指定するコンストラクタを呼ぶ

変換コンストラクタ

仮引数があるコンストラクタは、実引数の型をクラス型へ型変換するという見方ができます。

class Money {
public:
    Money(int amount) : m_amount{amount}
    {}

private:
    int  m_amount;
};

int main()
{
    Money m {1000};  // int から Money に型変換
}

このようなコンストラクタのうち、このあと紹介する explicit指定子を指定していないコンストラクタは、変換コンストラクタ(型変換コンストラクタ) と呼ばれます。引数が複数あっても、複数の型の組み合わせをクラス型に変換していると考えることが可能なので、これも変換コンストラクタといえます7

型変換コンストラクタを定義していると、ほかの場面でも暗黙の型変換を働かせることができます。

#include <iostream>

class Money {
public:
    Money(int amount) : m_amount{amount}
    {}

    inline int get_amount() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

void print_amount(Money money)
{
    std::cout << "\\" << money.get_amount() << "\n";
}

Money get_default_amount()
{
    return 1000;  // 戻り値型は Money型だが、int を返そうとしている
}

int main()
{
    Money m {1000};  // int から Money に型変換

    print_amount(2000);  // 仮引数は Money型だが、int を渡している
    m = get_default_amount();
}

実行結果:

\2000

explicit

変換コンストラクタがかえって、コードを分かりづらくしてしまう場合があります。

#include <iostream>
#include <initializer_list>

class Money {
public:
    Money(int amount) : m_amount{amount}
    {}

    // コンストラクタ
    // 合計額を保持するように初期化する。
    Money(std::initializer_list<int> amount_vec);

    inline int get_amount() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

Money::Money(std::initializer_list<int> amount_vec) : m_amount{0}
{
    for (int amount : amount_vec) {
        m_amount += amount;
    }
}

void print_amount(Money money)
{
    std::cout << "\\" << money.get_amount() << "\n";
}

int main()
{
    print_amount({400, 200, 100});
}

実行結果:

\700

初期化リストコンストラクタでもある新たな変換コンストラクタが追加されたことで、std::initializer_list<int> から Money型への暗黙的な変換が働くようになりました。ここでは、初期化子リスト内の各要素の合計額にするという変換が行われるようになっていますが、print_amount({400, 200, 100}); という呼び出しから、合計を出力していることを読み取るのは難しそうです。おそらく3つの金額を出力するように見えるでしょう。print_amount関数を作った人の立場に立てば、Money型の引数を受け取って、その値をそのまま出力するだけのつもりですから、print_amount という命名が問題というわけでもありません。

この例では、複数の金額を指定して初期化すると合計額になるという作り自体が有用なのかを検討するべきではありますが、この初期化コンストラクタ自体は活かすとするのなら、Moneyクラスのオブジェクトを定義するときにだけ使えるものであって欲しいといえます。

Money m {400, 200, 100};       // これはできて欲しい
print_amount({400, 200, 100}); // これはできないで欲しい

この要求を満たすには、変換コンストラクタの宣言に explicit指定子を付加します。

explicit 戻り値の型 コンストラクタ名(仮引数の並び);


#include <iostream>
#include <initializer_list>

class Money {
public:
    Money(int amount) : m_amount{amount}
    {}

    explicit Money(std::initializer_list<int> amount_vec);

    inline int get_amount() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

Money::Money(std::initializer_list<int> amount_vec) : m_amount{0}
{
    for (int amount : amount_vec) {
        m_amount += amount;
    }
}

void print_amount(Money money)
{
    std::cout << "\\" << money.get_amount() << "\n";
}

int main()
{
    Money m {400, 200, 100}; // OK
    print_amount(m);

    // 以下はコンパイルエラーになる
//  print_amount({400, 200, 100});
}

実行結果:

\700

explicit指定子が付加されたコンストラクタ(explicitコンストラクタ)は、直接初期化か、明示的にキャストを行った場合にだけしか使用されません。

直接初期化 (direct-initialization) とは、以下のいずれかの方法による初期化のことです8(未解説なものも含んでいます)。

これに対して、以下のような初期化はコピー初期化 (copy-initialization) と呼ばれます9(未解説なものも含んでいます)。

【上級】コピー初期化といっても、必ずしもコピーが行われるとは限らず、ムーブになる場合もあります。

暗黙の型変換を許可したいと思う場合を除き、仮引数を1つだけもつコンストラクタには explicit を付加すると安全性が高まるといえます10。int から Money への暗黙の型変換にはそれなりに価値があるといえるかもしれません。一方で、たとえば Color から Pen への暗黙の型変換は機能しないほうが良いといえます。

class Pen {
public:
    explicit Pen(Color color);
};

void select_pen(Pen pen);

Color red {255, 0, 0}
select_pen(red);  // エラー。ペンを選択する関数に色を渡すのは不自然だろう

【上級】仮引数が1つであっても、コピーコンストラクタやムーブコンストラクタに explicit を付加するのは単に使いづらくなるだけなので、これらは例外的であるといえます。

変換関数

変換コンストラクタと似た機能に、変換関数(型変換関数) (conversion function) があります。

変換関数は、クラスからほかの型へ変換する方法を記述したメンバ関数です。変換コンストラクタは、ある型からクラスへの変換を記述しているので、変換の方向が逆向きになっています。また、変換関数は変換元のクラスのメンバ関数として記述し、変換コンストラクタは変換先のクラスの(特殊な)メンバ関数として記述します。

変換関数の宣言方法はやや特殊です。

operator 型名();

「型名」の部分に変換先の型名を記述します。ここに指定した型への変換が必要になったとき、この変換関数がアクセスできる場所にあれば、変換関数が呼び出されます。変換結果は戻り値として返されますが、宣言には戻り値の型は記述しません。また、仮引数もありません

constメンバ関数にすることは可能です。

さきほどの Moneyクラスには、int型で表現した金額を返す get_amountメンバ関数がありますが、これを変換関数で記述すれば、Money型から int型への型変換が暗黙的に行えるようになります。

#include <iostream>

class Money {
public:
    // int から Money への変換コンストラクタ
    explicit Money(int amount) : m_amount{amount}
    {}

    // Money から int への変換関数
    inline operator int() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

int main()
{
    Money m {500};

    int amount {m};  // OK. 変換関数による型変換
    std::cout << "\\" << amount << "\n";
}

実行結果:

\500

変換関数の濫用はコードを分かりづらくする恐れもあります。変換関数があったほうが明らかに便利で、明確でもあるといえる状況でなければ使用を控えたほうがいいでしょう。このサンプルでいうと、get_amount というメンバ関数を使ったほうが意図も明確であるといえます。

なお、変換関数にも explicit指定子を付加することができます。explicit指定子が付加された変換関数は、直接初期化か、明示的にキャストを行った場合にだけしか使用されません

#include <iostream>

class Money {
public:
    explicit Money(int amount) : m_amount{amount}
    {}

    // Money から int への explicit 変換関数
    inline explicit operator int() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

int main()
{
    Money m {500};

    int amount1 {m};  // OK
    int amount2 = m;  // コンパイルエラー
    int amount3 = static_cast<int>(m);  // OK
}

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のように定義されたクラスがあります。

class C {
public:
    C(int a)
    {}

    C(int a1, int a2)
    {}
};

このとき、以下のそれぞれの変数定義はどのような結果になりますか?

C c1;
C c2(100);
C c3(10, 20);

解答・解説

問題2 (確認★)

問題1と同じ要領で、次の場合はどのような結果になりますか?

class C {
public:
    C() = default;

    C(int a)
    {}

    C(int a1, int a2)
    {}
};

C c1;
C c2(100);
C c3(10, 20);
C c4 {};
C c5 {100};
C c6 {10, 20};

解答・解説

問題3 (確認★)

問題1~2と同じ要領で、次の場合はどのような結果になりますか?

class C {
public:
    C(int a = 0)
    {}

    C(int a1, int a2, int a3 = 1000)
    {}
};

C c1;
C c2(100);
C c3(10, 20);
C c4 {};
C c5 {100};
C c6 {10, 20};
C c7 {10, 20, 30};

解答・解説

問題4 (確認★)

問題1~3と同じ要領で、次の場合はどのような結果になりますか?

class C {
public:
    C(int a = 0)
    {}

    C(int a1, int a2, int a3 = 1000)
    {}

    C(std::initializer_list<int> lst)
    {}
};

C c1;
C c2(100);
C c3(10, 20);
C c4 {};
C c5 {100};
C c6 {10, 20};
C c7 {10, 20, 30};

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る