C++ProgrammingC++ Developer

C++17の保証されたコピー削除ルールは、prvalue式を関数パラメータにバインドする際のコピー/ムーブコンストラクタのアクセスibilittyに対する要件をどのように変更しますか?

Hintsage AIアシスタントで面接を突破

質問への答え

C++17では、標準が保証されたコピー削除(強制コピー削除)を導入し、prvalues(純粋なrvalues)の具体化の方法を根本的に変更しました。クラス型のprvalueが同じ型のオブジェクトを初期化する場合—たとえば、関数から値で返すときや、一時オブジェクトを関数に渡すとき—オブジェクトは宛先ストレージに直接構築されます。その結果、コピーコンストラクタムーブコンストラクタは呼び出されず、重要なことに、これらのアクセス性(public対private)や存在(クラスが完全であり、このクラスのインスタンスが破壊可能であれば)さえ、操作が正しく形成されるために必要ではありません。これは、以前の標準と大きく対比され、削除は単なるオプションの最適化であり、コンパイルのためにアクセス可能で存在するコンストラクタが必要でした。

struct Immovable { Immovable() = default; Immovable(const Immovable&) = delete; Immovable(Immovable&&) = delete; }; Immovable factory() { return Immovable{}; // C++17ではOK:ムーブ/コピーは呼び出されない } void consume(Immovable x); // パラメータはprvalueから直接初期化される

生活からの状況

私たちのチームは、リソースハンドルがハードウェアコンテキストをラップし、登録されたカーネルアドレスのためにメモリ内で複製または移動できないカーネルモードドライバを構築していました。これらのハンドルを値で生成するためにファクトリ関数が必要でしたが、ハンドルは意図的にコピーおよびムーブコンストラクタを削除して、カーネルマッピングの偶発的な無効化を防いでいました。C++17より前では、この設計は値で返すことと互換性がなく、NRVOを使用しても、コンパイラが概念的にムーブコンストラクタがアクセス可能である必要があり、コンパイルエラーが発生していました。

解決策1: std::unique_ptrを介したヒープ割り当て

ハンドルをstd::unique_ptrでラップすることを考え、ポインタをムーブできるようにしながら、基礎となるオブジェクトは固定されていました。このアプローチは安全性を提供し、C++14で機能しました。

利点: 標準的なメモリ管理、メモリリークを防止、レガシーコードベース全体で広くサポート。

欠点: 動的割り当てのオーバーヘッドとポインタの間接参照を導入し、決定論的な低遅延が要求されるカーネルコンテキストでは制約が大きい。また、CPUキャッシュの断片化が発生し、割り当て失敗のための例外処理考慮が必要。

解決策2: アウトパラメータの初期化

呼び出し元が割り当てたオブジェクトへの参照をファクトリに渡し、その場で初期化します。

利点: C++標準バージョンに関係なくゼロコピーの保証;ヒープ割り当てがない;動かせない型と互換性がある。

欠点: 流畅なAPIスタイルを破壊する(auto h = create();Handle h; create(h);に変わる); 初期化前使用のリスクが増加し、標準アルゴリズムや範囲ベースのforループとの組み合わせが悪くなる。

解決策3: C++17の保証されたコピー削除を活用

ファクトリを再構成し、動かせない型を値として返すようにし、強制的な削除に依存してprvalueを呼び出し元のストレージに直接構築します。

利点: ヒープ使用を排除;値の意味論を保持;コンパイル時にゼロコストの抽象化を強制;ムーブ/コピーコンストラクタは存在しなくても、またはアクセス可能でなくてもよい。

欠点: 完全に純粋なrvaluesのみに適用(既存の名前付き変数を返すことはできない);C++17サポートを持つコンパイラが必要;構築中の例外処理の微妙な違いを理解する必要がある。

我々は解決策3を選択しました。なぜなら、ファクトリは純粋なprvaluesを生成し、保証された削除シナリオに完全に一致したからです。これにより、ハンドルは厳密に動かせないままであり、エルゴノミクスの値の意味論とauto宣言との互換性を維持できました。

ドライバは、数千の同時接続のためのマイクロ秒規模の初期化で出荷されました。アセンブリ検査によって、ハンドルは移動やコピーコードなしで呼び出し元のスタックフレーム内で直接構築されていることが確認されました。型システムは構築によってリソースの安全性を強制し、ホットパスからヒープ競合を完全に排除しました。

候補者が見逃しがちな点


保証されたコピー削除は、関数内の名前付き戻り値(lvalues)に適用されますか、それとも純粋なprvaluesに厳密に制限されていますか?

保証されたコピー削除は、prvalues(純粋なrvalues)にのみ独占的に適用されます。これは、名前のない戻りステートメントで作成される一時オブジェクトを含みます。名前付き戻り値最適化(NRVO)はオプションのコンパイラ最適化として残っており、広く実装されていますが、コンストラクタのアクセス性や副作用について同じ保証を提供しません。候補者が名前付きのローカル変数を返そうとし、ムーブコンストラクタが削除されていても、保証された削除がトリガーされると仮定すると、プログラムは不正に形成されます。なぜなら、名前付き変数はlvaluesであり、ムーブ/コピー操作が必要だからです。コンパイラがオプションのNRVOを適用しない限り、これは強制されていないからです。


保証されたコピー削除ルールの下で、明示的に削除されたコピーおよびムーブコンストラクタを持つクラスを関数から値で返すことはできますか?

はい。C++17では、返される式がprvalue(例: return MyClass{};)であれば、コピーおよびムーブコンストラクタは初期化のために考慮されません。オブジェクトは呼び出し元のストレージに直接構築されるため、削除されたコンストラクタはodr使用されず、コンパイルエラーを引き起こしません。ただし、このタイプの名前付き変数を返そうとすると失敗します。なぜなら、その操作は概念的にlvalueを戻りスロットに移動する必要があり、これが削除されたムーブコンストラクタを呼び出し、不正形成のプログラムを引き起こすからです。


保証されたコピー削除は、特にスタックのアンワインディング中のprvalue一時の寿命に関して例外安全性とどのように相互作用しますか?

保証されたコピー削除の下では、ターゲットオブジェクトの寿命が始まる前に、別の一時オブジェクトが作成されることはありません。prvalueは最終的な宛先に直接具体化されます。そのため、prvalueの構築中に例外が発生した場合、スタックのアンワインディングメカニズムは、破棄が必要な別の一時を見かけることはありません。代わりに、部分的に構築された宛先オブジェクトを見構えます。これは、呼び出し元の観点から、オブジェクトは完全に構築されているか、まったく存在しないかのどちらかであり、例外安全性の保証を簡素化し、目的のオブジェクトの寿命が正式に始まる前に放棄された一時に起因する二重破棄やリソース漏れが発生しないことを保証します。