C++の代入演算子について

AI実装検定のご案内

C++における**代入演算子(=)**は、一見すると単純な構文に見えますが、実際にはオブジェクトの寿命管理、コピー・ムーブの意味論、リソース管理と深く関わる、非常に重要な機能です。

本記事では、基本的な代入の仕組みから、クラス設計における注意点、C++11以降のモダンな考え方までを段階的に解説します。

目次

代入演算子の基本的な役割

a = b;

この式が行っている処理は次の通りです。

  1. 右辺 b を評価する
  2. その値を左辺 a に代入する
  3. 代入後の左辺自身を式の値として返す

そのため、代入演算子は「値を設定する」だけでなく、式として値を返す演算子でもあります。

初期化と代入はまったく別物

C++では 初期化代入 は明確に区別されます。

int a = 10;  // 初期化
int b;
b = 10;     // 代入

基本的な違い

観点初期化代入
タイミングオブジェクト生成時生成後
関与する仕組みコンストラクタ / コピー・ムーブ代入演算子
再実行不可可能

クラス型の場合

std::string a = "hello"; // 初期化
std::string b;
b = a;                   // 代入
  • 初期化時には
    • 通常コンストラクタ
    • コピーコンストラクタ
    • ムーブコンストラクタ
    • コピー省略(最適化)
      のいずれかが関与します。
  • 代入時には
    • コピー代入演算子
    • ムーブ代入演算子
      が使われます。

組み込み型(プリミティブ型)の代入

int a = 5;
int b = a;
  • 単純な値コピー
  • 高速かつ安全
  • 副作用なし
double x = 3.14;
double y;
y = x;

組み込み型では代入について特別な注意点はほぼありません。

配列は代入できない

int a[3] = {1,2,3};
int b[3];

// b = a; // コンパイルエラー

理由

  • 組み込み配列には代入演算子が定義されていない
  • 配列名はポインタそのものではない
    (※多くの式で先頭要素へのポインタに暗黙変換される)

代替手段

std::array<int,3> a = {1,2,3};
std::array<int,3> b;
b = a; // OK
  • std::array
  • std::vector
  • std::copy

memcpyトリビアルにコピー可能な型に限定すべき です。

参照と代入

int x = 10;
int y = 20;

int& ref = x;
ref = y;
  • refx の別名
  • ref = y;xにyの値を代入

参照は一度束縛すると 参照先を変更できません

ポインタの代入

int a = 10;
int b = 20;

int* p = &a;
p = &b;
  • ポインタ自身の値(アドレス)が代入される
  • 指す先が切り替わる
*p = 30; // bが30になる
書き方意味
p = q;ポインタの代入
*p = *q;値の代入

クラスにおけるデフォルト代入演算子

class A {
public:
    int x;
};

A a{10};
A b;
b = a;

コンパイラが生成するコピー代入演算子はメンバごとの代入(memberwise assignment)を行います。

  • 値型メンバ → 値コピー
  • クラス型メンバ → その operator= が呼ばれる
  • ポインタ型メンバ → アドレスがコピーされる(結果として浅いコピー)

問題が起きる典型例(所有権を持つポインタ)

class A {
public:
    int* p;

    A(int v) {
        p = new int(v);
    }
};
A a(10);
A b(20);
b = a; // 危険
  • p のアドレスがコピーされる
  • 同じメモリを複数オブジェクトが所有
  • デストラクタで二重解放の危険

コピー代入演算子の実装

class A {
public:
    int* p;

    A(int v = 0) {
        p = new int(v);
    }

    A& operator=(const A& other) {
        if (this == &other) return *this;

        delete p;
        p = new int(*other.p);
        return *this;
    }

    ~A() {
        delete p;
    }
};

ポイント

  • const A&:不要なコピー防止
  • 自己代入チェック
  • 左辺自身を返す(連鎖代入対応)
  • 深いコピー

※ 実務では 例外安全性 のため copy-and-swapstd::unique_ptr を使う設計が推奨されます。

連鎖代入が可能な理由

a = b = c;
  1. b = c が実行され、b を返す
  2. a = b が実行される

そのため operator=参照を返すのが慣習です。

ムーブ代入演算子(C++11以降)

A& operator=(A&& other) noexcept {
    if (this != &other) {
        delete p;
        p = other.p;
        other.p = nullptr;
    }
    return *this;
}
  • 一時オブジェクトからリソースを奪う
  • 不要な確保・コピーを回避
  • noexcept によりコンテナ最適化が有効になる

Rule of Three / Five / Zero

Rule of Three

  • デストラクタ
  • コピーコンストラクタ
  • コピー代入演算子

Rule of Five(C++11)

  • 上記 + ムーブコンストラクタ
  • ムーブ代入演算子

Rule of Zero(推奨)

  • リソース管理を標準ライブラリに任せる
  • 自前で特別メンバ関数を書かない

= delete による代入の禁止

class A {
public:
    A& operator=(const A&) = delete;
};
  • コピー代入を明示的に禁止
  • std::unique_ptr はコピー不可・ムーブ可
  • std::shared_ptr はコピー可(参照カウント)

実務的な設計指針

  • 生ポインタを所有しない設計を優先
  • std::vector, std::string, std::unique_ptr を活用
  • 代入演算子は「リソースの意味」を明確に表す

まとめ

  • = は「代入+左辺を返す」演算子
  • 初期化と代入は別物
  • デフォルト代入はメンバごとの代入
  • 所有権を持つクラスでは operator= の設計が重要
  • 現代C++では Rule of Zero が最も安全

以上、C++の代入演算子についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次