第32回 コピーコンストラクタとコピー代入
Foo b = a; や b = a; という代入で呼ばれる 2 つの関数がコピーコンストラクタとコピー代入演算子。ほとんどの場合はコンパイラ任せで OKですが、メンバにポインタでリソースを持つ場合(カスタム RAII など)は自分で書く必要があります。本回は「書かなくていいケース」と「書かないと破滅するケース」を区別します。
このページで押さえること
✅ 最低限ここだけ覚える
Foo b = a; → コピーコンストラクタ
b = a;(既存の b に) → コピー代入
- メンバが STL 型だけならコンパイラ任せで OK(Rule of Zero)
- 生ポインタを持つクラスは要注意(浅いコピーで壊れる)
⭐ 余裕があれば読む
- 浅いコピー vs 深いコピー
- 自己代入チェック
- Copy-and-swap イディオム
- コピー禁止 (
= delete)
1. まず触ってみる ― 2 種類のコピー
下のコードでは、どちらの行でコピーコンストラクタが呼ばれ、どちらでコピー代入が呼ばれるかを観察してください。
2 種類の違い基本
Foo a; // 普通のコンストラクタ
Foo b = a; // ① コピーコンストラクタ(新規作成+コピー)
Foo c(a); // ① と同じ
Foo d{a}; // ① と同じ
Foo e;
e = a; // ② コピー代入(既存の e を書き換え)
ここまでで覚えること(2 つ):
- 新しいオブジェクトを作りながらコピー → コピーコンストラクタ
- 既存のオブジェクトに代入 → コピー代入演算子
よくある素朴な疑問
Q. コンパイラが勝手に作ってくれるの?
→ はい。自分で書かなければメンバを 1 つずつコピーする版が自動生成されます。STL 型(vector / string)だけをメンバにしていれば、これで完璧。
Q. 自分で書く必要があるのは?
→ 生ポインタでメモリを所有しているクラスや、OS リソース(ファイルハンドル、ソケット)を直接持つクラス。でもこれらは本来スマートポインタ / RAII 型でラップすべき(Rule of Zero)。
Q. Foo b = a; は「代入」じゃないの?
→ 見た目は代入のようですが、新しい変数 b の初期化なのでコンストラクタ扱い。= がついていても紛らわしく、「初期化か代入か」は左辺が既存変数か新規変数かで判断します。
2. コピーコンストラクタの書き方
シグネチャは決まっています:
シグネチャ定型
class Foo {
public:
Foo(const Foo& other) : x_(other.x_), y_(other.y_) {
// 他のメンバがあればコピー
}
};
引数は同じクラスへの const 参照。これを外すとエラー(値渡しだと無限再帰、非 const は const 変数から呼べない)。
コンパイラが自動生成するコピー
概念的に同等暗黙生成
class Foo {
int x_;
std::string name_;
};
// コンパイラが作る版(概念的):
Foo(const Foo& other)
: x_(other.x_), name_(other.name_) {}
// 各メンバのコピーコンストラクタを順に呼ぶだけ
メンバが「自分でちゃんとコピーできる型」(int/double/string/vector など)だけなら、これで完璧に動きます。
3. 浅いコピー vs 深いコピー
生ポインタをメンバに持つクラスで、自動生成の「メンバを 1 つずつコピー」はポインタ値だけをコピーするので、2 つのオブジェクトが同じヒープ領域を指してしまいます。これが浅いコピー(shallow copy)の罠。
浅いコピー破綻する
class BadBuffer {
int* data_;
size_t size_;
public:
BadBuffer(size_t n)
: data_(new int[n]), size_(n) {}
~BadBuffer() { delete[] data_; }
// コピーコンストラクタを書かない → 暗黙版:
// BadBuffer(const BadBuffer&) = 各メンバをコピー
// → data_ も同じヒープアドレスをコピー
};
BadBuffer a{100};
BadBuffer b = a; // ← 同じ data_ を指す!
// スコープ終了で a と b 両方が delete[] data_ → 二重解放!
深いコピー安全
class GoodBuffer {
int* data_;
size_t size_;
public:
GoodBuffer(size_t n)
: data_(new int[n]), size_(n) {}
~GoodBuffer() { delete[] data_; }
// 自分で書いた深いコピー
GoodBuffer(const GoodBuffer& other)
: data_(new int[other.size_]), size_(other.size_)
{
std::copy_n(other.data_, size_, data_); // 中身をコピー
}
};
GoodBuffer a{100};
GoodBuffer b = a; // b は独自のヒープ領域を持つ
浅い vs 深いの図解
4. コピー代入演算子
b = a(b が既存オブジェクト)のときに呼ばれるのがコピー代入演算子。
シグネチャ定型
class Foo {
public:
Foo& operator=(const Foo& other) {
if (this == &other) return *this; // 自己代入チェック
// メンバをコピー(必要なら古いリソース解放 → 新規確保)
return *this; // 連鎖代入のため
}
};
ポイントは 3 つ:
- 自己代入チェック(
a = a; の場合に壊れないように)
- 自分の古いリソースを解放してから新しいリソースをコピー
*this を返す(a = b = c のような連鎖のため)
Copy-and-swap イディオム
正しく書くのが難しいコピー代入を、コピーコンストラクタと swap だけで自動導出する有名なテクニック:
copy-and-swapイディオム
class Foo {
...
public:
Foo(const Foo& other) { /* 深いコピー */ }
// ※ 引数は値渡し(=コピーが作られる)
Foo& operator=(Foo other) {
swap(*this, other); // 自分と other の中身を交換
return *this;
} // ← ここで other(= 元の this の中身)が破棄される
};
これで「自己代入チェック」「古いリソース解放」が swap と破棄で自動的に行われます。ただし swap を正しく実装する必要あり(STL の std::swap を使えば通常 OK)。
⭐
結論から言うと…
多くのケースでコピーを自分で書く必要はない。そのための設計原則が次の §5 の Rule of Zero。
5. Rule of Zero ― 書かない勇気
「生ポインタでリソースを持つ」のを避け、STL 型(vector / string / unique_ptr / shared_ptr)にラップすると、コピー / ムーブ / デストラクタを自分で書く必要がなくなります。これが Rule of Zero。
昔のやり方(Rule of Three)手動
class Buffer {
int* data_;
size_t size_;
public:
Buffer(size_t n)
: data_(new int[n]), size_(n) {}
// Rule of Three: 以下の 3 つ全部必要
~Buffer() { delete[] data_; }
Buffer(const Buffer&); // copy ctor
Buffer& operator=(const Buffer&); // copy assign
};
モダン(Rule of Zero)推奨
class Buffer {
std::vector<int> data_; // STL 型でラップ
public:
explicit Buffer(size_t n) : data_(n) {}
// 以下、何も書かない!
// デストラクタ、コピー、ムーブが自動で正しく動く
};
覚えるべきは:
- Rule of Zero:データメンバが全部 STL 型なら、特殊関数を一切書かない(推奨)
- Rule of Three:C++03 時代。デストラクタ/コピー/代入の 3 つはセットで書く
- Rule of Five:C++11 以降。さらにムーブコンストラクタ/ムーブ代入を加えた 5 つ
詳細は次の第 29 回。でも答えは常に「Rule of Zero を狙え」。
コピー禁止のパターン
所有権が 1 つしかない設計(ファイル、ソケット、unique_ptr)は、コピー自体を禁止します:
コピー禁止= delete
class UniqueResource {
public:
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
// 代わりにムーブを許す(次章)
UniqueResource(UniqueResource&&) = default;
};
確認クイズ
コピーを 4 問で確認。
Q1. 次のうちコピー代入演算子が呼ばれるのは?
Foo a;
Foo b = a;
Foo c; c = a;
Foo d{a};
①は普通のコンストラクタ、②と④は新規作成+コピーなのでコピーコンストラクタ、③は既存の c に代入なのでコピー代入演算子が呼ばれます。
Q2. 以下のクラスを Foo b = a; でコピーするとどうなる?
class Foo { int* p_; public: Foo(){ p_ = new int(42); } ~Foo(){ delete p_; } };
a と b は独立した値を持つ
コンパイルエラー
a.p_ と b.p_ が同じアドレスを指し、二重解放で UB
p_ は nullptr にリセットされる
暗黙のコピーコンストラクタは「メンバを 1 つずつコピー」なので、ポインタ値だけコピーされて両者が同じヒープを指します。両方のデストラクタで同じアドレスを delete = 二重解放(未定義動作)。これを避けるには深いコピー or Rule of Zero。
Q3. Rule of Zero の正しい説明は?
必ずコピーとデストラクタを書くべき
メンバを STL 型にラップして特殊関数を一切書かない
コピーは必ず禁止する
ヒープを使わない
Rule of Zero:リソース管理は STL 型(vector / unique_ptr など)に任せ、自分のクラスではコピー/ムーブ/デストラクタを一切書かない。もっとも安全で保守しやすい設計。
Q4. コピーコンストラクタのシグネチャとして正しいのは?
Foo(Foo other)
Foo(Foo* other)
Foo(const Foo& other)
Foo(Foo&& other)
コピーコンストラクタはconst Foo& を受け取る。①の値渡しは無限再帰(コピー中にコピーが必要)。②はポインタ(別物)。④はムーブコンストラクタ(次章)。