導入
「コピーコンストラクタってなんのためにあるの?」という質問を受け、自分なりの回答を書いてみます。
結論
メモリを直接操作できるC++だからこそ必要。
そしてコピーコンストラクタを定義するときは、代入演算子(コピーオペレータ)も定義しよう。
[前提] コピーの種類
コピーには大きく2種類ある。
シャローコピー(Shallow copy)
参照をコピーするコピーです。
メンバ変数の参照をコピーします。
参照ををコピーするので高速ですが、他のインスタンスと共有することになるので、適切に管理することが必要です。
一方の変更を他方が参照することになります。
マルチスレッド環境ではスレッドセーフであるかも重要になります。
ディープコピー(Deep copy)
実体をコピーするコピーです。
メンバ変数のインスタンスをコピーします。
新しいインスタンスを生成するため、低速ですが、コピー元とコピー先は別のインスタンスになります。
シャローコピーと異なり、一方の変更を他方は参照できません。
C++でのコピー
コピーの種類
シャローコピーに相当するのはポインタのコピーです。
あくまでポインタの持つアドレスがコピーされるだけで、ポインタの示す領域はコピーされません。
char buffer[256];
char* ptrA = &(buffer[0]);
char* ptrB = ptrA; // ポインタのコピー。複製されるのはbuffer[256]へのポインタだけで、配列が複製されることはない。
ディープコピーに相当するのは構造体のコピーです。
typedef struct {
char buffer[256];
} stBuffer;
stBuffer buffA;
stBuffer buffB = buffA; // 構造体のコピー。buffAの全領域がbuffBの領域へ複製される。
C++のオブジェクトのコピーはシャローコピーかディープコピーか
どちらにもなれますし、どちらでもないものにもなれます。
クラスの設計者次第です。
class SyallowCopy {
public:
char* m_ptr;
SyallowCopy(char * ptr) : m_ptr(ptr) { }
SyallowCopy(const SyallowCopy& rhs) : m_ptr(rhs.ptr) { }
};
char buffer[256] = "バッファだよ";
SyallowCopy objA(&(buffer[0]));
SyallowCopy objB = objA; // ここでコピーコンストラクタを呼び出す
SyallowCopy objC(objA); // これもコピーコンストラクタを呼び出す
// objA.m_ptrもobjB.m_ptrもobjC.m_ptrもbufferを示す
std::strcpy( objA.m_ptr, "変更したよ" );
// bufferが"変更したよ"という文字列になっている。
class DeepCopy {
public:
char m_buffer[256];
DeepCopy(const char * ptr) { std::strcpy(&(m_buffer[0]), ptr); }
DeepCopy(const DeepCopy& rhs) { std::strcpy(&(m_buffer[0]), &(rhs.m_buffer[0])); }
};
DeepCopy objA("バッファだよ");
DeepCopy objB = objA; // ここでコピーコンストラクタを呼び出す
DeepCopy objC(objA); // これもコピーコンストラクタを呼び出す
// objA.m_bufferもobjB.m_bufferもobjC.m_bufferも
// "バッファだよ"という文字列が入っていますがすべて違う領域です。
std::strcpy( &(objA.m_buffer[0]), "変更したよ" );
// objA.m_bufferは"変更したよ"になっているが、
// objB.m_bufferもobjC.m_bufferは"バッファだよ"のまま
どちらでもないの例としてはstd::auto_ptrです。
std::auto_ptrはコピー元からコピー先にアドレスの所有権が移ります。
※std::auto_ptrはC++11で非推奨になっています。
auto ptrA = std::auto_ptr<int>( new int() );
auto ptrB = ptrA; // ここでptrAからptrBに所有権が移る
// ptrA.get() は nullptr、ptrB.get() はnew int()で取得したヒープ領域のアドレス
自動生成されるコピーコンストラクタ
デフォルトコンストラクタと同じで、特段の指定がなければ自動生成してくれます。
自動生成される場合、すべてのメンバ変数がコピーされます。
この時のコピーは、組込み型の場合はメモリ上でコピーされますが、クラスの場合はコピーコンストラクタが呼び出されます。
自動生成のコピーコンストラクタだと問題になるケース
リソースを管理するクラスの場合、問題となるケースがあります。
class ResourceManager {
public:
char* m_ptr;
ResourceManager(size_t size) : m_ptr(new char[size]) { }
~ResourceManager() { delete[] m_ptr; }
};
{
ResourceManager objA(5); // new char[5]が呼ばれる。取得したヒープのアドレスをAddrAとする
{
ResourceManager objB = objA; // 自動生成のコピーコンストラクタが呼ばれる
// objA.m_ptrもobjB.m_ptrもAddrAになっている
} // objBのデストラクタが呼ばれ、AddrAが解放される
} // objAのデストラクタが呼ばれ、AddrAを解放するが、2重解放になってしまう
これはメモリなので分かりやすく例外が発生しますが、プロジェクト独自のリソースだったり、エラー処理が不十分だったりするともっとややこしい事態が発生し、解析に苦労することもあります。
※objAとobjBのデストラクタの間で、objBのデストラクタが解放したリソースを他で取得していたりすると・・・。
まとめ
C++のオブジェクトのコピー処理は、クラス設計者が適切に設計する必要が場合ある。
なので、コピーコンストラクタでコピー処理を定義できるようになっている。
※代入演算子(コピーオペレータ)も同じ。
余談
ガーベージコレクタ(GC)がないからnew/deleteをちゃんと管理する必要があるんですよね。
GCがあるとリークしない(しにくい)からいいなぁ・・・。
でも、結局メモリ以外のリソース解放はGCがやってくれないからあんまりスマートでない対処が必要になってますよね・・・。
.NET FrameworkのDisposeメソッドとか。