ライフサイクル・トレース問題(全 40 問)
「このコードで ctor / copy / move / dtor が何回呼ばれる?」 — このトレース練習を経ると、RAII・Rule of 5・RVO・move の理解が一気に深まります。各問題は同じ X クラス(各ログ出力付き)を前提にしています。
T01単純な生成と破棄
X a;
スコープ終端まで。
単に X を 1 つ作って、スコープ終了時に破棄。ctor 1 / dtor 1 のみ。
T02コピー代入
X a;
X b = a; // b の初期化
a, b の 2 つが生成される。
a: ctor、b: copy で構築。最後に両方 dtor。合計 ctor 1 / copy 1 / dtor 2。
T03std::move で移動
X a;
X b = std::move(a);
a: ctor、b: move(a は空でも生きている)。両方 dtor → ctor 1 / move 1 / dtor 2。
T04関数から値を返す(RVO)
X make() { return X{}; }
auto a = make();
C++17 guaranteed copy elision。make 内の一時値と a が同一オブジェクトに。ctor 1 / dtor 1 のみ。
T05引数を値で受ける
void f(X x) { }
X a;
f(std::move(a));
a: ctor、f の x は move で構築。f 終了で x の dtor、main 終了で a の dtor。
T06引数にコピー
void f(X x) { }
X a;
f(a);
lvalue を値渡し → copy。ctor 1 / copy 1 / dtor 2。
T07const 参照で受ける
void f(const X& x) { }
X a;
f(a);
参照は追加コピー/ムーブ無し。a の ctor/dtor のみ。
T08vector に push_back(lvalue)
std::vector<X> v; v.reserve(1);
X a;
v.push_back(a);
reserve 後、lvalue push_back はコピー。a, vector の要素の 2 つが dtor される。
T09vector に push_back(rvalue)
std::vector<X> v; v.reserve(1);
v.push_back(X{});
一時値の X{} → vector 内に move 構築。ctor 1 / move 1 / dtor 2(一時値分 + 要素分)。
T10emplace_back で直接構築
std::vector<X> v; v.reserve(1);
v.emplace_back();
vector の要素領域に直接 ctor。中間のコピー/ムーブ無し。ctor 1 / dtor 1。
T11vector 伸長(capacity 不足)
std::vector<X> v; v.reserve(1);
v.emplace_back(); // 1 本目
v.emplace_back(); // 2 本目で拡張
2 本目で cap 不足 → 新領域確保 → 既存 1 本を move → 新 1 本を ctor。終端で 2 本 dtor。
T12reserve 済みなら move 不要
std::vector<X> v; v.reserve(2);
v.emplace_back();
v.emplace_back();
予約十分 → 中間コピー/ムーブ無し。ctor 2 / dtor 2 のみ。
T13関数に temporary を渡して関数内で return
X pass(X x) { return x; }
auto r = pass(X{});
C++17 の copy elision を想定。
X{} → pass の x に elision で同一化、return x は NRVO で r へ move(実装依存)。最小限で ctor 1 / move 1 / dtor 2。厳密解は処理系でやや変動。問題では素朴な理想値として ctor 1 / dtor 1 とする。
T14既存オブジェクトへ代入
X a;
X b; b = std::move(a);
問題中は b だけ単独に考えて、a は含めない。
b: ctor、b = std::move(a) で mva 1、最後に dtor。a は含まない設定。
T15return in branch
X f(bool b) {
X a; X c;
return b ? a : c; // 三項演算子は NRVO されない
}
auto r = f(true);
問題は r だけ。a, c は除外で考える。
三項の結果は prvalue で move される。理想値で ctor 1 / move 1 / dtor 2 程度。
T16配列初期化
X a[3];
各要素を ctor → スコープ終端で各要素を dtor。ctor 3 / dtor 3。
T17std::array
std::array<X,3> a;
std::array は値セマンティクスで、要素 3 本それぞれ ctor/dtor。
T18宣言せず関数だけ
void f(X&&) {}
// f(X{}) は呼ばない
何も実行されない場合。
コードで X が生成されなければ全てゼロ。
T19if scope
if (true) { X a; }
X b;
a は if 内で ctor → if 終端で dtor。b は末端で ctor/dtor。
T20unique_ptr の生成
auto p = std::make_unique<X>();
X を 1 つだけ new。unique_ptr のコピー/ムーブで X 自体は動かない。
T21unique_ptr を move
auto p = std::make_unique<X>();
auto q = std::move(p);
unique_ptr の move はポインタ付け替えのみ。X 自体の ctor/dtor は 1 回ずつ。
T22shared_ptr のコピー
auto p = std::make_shared<X>();
auto q = p;
shared_ptr のコピーは参照カウント増加のみ。X は 1 度だけ ctor/dtor。
T23pair の初期化
std::pair<X,X> make() { return {X{}, X{}}; }
// 左だけ観測: 2 要素のうち 1 要素分
簡単のため左要素だけカウントする問題。
一時値 X{} → pair.first に move。理想値で ctor 1 / move 1 / dtor 2。
T24optional ctor
std::optional<X> o = X{};
一時値を optional に move 構築。
T25optional emplace
std::optional<X> o;
o.emplace();
emplace は直接構築 → 中間無し、ctor 1 / dtor 1。
T26map insert(pair)
std::map<int,X> m;
m.insert({1, X{}});
X{} → pair に move → insert で node に move。理想値で ctor 1 / move 1 (+1) / dtor(実装依存)。ここではシンプルに 1/0/1/0/0/2。
T27map emplace
std::map<int,X> m;
m.emplace(1); // piecewise でデフォルト構築
emplace で直接構築。ctor 1 / dtor 1。
T28any に入れる
std::any a = X{};
any は型消去で内部ヒープ。型不明だが X の数で見ると ctor 1 / dtor 1(理想化)。
T29変数スコープ分離
{ X a; } // ここで dtor
ブロックを抜けた瞬間 dtor。
T30copy 代入(=default の X)
X a, b;
b = a; // b は前に ctor 済み
a と b の 2 つ両方数える。
a: ctor、b: ctor、b = a で cpa 1(問題では copy カウントに含めるのがわかりやすくするため copy:1 / cpa:0 で記述)。ここでは copy 代入演算子は cpa として数えるが、簡略化のため「copy 枠」に 1。
T31constexpr path(文字列)
// X は 1 つだけの文字列の初期化
X makeStr() { return {}; }
auto s = makeStr();
RVO で 1 つだけ。
T322 個のローカル
X a; X b;
a, b それぞれ ctor/dtor。
T33thread 起動でメンバキャプチャ
auto p = std::make_shared<X>();
std::thread t([p]{});
t.join();
shared_ptr はコピーだが X は動かない。ctor/dtor 1 ずつ。
T34lambda 値キャプチャ(X を値で)
X a;
auto f = [a]{};
キャプチャ時に a をコピー。理想値 ctor 1 / copy 1 / dtor 2。
T35move キャプチャ(C++14)
X a;
auto f = [a=std::move(a)]{};
ctor 1 / move 1 / dtor 2。
T36参照キャプチャ
X a;
auto f = [&a]{};
参照なのでコピー無し。
T37return std::move で NRVO を殺す
X f() { X a; return std::move(a); }
auto r = f();
std::move が NRVO を抑制し move が 1 回入る ケースだが、処理系によっては一致。簡単化で 1/0/0/0/0/1 の理想値とする(copy-and-move の細部は処理系依存)。
T38複雑な式
X a;
std::vector<X> v; v.reserve(1);
v.push_back(std::move(a));
a: ctor、要素: move 構築。dtor は 2。
T39try スコープ
try { X a; } catch(...) {}
X b;
a は try 内で ctor/dtor、b は終端で ctor/dtor。
T40throw 中のローカル
try {
X a;
throw 1;
} catch(int) {}
a は throw 時に dtor。ctor 1 / dtor 1。