C++ Learning

第30回 オブジェクトライフサイクル総合可視化 ★サイト最大の目玉

ここまで第 25〜29 回で学んだ コンストラクタ → コピー → ムーブ → デストラクタ の連鎖を、シナリオを選んで 1 ステップずつ追いかける総合アニメ。「関数から戻り値を返したときに実際何が起きるか」「std::move を使うとどう変わるか」「RVO(戻り値最適化)で何が省略されるか」を目で見て確かめます。

このページで押さえること
✅ 最低限ここだけ覚える
  • オブジェクトは ctor → 使用 → dtor が基本
  • 関数引数でコピーまたはムーブが起きる
  • 関数戻り値は RVO か move でコピーしない
  • std::move を付けた引数はムーブを狙える
⭐ 余裕があれば読む
  • RVO / NRVO の違い
  • temporary の扱い
  • コピー省略(C++17 で強制化)
  • ムーブできるタイミングの詳細

1. 総合シミュレータ

シナリオを選んで「▶ Step」でコードを 1 行ずつ進めてください。右のパネルに生きているオブジェクトヒープの状態イベントログが表示されます。

基本: ctor → dtor
コピー
ムーブ
関数引数
戻り値 / RVO
vector 格納
step 0 / 0
生きているオブジェクト
ヒープ
ctor
0
copy
0
move
0
dtor
0
読み方のコツ: 1 回の Step で実行される行をオレンジで強調。右側の「生きているオブジェクト」と「ヒープ」が変化する様子と、ログで何が起きたかを対応させながら見てください。ctor / copy / move / dtor のカウンタが合っているか確認するのも理解の助けになります。

2. 各シナリオの解説

① 基本: ctor → dtor

オブジェクトが作られて、スコープを抜けて破棄されるだけの一番シンプルなケース。コンストラクタ 1 回、デストラクタ 1 回。

② コピー: 2 つの独立したオブジェクト

Buffer b = a; でコピーコンストラクタが発動。a と b はそれぞれ別のヒープ領域を持ちます。中身の全要素がコピーされるため、大きなデータだと遅い。

③ ムーブ: 所有権の引っ越し

Buffer b = std::move(a); でムーブコンストラクタが発動。ヒープ領域はそのまま、ポインタだけが a → b へ移る。a は「有効だが空」の状態に。

④ 関数引数

関数に値渡しするとコピーが発生(小さい型なら些細)。std::move で渡せばムーブに。const& で受ければコピーもムーブも発生しない。

⑤ 戻り値 / RVO

関数内で作ったオブジェクトを return した場合、C++17 以降は必ずコピー省略(RVO)が発動。コンストラクタは呼ばれず、呼び出し側で直接構築される。この最適化があるから「重いオブジェクトは値で返せ」が推奨されるのです。

⑥ vector 格納

push_back するときに、ムーブコンストラクタが noexcept ならムーブで格納、そうでなければコピー。自作型に noexcept を付けると性能が別物になります。

3. RVO ― コピー省略の最適化

Return Value Optimization(RVO)は、関数から値を返すときにコピーもムーブも省略する最適化。C++17 からはコンパイラに強制される規格になりました。

RVO なしの概念従来
std::string make() { std::string s = "hello"; // ctor return s; // ① move/copy } auto x = make(); // ② move/copy // ctor 1 + move/copy 2 + dtor 3
RVO ありの実際C++17~強制
std::string make() { std::string s = "hello"; // そのまま x の場所で構築! return s; // ← 省略 } auto x = make(); // ← 省略 // ctor 1 + dtor 1 (!)

ポイントは:

このおかげで: モダン C++ では std::vectorstd::string を値で返しても、コピーコストも考えなくていい。C の時代のように「出力引数ポインタ」で渡す必要がなく、素直に値で返せるのです。
ここまでがサイト最大の山場
第 25〜30 回の総仕上げ。STEP 6 からはスマートポインタ編に入ります。

4. ライフサイクルの鉄則

STEP 5 全体のまとめ:

  1. コンストラクタで初期化、デストラクタで後始末。リソース管理は常にこのペアで(RAII)
  2. メンバを STL 型にする。Rule of Zero で特殊メンバ関数は書かない
  3. 重いオブジェクトは値で返して構わない(RVO + ムーブで速い)
  4. 関数引数は読み取りなら const T&、書き換えなら T&、消費するなら値渡し + std::move
  5. ムーブ後のオブジェクトには触らない(有効だが未定義の状態)
  6. 自作 RAII クラスを書くなら5 つ全部(Rule of Five)+ noexcept
STEP 5 全体の格言:リソース管理は型に任せろ。あなたは書かない」。std::vector / std::string / std::unique_ptr / std::shared_ptr / std::lock_guard / std::ifstream ... C++ 標準ライブラリにはあらゆる種類の RAII 型が揃っています。自前で malloc したりファイルを開いたりする前に、これらの既存の RAII 型で包めないかを考えるのがモダン C++ の第一歩。
広告スペース

確認クイズ

総合問題を 4 問。

Q1. 次のコードで発生するコピーの数は?
std::string make() {
std::string s = "hello";
return s;
}
std::string x = make();

3
2
1
0(C++17 以降)
C++17 以降はコピー省略が規格で強制されており、関数内の s は呼び出し側の x の場所で直接構築されます。コピーもムーブも 0 回、コンストラクタ 1 回・デストラクタ 1 回だけ。

Q2. std::move(x) を付けて引数で渡すと、実際に何が起きる?

x が即座に削除される
x がコピーされる
x を右辺値参照にキャストするだけ(実際の移動は呼ばれた側次第)
x のアドレスが変わる
std::move は実は何も動かさず、型を T&& にキャストするだけ。実際の中身の移動は、受け取り側(例えば vector の push_back が呼ぶムーブコンストラクタ)が行います。

Q3. ムーブされた後のオブジェクトにしていいことは?

元の値を読み取る
そのまま使い続ける
再代入する、または破棄を待つ
アドレスを取る以外は禁止
ムーブされた側は有効だが未定義の状態。値に期待してはいけないが、再代入してから再利用するか、破棄されるのを待つのは OK。

Q4. STEP 5 全体の「鉄則」として最も中心的なメッセージは?

常に new/delete を自分で管理する
ムーブセマンティクスを自分で書く
リソース管理は STL 型(RAII)に任せ、自分では書かない
コピーを禁止する
Rule of Zero + RAII。標準ライブラリにはあらゆるリソース種別用の RAII 型が揃っているので、自分でムーブやコピーを書く必要はほぼ無い。これがモダン C++ の最大の生産性向上。