C++における**代入演算子(=)**は、一見すると単純な構文に見えますが、実際にはオブジェクトの寿命管理、コピー・ムーブの意味論、リソース管理と深く関わる、非常に重要な機能です。
本記事では、基本的な代入の仕組みから、クラス設計における注意点、C++11以降のモダンな考え方までを段階的に解説します。
目次
代入演算子の基本的な役割
a = b;
この式が行っている処理は次の通りです。
- 右辺
bを評価する - その値を左辺
aに代入する - 代入後の左辺自身を式の値として返す
そのため、代入演算子は「値を設定する」だけでなく、式として値を返す演算子でもあります。
初期化と代入はまったく別物
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::arraystd::vectorstd::copy
※ memcpy は トリビアルにコピー可能な型に限定すべき です。
参照と代入
int x = 10;
int y = 20;
int& ref = x;
ref = y;
refはxの別名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-swap やstd::unique_ptr を使う設計が推奨されます。
連鎖代入が可能な理由
a = b = c;
b = cが実行され、bを返す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++の代入演算子についてでした。
最後までお読みいただき、ありがとうございました。
