特殊なコンストラクタ
コピーコンストラクタ
自動生成されるコンストラクタは、デフォルトコンストラクタのほかにコピーコンストラクタというものもあります。
デフォルトコピーコンストラクタ
#include <iostream>
#include <string>
class SimpleClass
{
private:
int number;
std::string name;
public:
//コンストラクタ
SimpleClass() : number(0) {}
SimpleClass(int n, const char* s)
: number(n), name(s) {}
int getNumber() { return number; }
void setNumber(int n) { number = n; }
std::string getName() { return name; }
void setName(const char* s) { name = s; }
};
int main()
{
SimpleClass sc1(1, "John");
std::cout << "number: " << sc1.getNumber() << std::endl;
std::cout << "name: " << sc1.getName() << std::endl;
//number: 1 name: John
//コピーコンストラクタの呼び出し
SimpleClass sc2(sc1);
//↓でもOK
//SimpleClass sc2 = sc1;
std::cout << "number: " << sc2.getNumber() << std::endl;
std::cout << "name: " << sc2.getName() << std::endl;
//number: 1 name: John
std::cin.get();
}
このSimpleClass
のコンストラクタ定義は「引数なし」「int型、const char*型」のふたつしかありません。
しかし、35行目ではSimpleClass
のインスタンスを引数にして、新しいSimpleClass
のインスタンスを生成しています。
sc2
の中身を確認すると、sc1
と同じ値になっています。
なぜこんなことができるのかというと、デフォルトコピーコンストラクタ(既定のコピーコンストラクタ)というものが自動的に生成されるからです。
デフォルトコピーコンストラクタは、自分と同じクラスのインスタンスを引数とするコンストラクタで、メンバ変数をすべてコピーしたものが新しく作られます。
(ただし静的メンバ(static
)は除く)
自動生成のコピーコンストラクタを使わない方がいいケース
コピーコンストラクタの自動生成は便利ですが、これに頼っていると思わぬバグの原因となる場合があります。
#include <iostream>
class PointerClass
{
int *pointer;
public:
PointerClass(int* p = nullptr) : pointer(p) {}
int *get() { return pointer; }
void set(int *p) { pointer = p; }
};
int main()
{
int num = 10;
PointerClass pc1(&num);
//デフォルトコピーコンストラクタを利用
PointerClass pc2(pc1);
//pc2から値を書き換え
*pc2.get() = 20;
std::cout << "pc1値: " << *pc1.get() << std::endl;
std::cout << "pc1ポインタ: " << pc1.get() << std::endl;
std::cout << "pc2値: " << *pc2.get() << std::endl;
std::cout << "pc2ポインタ: " << pc2.get() << std::endl;
std::cin.get();
}
pc1値: 20 pc1ポインタ: 00CFFA38 pc2値: 20 pc2ポインタ: 00CFFA38
メンバ変数にポインタを持つシンプルなクラスです。
ポインタというのはメモリ上の位置を示す値です。
コピーコンストラクタはこの「値」までもそのままコピーしてしまいます。
つまり、インスタンスpc1
とpc2
のメンバ変数が指すメモリ位置はどちらも同じになっています。
これではインスタンスpc2
を通してメモリ上の値を書き換えると、本来無関係なはずのインスタンスpc1
にも影響が出てしまいます。
こういった場合にはデフォルトのコピーコンストラクタを使用すべきではありません。
コピーコンストラクタの定義
これを解決するには、自分でコピーコンストラクタを定義します。
class PointerClass
{
int *pointer;
public:
PointerClass(int* p = nullptr) : pointer(p) {}
//コピーコンストラクタ
PointerClass(const PointerClass&) : pointer(nullptr) {}
int *get() { return pointer; }
void set(int *p) { pointer = p; }
};
コンストラクタの引数を、自身のクラスの参照を引数に受け取るようにするとコピーコンストラクタを定義することができます。
ここでコピーしたいメンバ変数を任意に選ぶことができます。
単純なメンバ変数のコピー以外にも処理は書けますが、「コピー」に関係のない処理を書くとややこしくなるので書かない方が良いです。
引数は参照なので、それを通して書き換えが可能な状態です。
それでは危険なので、引数にはconst
を付けておきます。
これで、コピーコンストラクタ内で引数を書き換えようとするとコンパイラがエラーを出してくれます。
一部のC++標準関数で自作クラスを扱う場合、コピーコンストラクタの引数にconstを要求することがあるようなので、必ず付けるようにした方が良いでしょう。
なお、このコピーコンストラクタの処理では実引数を使用しないので、仮引数名の記述は省略しています。
(データ型の記述だけで良い)
ただし、このままだと「メンバ変数が勝手にコピーされない」だけで、コピーコンストラクタを呼び出すことはできてしまいます。
コピーできたように見えるのに実際にはできていないので(メンバ変数pointer
はnullptr
)、そのままメンバのポインタ変数を使用するとアクセス違反が発生します。
int main()
{
int num = 10;
PointerClass pc1(&num);
//コピーコンストラクタが使用できてしまう
PointerClass pc2(pc1);
//実行時エラー
*pc2.get() = 20;
}
このような場合には、コピーコンストラクタ自体を禁止してしまうことができます。
コピーコンストラクタの禁止
class PointerClass
{
int *pointer;
public:
PointerClass(int* p = nullptr) : pointer(p) {}
//デフォルトコピーコンストラクタを削除
PointerClass(const PointerClass&) = delete;
};
int main()
{
int num = 10;
PointerClass pc1(&num);
//コンパイルエラー
//PointerClass pc2(pc1);
//これもコンパイルエラー
//PointerClass pc3 = pc1;
}
自動的に生成されるコンストラクタは、引数リストの直後に= delete;
を記述することで削除することができます。
delete
キーワードについてはページ後段で改めて説明します。
コピーコンストラクタ以外にも自動生成されるコンストラクタはいくつかありますが、それぞれ引数宣言が異なるので、対象はそれにより判別されます。
コピーコンストラクタの引数は自身のクラスの参照型ひとつです。
delete
キーワードが使用できるのはC++11(2011年のバージョン)からです。
それよりも以前のバージョンでは、コピーコンストラクタをprivate
に記述することで外部からの呼び出しを禁止することができます。
class PointerClass
{
int *pointer = nullptr;
//コピーコンストラクタ
//privateなので外部から呼び出すことができない
PointerClass(const PointerClass&) {}
public:
PointerClass(int* p = nullptr) : pointer(p) {}
};
なお、この方法で禁止できるのはあくまで「コピーコンストラクタ」の使用です。
代入操作は禁止されませんから、以下のような場合は相変わらずコピーできてしまいます。
class PointerClass
{
int *pointer = nullptr;
public:
PointerClass(int* p = nullptr) : pointer(p) {}
//デフォルトコピーコンストラクタの削除
PointerClass(const PointerClass&) = delete;
};
int main()
{
int num = 10;
PointerClass pc1(&num);
PointerClass pc2;
//インスタンスの代入
//メンバ変数がそのままコピーされる
pc2 = pc1;
}
代入演算子によるコピー処理も、クラスに自動的に追加される処理のひとつです。
これを防ぐには代入処理の書き換えが必要になります。
その方法は演算子のオーバーロードで改めて説明します。
変換コンストラクタ
引数がひとつだけのコンストラクタは、以下のようにして呼び出すことが可能です。
class SimpleClass
{
int number;
public:
SimpleClass() : number(0) {}
//変換コンストラクタ
SimpleClass(int n) :number(n) {}
int get() { return number; }
void set(int n) { number = n; }
};
int main()
{
//変換コンストラクタの呼び出し
SimpleClass sc1 = 10;
std::cout << sc1.get() << std::endl;
//10
//暗黙の変換ができないのでNG
//SimpleClass sc2 = "abc";
}
引数付きコンストラクタは、通常は()
演算子(関数呼び出し演算子)で実行します。
引数ひとつで呼び出せるコンストラクタは変換コンストラクタとして働き、=
演算子(代入演算子)を使用してコンストラクタを実行できます。
=
演算子の右辺に引数と同じデータ型、または暗黙的な変換が可能なデータ型を渡すと変換コンストラクタが呼び出され、インスタンスが生成されます。
変換ができない場合はインスタンスを生成することはできません。
この場合の暗黙の型変換とは、初期化に使用するデータ型から直接そのクラスの型へ変換することです。
直接的な変換ができない場合は変換コンストラクタは使用できません。
#include <string>
class C
{
public:
C(std::string s) {}
};
int main()
{
//二段階の変換が必要なのでNG
//C c1 = "abc";
//明示的にコンストラクタを呼び出すのはOK
C c2("abc");
//直接変換が可能ならOK
std::string s = "abc";
C c3 = s;
}
例えば上記コードの場合、文字列リテラル(const char*
型)→std::string
型→C
型、という変換が必要です。
この場合、変換コンストラクタは使用できません。
明示的にコンストラクタを呼び出す場合はこの変換は可能です。
「引数ひとつで呼び出せる」という条件を満たせば良いので、以下のコンストラクタはすべて変換コンストラクタとして働きます。
class SimpleClass
{
int integer;
double real;
std::string str;
public:
//全部変換コンストラクタ
SimpleClass(double x) : integer(0), real(x) {}
SimpleClass(const char* x) : integer(0), real(0), str(x) {}
SimpleClass(int x, double y = 0.0) //これもOK
: integer(x), real(y) {}
};
デフォルト引数を指定した引数は、その引数を省略して呼び出すことができます。
その結果、引数ひとつで呼び出せるようになっていれば変換コンストラクタとして働きます。
当然ながら、デフォルト引数を省略しない形では変換コンストラクタは呼び出せません。
//こういうのは×
SimpleClass sc1 = (1, 2.0);
//これは〇(通常のコンストラクタの明示的な呼び出し)
SimpleClass sc2(1, 2.0);
変換コンストラクタの呼び出しには代入演算子(=
)を使用しますが、これは代入ではなくコンストラクタの暗黙的な呼び出しです。
変数の初期化時ではなく、すでに存在するインスタンスに対して=
を使用した場合は、変換コンストラクタによりインスタンスが作成され、そのインスタンスから代入処理が行われます。
class SimpleClass
{
int number;
public:
//デフォルトコンストラクタ
//変換コンストラクタとしても働く
SimpleClass(int n = 0) : number(n) {}
};
int main()
{
//変換コンストラクタの呼び出し
SimpleClass sc1 = 10;
//デフォルトコンストラクタの呼び出し
SimpleClass sc2;
//変換コンストラクタにより一時オブジェクトが生成され、
//インスタンスの代入が行われる
sc2 = 20;
//↑はこのような処理になる
{
SimpleClass _tmp = 20;
sc2 = _tmp;
}
//「20」から生成されたインスタンスは一時オブジェクトなので
//代入処理の終了後に直ちに破棄される
}
変換コンストラクタの禁止
変換コンストラクタは便利ですが、以下のような問題もあります。
class SimpleClass
{
int integer;
public:
SimpleClass(int x) : integer(x) {}
};
void func(SimpleClass c)
{
//省略
}
int main()
{
//これはエラーにならない
func(10);
}
関数func
はSimpleClass
のインスタンスを受け取るつもりで作った関数です。
しかしこの関数は、SimpleClass
のインスタンスのほか、整数を指定して呼び出すこともできます。
これは整数が関数に渡された時に暗黙的にSimpleClass
の変換コンストラクタが呼び出され、インスタンスが生成されるためです。
この動作が意図したものならば良いのですが、普通は意図していない場合が多いでしょう。
このような場合、変換コンストラクタを禁止することができます。
class SimpleClass
{
int integer;
public:
//暗黙的な呼び出しの禁止
explicit SimpleClass(int x) { integer = x; }
};
void func(SimpleClass c)
{
//省略
}
int main()
{
//暗黙的なコンストラクタの呼び出しはコンパイルエラー
//func(10);
//明示的なコンストラクタの呼び出しはOK
SimpleClass sc(10);
func(sc);
//これもダメ
//SimpleClass sc2 = 20;
}
コンストラクタの手前にexplicit
というキーワードを付けると、コンストラクタの暗黙的な呼び出しを禁止することができます。
変換コンストラクタは意図しない動作となることが多いので、基本的に禁止しておいた方がいいかもしれません。
ムーブコンストラクタ
C++11からはムーブコンストラクタという新しいコンストラクタが導入されています。
ムーブについてはムーブセマンティクスの項で改めて説明します。
default/delete宣言
クラスを定義すると、コンストラクタなどのいくつかの関数が自動的に生成されます。
自動生成される関数は、別の関数(コンストラクタなど)を定義すると自動生成されなくなることがあります。
C++11からは、default
キーワードとdelete
キーワードでこの挙動を制御することができます。
class SimpleClass
{
//デフォルトコンストラクタを削除
SimpleClass() = delete;
SimpleClass(int) {}
//デフォルトのコピーコンストラクタを使用
SimpleClass(const SimpleClass&) = default;
//デストラクタを削除
~SimpleClass() = delete;
};
目的の自動生成関数の引数定義(丸括弧)の後ろに= default;
と記述することで、この関数の生成を強制することができます。
その他、処理は既定のままで良いがinline
やvirtual
キーワードを付けたい場合にも使用できます。
= delete;
と指定することで、自動生成関数の生成を禁止することができます。
自動生成関数の生成ルール
以下はまだ説明していないクラスの機能についての記述があるので、参考程度にみてください。
クラスを定義すると、以下の特殊メンバ関数(コンストラクタ、演算子のオーバーロード)が自動的に生成されます。
- デフォルトコンストラクタ
- コピーコンストラクタ
- コピー代入演算子
- ムーブコンストラクタ
- ムーブ代入演算子
- デストラクタ
これらのメンバ関数は、特定のメンバ関数を宣言(定義)するなどによって自動生成されなくなります。
以下はその動作をまとめた表です。
表の見方は、まず左端の列(縦)からユーザー定義する関数を探し、その関数を定義した場合の動作を横に見ていきます。
例えばコピーコンストラクタ(copy ctor)を定義した場合は、デフォルトコンストラクタ(def ctor)は×、コピー代入演算子(copy op)は△、…となります。
def ctor | copy ctor | copy op | move ctor | move op | destructor | |
---|---|---|---|---|---|---|
def ctor | ◎ | ○ | ○ | ○ | ○ | ○ |
copy ctor | × | ◎ | △ | × | × | ○ |
copy op | ○ | △ | ◎ | × | × | ○ |
move ctor | × | × | × | ◎ | × | ○ |
move op | ○ | × | × | × | ◎ | ○ |
destructor | ○ | △ | △ | × | × | ◎ |
ctor = コンストラクタ、op = オペレーター(演算子)
◎ = ユーザー宣言/定義
○ = デフォルト(自動生成)
△ = 自動生成されるが非推奨
× = 削除
この表で削除(×)となっているものは、内部的には「宣言されない」場合と「宣言が削除される」場合があります。
結果的に使用できないという点は同じなのでひとつにまとめています。
非推奨(△)となっているものは、自動生成はされるもののそのまま使用せずに自分で定義することが推奨されます。
例えばコピーコンストラクタをユーザー定義した場合、コピー代入演算子は自動生成されます。
コピーコンストラクタとコピー代入演算子は同じ動作であるべきですが、一方のみを自分で定義するとそれぞれで異なる処理が行われる可能性があります。
このような場合は自動生成に任せずに両方を自分で定義すべきです。
デストラクタは、クラスが持つ何らかのリソースを解放する処理を書くのが一般的です。
リソースは多くの場合でポインタで管理しますが、既定のコピーコンストラクタ/代入演算子はメンバを全てコピーするため、ポインタもコピーされます。
デストラクタでこのポインタを解放すると二重解放になってしまうので、コピーコンストラクタ/代入演算子ではポインタをコピーしないように自分で定義する必要があります。
class C1
{
int* p; //リソース
public:
//リソースの作成
C1() : p(new int[100]) {}
~C1()
{
//リソースの解放
delete[] p;
}
};
class C2
{
int* p; //リソース
public:
//リソースの作成
C2() : p(new int[100]) {}
//コピーコンストラクタと
//コピー代入演算子では
//リソースはコピーしない
C2(const C2&) : p(nullptr) {}
C2& operator=(const C2&)
{
p = nullptr;
}
~C2()
{
//リソースの解放
delete[] p;
}
};
int main()
{
{
C1 c1_1;
//既定のコピーコンストラクタの使用
//ポインタもコピーされる
C1 c1_2 = c1_1;
}
//変数c1_1とc1_2の寿命はここまで
//この時点でポインタが二重解放される
{
C2 c2_1;
//ユーザー定義のコピーコンストラクタの使用
//ポインタはコピーされない
C2 c2_2 = c2_1;
}
//変数c2_1とc2_2の寿命はここまで
//こちらは安全
}