C++ Learning

第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 深いの図解

❌ 浅いコピー
a
[heap ①]
b
[heap ①] ← 同じ!
両者が同じヒープを指す。破棄時に二重解放
✅ 深いコピー
a
[heap ①]
b
[heap ②] ← 独立
独立したヒープを持つ。各自が自分のを破棄。

4. コピー代入演算子

b = a(b が既存オブジェクト)のときに呼ばれるのがコピー代入演算子

シグネチャ定型
class Foo { public: Foo& operator=(const Foo& other) { if (this == &other) return *this; // 自己代入チェック // メンバをコピー(必要なら古いリソース解放 → 新規確保) return *this; // 連鎖代入のため } };

ポイントは 3 つ:

  1. 自己代入チェックa = a; の場合に壊れないように)
  2. 自分の古いリソースを解放してから新しいリソースをコピー
  3. *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& を受け取る。①の値渡しは無限再帰(コピー中にコピーが必要)。②はポインタ(別物)。④はムーブコンストラクタ(次章)。