C++ 変換コンストラクタ【暗黙のコンストラクタ呼び出し、変換時の処理コスト】

変換コンストラクタ

実引数を一つだけとるようなコンストラクタは変換コンストラクタ(converting constructor)と呼ばれる。変換コンストラクタが定義されたクラスでは、暗黙的なコンストラクタ呼び出しによる初期化が行えるようになる。

struct T {
  int i;
  T(int v) : i(v) {} // 変換コンストラクタ
};

T a = 9; // 暗黙的なコンストラクタ呼び出し
T b(9);  // 明示的なコンストラクタ呼び出し

// ↑ いずれも`T(int v)`のコンストラクタが呼ばれる

このように、暗黙の型変換を実現することができる。

なお、変換コンストラクタが定義されたクラスでは、デフォルトコンストラクタの暗黙的な定義が行われなくなるため、必要に応じてデフォルトコンストラクタを明示的に定義しなければならない。

struct T {
   T(int v) {}
   T() {}          // 明示的に定義
// T() = default;  // C++11では明示的なデフォルト定義も可能
// T(int v = 0) {} // デフォルト引数で対処することも可能(後に解説)
};

T t(); // ok

目次

戻り値や引数で利用することも可能

暗黙の変換コンストラクタ呼び出しは、戻り値を返す際や関数呼出し時の実引数内でも行われる。

struct T { T(int v) {} }; // 変換コンストラクタ`T(int v)`を定義

T fn(T a) {
  return 8; // 変換コンストラクタが呼ばれる(戻り値型への変換)
}

int main() {
  fn(9); // 変換コンストラクタが呼ばれる(実引数型への変換)
  static_cast<T>(7); // 変換コンストラクタが呼ばれる(明示的なキャスト)
}

デフォルトコンストラクタとして機能させる方法

変換コンストラクタの仮引数にデフォルト引数を指定することによって、実質的にデフォルトコンストラクタとしても機能させることができる。

struct T {
  T(int i = 0) {}
};

T t = 9; // 変換コンストラクタとして機能
T u();   // デフォルトコンストラクタとしても機能
         // `T(0)`相当の呼び出しが行われる

ただし、この場合はデフォルトコンストラクタの明示的な定義は正常に機能しなくなってしまう。両機能が重複してしまうためである。

struct T {
  T(int i = 0) {}
  T() {}
};

T(); // error: Call to constructor of 'T' is ambiguous
もっともC++では、コンストラクタを独自定義した際にデフォルトコンストラクタの暗黙的な定義が行われなくなってしまうという性質があるが、上記のデフォルト引数の活用によってデフォルトコンストラクタと変換コンストラクタを一括に定義して対処することもできる。

デフォルト引数と暗黙的な変換コンストラクタ呼出し

なお、引数が二つ以上のコンストラクタであっても、二つ目以降の仮引数にデフォルト引数を指定することによって、変換コンストラクタの暗黙的な呼び出しを行わせることができる。

struct T {
  int i;
  T(int a, int b = 1) : i(a * b) {}
};

T v = 9; // OK

当然、デフォルト引数を利用する際にはコンストラクタの重複定義に注意する必要がある。

struct T {
  T(int a) {} // 余計な定義になってしまう
  T(int a, int b = 1) {}
};

T v = 9; // エラー:Conversion from 'int' to 'T' is ambiguous

複数の引数を取る変換コンストラクタ

複数の引数を取る変換コンストラクタは、波括弧{}による初期化式を用いて呼び出す事ができる。

struct T {
  T(int a)               { puts("1"); }
  T(int a, int b)        { puts("2"); }
  T(int a, int b, int c) { puts("3"); }
};

T a = 1;         // "1"
T b = {1, 2};    // "2"
T c = {1, 2, 3}; // "3"

初期化子リスト(initializer_list)を受け取るコンストラクタを宣言した場合、{}による初期化式はinitializer_listを受け取るコンストラクタを優先的に呼び出すようになる。

struct T {
  T(int a)               { puts("1"); }
  T(int a, int b)        { puts("2"); }
  T(int a, int b, int c) { puts("3"); }
  T(std::initializer_list<int> list) { puts("4"); }
};

T a = {1};       // "4"
T b{1};          // "4"
T c = {1};       // "4"
T d = {1, 2};    // "4"
T e = {1, 2, 3}; // "4"

この場合、複数の引数を受け取るコンストラクタは、丸括弧を用いて明示的に呼び出す必要がある。

T f = 1;      // "1"
T g(1);       // "1"
T h(1, 2);    // "2"
T i(1, 2, 3); // "3"
// T j(1, 2, 3, 4); // error: no matching constructor for initialization of 'T'
// T k{1, 2, 3, 4}; // "4"

変換コンストラクタを無効にする方法

変換コンストラクタの暗黙的な呼び出しを禁止するためにはexplicitキーワードを利用する。この場合、明示的な初期化や変換を行った場合にのみコンストラクタ呼び出しが行えるようになる。

struct T {
  explicit T(int v) {}
};

T a = 9;    // error: no viable conversion from 'int' to 'T'
T b = {9};  // error: chosen constructor is explicit in copy-initialization
T c(9);     // OK
T d{9};     // OK
T e = T(9); // OK
T f = static_cast<T>(9); // OK

変換コンストラクタと処理コスト

暗黙的なコンストラクタ呼び出しによる初期化方法は一見すると=演算子によってコピー代入や暗黙的なコピーコンストラクタ呼び出しが行われてしまうようにも思えるが、実際にはコンパイラ側の最適化によって、初期化のみが行われるようになることがほとんどである。つまり暗黙的/明示的いずれの記法も内部的には同じ初期化処理が行われ、処理コストは変わらないものとなる。なおこの挙動はC++17以降、言語仕様によって保証されている。

要するに、この場合のT v = 9;という式は、厳密には変換後の一時オブジェクトを代入する式となるが、実際には単なる初期化式として扱われることになる。

ただし、初期化後の変数に対して再代入を行う際には、変換コンストラクタの呼び出しとコピー代入処理が同時に働き、余計な処理コストを産んでしまうことになるため引き続き注意したい。

T a;   // デフォルトコンストラクタ呼び出し
a = 9; // 変換コンストラクタ & コピー代入

T b = 9; // 変換コンストラクタのみが働く
広告
広告