代入演算子のオーバーロード
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 44/104。
コピー代入演算子 (=) は、既存のオブジェクトを別のオブジェクトに代入するときに呼び出されます。コピーコンストラクタ(新しいオブジェクトを作成するもの)とは異なり、代入演算子は、すでに存在しているオブジェクトに対して動作し、事前にクリーンアップが必要なリソースを保持している可能性があります。
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; }
Buffer& operator=(const Buffer& other) {
if (this != &other) { // 1. 自己代入のチェック
delete[] data; // 2. 既存のリソースをクリーンアップ
size = other.size; // 3. データをコピー
data = new int[size];
for (size_t i = 0; i < size; ++i)
data[i] = other.data[i];
}
return *this; // 4. *this を返す
}
};自己代入のチェック (this != &other) は不可欠です。これがないと、b = b; はコピーを試みる前にデータを削除してしまい、未定義の動作を引き起こします。
この演算子は、a = b = c; のような連鎖を可能にするために、*this を参照によって返します。これは組み込み型の動作と一致します。
リソースを管理するクラスでは、コピー代入演算子とムーブ代入演算子の両方を実装することがよくあります。
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // 現在のリソースを解放する
data = other.data; // リソースを移動する
size = other.size;
other.data = nullptr; // 移動元を有効な状態に保つ
other.size = 0;
}
return *this;
}ムーブ代入演算子は、コピーする代わりに所有権を移譲するため、vec[0] = createBuffer(); のような操作が非常に効率的になります。
チャレンジ
簡単動的に割り当てられた文字配列を管理し、既存のオブジェクト間での代入を適切に処理する DynamicString クラスを作成しましょう。これは、メモリリークやダングリングポインタを発生させずに、あるオブジェクトから別のオブジェクトへ文字列データを安全に転送する必要がある実用的なシナリオです。
コードを整理するために、2つのファイルを作成します:
DynamicString.h: 動的メモリを使用してCスタイルの文字列を格納するDynamicStringクラスを定義します。クラスでは以下を管理する必要があります:- 文字列データのためのプライベートな
char*ポインタ - 長さを保持するためのプライベートな
size_t const char*を受け取り、ディープコピーを作成するコンストラクタ- 割り当てられたメモリを適切に解放するデストラクタ
- 自己代入保護を備えたディープコピーを実行するコピー代入演算子
- 所有権を効率的に転送するムーブ代入演算子
- 内部文字列を返す
c_str()メソッド (const) - 文字列の長さを返す
length()メソッド (const)
コピー代入演算子は、自己代入のチェック、既存リソースのクリーンアップ、新しいデータのコピー、そして
*thisの返却という4つの必須ステップに従う必要があります。ムーブ代入演算子は、ソースのポインタを奪い、ソースを有効な空の状態(長さ0の nullptr)にする必要があります。- 文字列データのためのプライベートな
main.cpp: 入力から3つの文字列を読み取ります(それぞれ別の行)。3つのDynamicStringオブジェクトを作成します:最初の入力でstr1、2番目の入力でstr2、3番目の入力でstr3を作成します。代入演算子の動作を実演してください:
- 3つの文字列すべての初期状態を出力します
- コピー代入を使用します:
str1 = str2; - コピー代入後の str1 と str2 を出力します(両方とも同じ内容になるはずです)
- ムーブ代入を使用します:
str2 = std::move(str3); - ムーブ代入後の str2 と、str3 の長さを出力します(ムーブされた後の長さは 0 になるはずです)
出力形式:
Initial: str1: <value> str2: <value> str3: <value> After copy (str1 = str2): str1: <value> str2: <value> After move (str2 = std::move(str3)): str2: <value> str3 length: 0
strlen と strcpy のために <cstring> を使用してください。ムーブ代入演算子には noexcept を指定してください。std::move のために main.cpp で <utility> をインクルードしてください。ヘッダーファイルにはヘッダーガードを忘れないでください。
チートシート
コピー代入演算子 (=) は、既存のオブジェクトを別のオブジェクトに代入します。新しいデータをコピーする前に、既存のリソースのクリーンアップを処理する必要があります。
コピー代入演算子の構造
ClassName& operator=(const ClassName& other) {
if (this != &other) { // 1. 自己代入のチェック
// 2. 既存のリソースをクリーンアップ
// 3. other からデータをコピー
}
return *this; // 4. *this を返す
}動的メモリを使用した例
class Buffer {
int* data;
size_t size;
public:
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data; // 既存のリソースをクリーンアップ
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; ++i)
data[i] = other.data[i];
}
return *this;
}
};自己代入のチェック (this != &other) は、b = b; のような場合に、データをコピーする前に削除してしまうのを防ぎます。
*this を参照で返すことで、a = b = c; のような連鎖的な代入が可能になります。
ムーブ代入演算子
ムーブ代入演算子は、コピーする代わりに所有権を移譲し、効率を向上させます:
ClassName& operator=(ClassName&& other) noexcept {
if (this != &other) {
// 現在のリソースをクリーンアップ
// other からリソースを奪う
// other を有効な状態(nullptr、0など)にする
}
return *this;
}ムーブ代入の例
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data; // リソースを奪う
size = other.size;
other.data = nullptr; // ソースを有効な状態にする
other.size = 0;
}
return *this;
}標準コンテナで最適なパフォーマンスを得るために、ムーブ代入には noexcept を指定してください。
自分で試してみよう
#include <iostream>
#include <string>
#include <utility>
#include "DynamicString.h"
using namespace std;
int main() {
// 入力から3つの文字列を読み込む
string input1, input2, input3;
getline(cin, input1);
getline(cin, input2);
getline(cin, input3);
// 3つの DynamicString オブジェクトを作成する
DynamicString str1(input1.c_str());
DynamicString str2(input2.c_str());
DynamicString str3(input3.c_str());
// TODO: 3つの文字列すべての初期状態を出力する
// フォーマット:
// Initial:
// str1: <value>
// str2: <value>
// str3: <value>
// TODO: コピー代入を使用する: str1 = str2;
// TODO: コピー代入後の str1 と str2 を出力する
// フォーマット:
// After copy (str1 = str2):
// str1: <value>
// str2: <value>
// TODO: ムーブ代入を使用する: str2 = std::move(str3);
// TODO: ムーブ後の str2 と str3 の長さを出力する
// フォーマット:
// After move (str2 = std::move(str3)):
// str2: <value>
// str3 length: 0
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス