TopC++ 入門 › 演習 › ライフサイクル・トレース

ライフサイクル・トレース問題(全 40 問)

「このコードで ctor / copy / move / dtor が何回呼ばれる?」 — このトレース練習を経ると、RAII・Rule of 5・RVO・move の理解が一気に深まります。各問題は同じ X クラス(各ログ出力付き)を前提にしています。

前提:すべての問題で使う X クラス

struct X {
    X()                       { std::cout << "ctor "; }
    X(const X&)              { std::cout << "copy "; }
    X(X&&) noexcept           { std::cout << "move "; }
    X& operator=(const X&)   { std::cout << "cpa ";  return *this; }
    X& operator=(X&&) noexcept { std::cout << "mva ";  return *this; }
    ~X()                      { std::cout << "dtor "; }
};

入力欄に「ctor / copy / move / cpa / mva / dtor」それぞれの回数を入れてください。回答欄の並びはこの順。コピー省略(C++17 guaranteed copy elision)が効く箇所に注目。

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。