4
4

More than 3 years have passed since last update.

コピーコンストラクタっているの?

Posted at

導入

「コピーコンストラクタってなんのためにあるの?」という質問を受け、自分なりの回答を書いてみます。

結論

メモリを直接操作できるC++だからこそ必要。
そしてコピーコンストラクタを定義するときは、代入演算子(コピーオペレータ)も定義しよう。

[前提] コピーの種類

コピーには大きく2種類ある。

シャローコピー(Shallow copy)

参照をコピーするコピーです。
メンバ変数の参照をコピーします。

参照ををコピーするので高速ですが、他のインスタンスと共有することになるので、適切に管理することが必要です。
一方の変更を他方が参照することになります。
マルチスレッド環境ではスレッドセーフであるかも重要になります。

ディープコピー(Deep copy)

実体をコピーするコピーです。
メンバ変数のインスタンスをコピーします。

新しいインスタンスを生成するため、低速ですが、コピー元とコピー先は別のインスタンスになります。
シャローコピーと異なり、一方の変更を他方は参照できません。

C++でのコピー

コピーの種類

シャローコピーに相当するのはポインタのコピーです。
あくまでポインタの持つアドレスがコピーされるだけで、ポインタの示す領域はコピーされません。

C++でのシャローコピー
char buffer[256];

char* ptrA = &(buffer[0]);

char* ptrB = ptrA; // ポインタのコピー。複製されるのはbuffer[256]へのポインタだけで、配列が複製されることはない。

ディープコピーに相当するのは構造体のコピーです。

C++でのディープコピー
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_ptr
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メソッドとか。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4