C++は書き慣れた人でも、ポインタやconst、スマートポインタ、ムーブといった細かなルールで迷いやすい言語です。曖昧な理解のままでもコードは動きますが、思わぬバグやリークの原因になりかねません。本記事では、実務や学習で押さえておきたい重要ポイントを10問のクイズにまとめました。腕試しや知識の再確認にぜひ挑戦してみてください。
Q1 : std::move の説明として正しいものはどれですか? オブジェクトのメモリ内容をバイト単位で別の場所へコピーする オブジェクトが使用していたメモリを解放する ムーブコンストラクタを直接呼び出して中身を移す 引数を右辺値参照へキャストするだけで、それ自体は何も移動しない
std::move という名前から誤解されがちですが、実体は static_cast 相当のキャストで、引数を右辺値参照として扱えるようにするだけです。実際に資源を移すのは、その右辺値を受け取ったムーブコンストラクタやムーブ代入演算子です。ムーブ後のオブジェクトは有効だが未規定の状態になるため、代入や破棄以外の利用は避けるべきです。なお const オブジェクトに std::move を使うと const T&& になり、ムーブが選ばれずコピーになる点にも注意が必要です。
Q2 : C++17で導入された std::string_view について正しい説明はどれですか? 文字列を所有せず、既存の文字列への読み取り専用の参照(ビュー)として振る舞う 文字列のコピーを内部に保持する 常にヌル終端されていることが保証される 文字列の内容を自由に書き換えられる
std::string_view は、文字列の先頭ポインタと長さだけを持つ軽量なビューで、文字列そのものを所有せず、内容の変更もできません。コピーを伴わないため、関数の引数で文字列を受け取るときに、std::string と文字列リテラルの両方を効率よく受けられます。一方で、参照先の文字列が先に破棄されるとダングリング参照になる点と、ヌル終端が保証されない点には注意が必要です。C APIへ渡す際は、std::string に変換するなどの対応が要ります。
Q3 : ラムダ式 int x = 0; auto f = [x]() { x++; }; がコンパイルエラーになる理由として正しいものはどれですか? ラムダ式では変数をキャプチャすることができないから ラムダ式の本体で++演算子は使えないから 値キャプチャされた変数は、operator()がconstのためデフォルトでは変更できず、mutableが必要だから ラムダ式には必ず戻り値の型を明示する必要があるから
ラムダ式は内部的にはクラスとして扱われ、その operator() はデフォルトで const メンバ関数になります。そのため値でキャプチャした変数(ここではx)は、ラムダ内では書き換えられません。変更したい場合は [x]() mutable { x++; } のように mutable を付けます。ただしその変更はラムダ内部のコピーにだけ作用し、外側のxには影響しません。外側の変数を書き換えたいなら、参照キャプチャの [&x] を使います。戻り値の型は省略でき、自動で推論されます。
Q4 : 2つのオブジェクトが互いに std::shared_ptr で保持し合うとメモリリークが起きます。これを避けるために片方に使うべきスマートポインタはどれですか? std::unique_ptr std::auto_ptr std::atomic std::weak_ptr
shared_ptr 同士が循環参照すると、互いの参照カウントが0にならず、オブジェクトが解放されません。これを避けるには、片方を std::weak_ptr にします。weak_ptr は所有権を持たず、参照カウントを増やさない観測用のポインタです。使うときは lock() で shared_ptr を取得し、対象がすでに破棄されていれば空のポインタが返ります。親子関係では、親から子を shared_ptr、子から親を weak_ptr で持つ設計が典型的です。なお auto_ptr はC++17で削除されています。
Q5 : std::unique_ptr<int> a = std::make_unique<int>(5); の後に std::unique_ptr<int> b = a; と書くとどうなりますか? 参照カウントが2になり、aとbが所有権を共有する aとbが同じオブジェクトを指し、二重解放が起きる コピーコンストラクタが削除されているためコンパイルエラーになる 実行時にstd::bad_alloc例外が投げられる
std::unique_ptr は所有権を唯一のポインタだけが持つ設計で、コピーコンストラクタとコピー代入演算子が delete されています。そのため b = a; のようなコピーはコンパイル時にエラーになります。所有権を移したい場合は std::unique_ptr b = std::move(a); のようにムーブを使います。ムーブ後のaはnullptrになります。所有権を共有したい場合は参照カウント方式の std::shared_ptr を使うのが適切です。この仕組みによりリソースの二重解放を型システムで防げます。
Q6 : 基底クラスのデストラクタがvirtualでない場合、基底クラスのポインタ経由で派生クラスのオブジェクトをdeleteするとどうなりますか? 未定義動作になる 必ずコンパイルエラーになる 派生クラスのデストラクタが先に正しく呼ばれる std::bad_alloc例外が送出される
C++の規格では、基底クラスのデストラクタが仮想でないとき、基底クラスへのポインタを通じて派生クラスのオブジェクトをdeleteすると未定義動作になると定められています。実際には派生クラス側のデストラクタが呼ばれず、派生クラスが確保したリソースがリークすることが多いです。ポリモーフィックに使う基底クラスでは、デストラクタを virtual ~Base() = default; のように宣言するのが基本的な作法です。コンパイルエラーにはならないため気づきにくい点にも注意が必要です。
Q7 : std::vector<int> v; v.reserve(100); を実行した直後の v.size() の値はどれですか? 100 1 未定義 0
reserve は内部のメモリ領域(capacity)を事前に確保するだけで、要素は追加しません。そのため reserve(100) の直後でも size() は0のままで、capacity() が100以上になります。要素数を100にしたい場合は resize(100) を使います。resize は要素を実際に生成して size を変えます。reserve は push_back を繰り返す際の再確保とコピーを減らす最適化として有効です。なお reserve 後に要素数を超えない範囲で追加している間は、イテレータや参照が無効になりません。
Q8 : std::vector<int> v の各要素を範囲forで書き換えたいとき、正しい書き方はどれですか? for (auto x : v) { x *= 2; } for (auto& x : v) { x *= 2; } for (const auto& x : v) { x *= 2; } for (const auto x : v) { x *= 2; }
範囲forで要素そのものを変更するには、参照で受ける必要があります。auto& x とすればxは各要素への参照になり、x *= 2 がvectorの中身に反映されます。auto x はコピーを受け取るため、変更しても元の要素は変わりません。const auto& はconst参照なので変更しようとするとコンパイルエラーになり、const auto も同様に変更できません。大きなオブジェクトを読み取るだけなら、コピーを避けるために const auto& を使うのが一般的です。
Q9 : std::map<std::string, int> m; で、キー存在しない std::out_of_range例外が送出される 未定義動作になる キー 何も挿入されずend()が返る
std::map の operator[] は、指定したキーが存在しない場合に、そのキーと値初期化された値(intなら0)を持つ要素を新たに挿入し、その値への参照を返します。このため、単に存在確認のつもりで m[key] を使うと、意図せず要素が増えてしまいます。挿入せずに検索したいときは find() や count()、C++20以降なら contains() を使います。また、例外を投げるアクセスが必要な場合は at() を使うと、キーが無いときに std::out_of_range が送出されます。
Q10 : int* const p; ポインタpの指す先の値は変更できないが、pを別のアドレスに付け替えることはできる pを別のアドレスに付け替えることはできないが、pの指す先の値は変更できる pの指す先の値もpの付け替えも、どちらもできない pの指す先の値もpの付け替えも、どちらも自由にできる
constの位置がポイントです。アスタリスクの右側にconstがある int* const p は、ポインタ変数そのものがconstという意味になります。そのため初期化後にpへ別のアドレスを代入することはできませんが、*p = 10; のように指す先の値を書き換えることは可能です。逆に const int* p(または int const* p)は、指す先の値が変更不可でポインタは付け替え可能です。両方を禁止したい場合は const int* const p と書きます。読み方のコツは、constを右から左へ読むことです。