C++98では、メンバー関数は隠れた this ポインタを介して暗黙のオブジェクトにアクセスし、constおよび非constコンテキストを処理するために異なるオーバーロードが必要でした。その後、C++11では、左辺値と右辺値のオブジェクトを区別するためにリファレンス修飾子が導入されました。これにより、すべてのcv-ref組み合わせをカバーするために関数ごとに最大4つのオーバーロードが必要になり、一般的なライブラリのコード重複やメンテナンス負担が大きくなりました。
重要な問題は、メンバー関数が引数と同じ値カテゴリーとcv修飾を持つオブジェクトを返さなければならず、効率的なムーブセマンティクスを可能にするか、またはダングリング参照を防ぐ必要があるということです。オブジェクトの型を推測できない場合、開発者は冗長なオーバーロードセットを書くか、コピーセマンティクスを妥協し、効率的でない右辺値処理や、オブジェクト参照を伝播する微妙なライフタイムバグを引き起こしました。
C++23では、明示オブジェクトパラメータが導入され、構文が void foo(this auto&& self) のようになります。ここで、self はオブジェクトの値カテゴリーとcv修飾子をキャプチャする推論されたパラメータになり、別々の & と && のオーバーロードが不要になります。std::forward<decltype(self)>(self) が正しいカテゴリーを伝播させるからです。しかし、静的メンバー関数には暗黙のオブジェクトがまったく存在しないため、この構文を適用することは、self にバインドするオブジェクトが必要であるという基本的要件に違反し、標準に従ってプログラムは不正になるのです。
// Pre-C++23: 四つのオーバーロードが必要 class Builder { public: Builder& setName(...) & { /* ... */ return *this; } Builder const& setName(...) const& { /* ... */ return *this; } Builder&& setName(...) && { /* ... */ return std::move(*this); } Builder const&& setName(...) const&& { /* ... */ return std::move(*this); } }; // C++23: 一つのオーバーロード class Builder { public: template<typename Self> auto setName(this Self&& self, ...) -> Self&& { // ... return std::forward<Self>(self); } };
私たちのチームは高性能なJSONライブラリを開発し、DOMノードがツリー構築のためにメソッドチェーンをサポートする必要があったため、Nodeクラスは異なる返り値セマンティクスを持つ addChild() メソッドを提供しなければなりませんでした。これらのメソッドは、親が左辺値のときには参照で親を返し、親が右辺値の一時オブジェクトのときには値で返さなければならず、ムーブエリジオンを可能にし、期限切れオブジェクトの accidental modification を防ぐ必要がありました。
初期実装では従来のリファレンス修正オーバーロードを使用しました。私たちは addChild の4つのバージョンを維持しました。一つは左辺値のために Node& を返し、一つはconst左辺値のために Node const& を返し、一つは右辺値のために Node&& を返し、一つはconst右辺値のために Node const&& を返していました。このアプローチはパフォーマンス要件を満たしましたが、テスト面積が4倍になり、const&& オーバーロードが & オーバーロードからのコピー&ペーストエラーによりダングリング参照を誤って返すという重大なバグが発生しました。
私たちはリファレンス修飾子を完全に放棄し、常に値で返してRVO に依存してコピーを最適化することを考えましたが、これは命名されたオブジェクトに不要な移動を強いることになり、返されたノードへの参照を保存する既存のコードとのAPI互換性を破壊しました。また、派生型を推測する基底クラステンプレートを使用したCRTPも評価しましたが、これにより実装の詳細がユーザーに公開され、継承階層が複雑になり、値カテゴリーの伝播問題を完全には解決できませんでした。
C++23 の明示オブジェクトパラメータを採用することで、オーバーロードセットを1つのテンプレートメソッドに統合することができました:template<typename Self> auto addChild(this Self&& self, ...) -> Self。これにより、必要な正確な値カテゴリーがキャプチャされ、実装に冗長な std::move または std::forward なしで完璧な転送が可能になり、メソッドのサイクロマティック複雑度が1つのパスに減少しました。その結果、ボイラープレートコードが75%削減され、オーバーロードの収束に関連するバグのカテゴリーが排除されました。
なぜ明示オブジェクトパラメータ構文を使用すると、パラメータリストの後に従来のcv修飾子やリファレンス修飾子を付加できなくなるのですか?
従来のメンバー関数は、cv修飾子やリファレンス修飾子をパラメータリストの後に配置して、暗黙のthis ポインタ型を変更します。明示オブジェクトパラメータを使用する場合、this Self&& self はすでに Self の型推論内でcv修飾とリファレンスカテゴリーをエンコードしています。パラメータリストの後に追加の修飾子を付加することは、存在しない暗黙のオブジェクトを修飾しようとするため、型システムの矛盾を引き起こします。この組み合わせは標準で明示的に禁止されています。なぜなら、明示パラメータはパラメータと修飾子の両方の役割を包含するからであり、両方を許可すると呼び出しを支配するセマンティクスがどちらであるかについてのあいまいさが生じるからです。
明示オブジェクトパラメータを使用すると、関数本体内の名前のルックアップが従来のメンバー関数とどのように異なるのですか?
従来のメンバー関数では、修飾されていない名前のルックアップは自動的にクラススコープを検索し、まるでthis-> が前に付け加えられるかのように動作します。明示オブジェクトパラメータでは、暗黙の this ポインタが存在せず、パラメータ self を明示的に使用してメンバーにアクセスする必要があります。候補者はしばしば void foo(this auto& self) 内の member が自動的に this->member に解決されると仮定しますが、実際には self. 修飾や ClassName::member のような明示的なクラス修飾が必要です。これにより基本的なルックアップルールが変更され、特に派生クラスから保護されたメンバーにアクセスする場合において、self. が推論された型に対するアクセスチェックをトリガーすることになります。これは、その型が静的なクラス型ではなくなります。
明示オブジェクトパラメータは仮想関数のオーバーライドに参加できますか?そのオーバーライド関係についての制約は何ですか?
明示オブジェクトパラメータは仮想関数に現れるかもしれませんが、オーバーライドのマッチングルールを根本的に変更します。基底クラスが virtual void bar(this Base& self) を宣言している場合、派生クラスが void bar(this Derived& self) と宣言してもオーバーライドはできません。これは従来のオーバーライドでは共変返り値を許可しますが、明示オブジェクトパラメータは関数のシグネチャの一部と見なされるためです。Base& と Derived& は異なる型であるため、これは正当なオーバーライドとは見なされません。これにより、明示オブジェクトパラメータを使用して "sfinaeに優しい" 仮想関数や、多型階層内での型保存メソッドチェーンを実現するための一般的なパターンが阻害されます。オーバーライドするためには、派生関数は基底の明示パラメータ型と正確に一致する必要があり、そのためにそのパラメータの推測の利点が無効になります。