実装イディオムは、所有権、ライフタイム、または依存関係の境界を明示する必要があります。簡潔な構文が有用なのは、関連する無効化規則と計算量が読み手に明確に伝わる場合に限られます。
シーケンスコンテナーでの Erase-Remove #
std::remove と std::remove_if はアルゴリズムです。保持する要素が範囲の先頭部分を占めるように並べ替え、新しい論理的な終端を返します。要素を消去することも、コンテナーのサイズを変更することもありません。コンテナーが範囲を受け取る erase オーバーロードを提供している場合は、直ちに末尾部分を消去してください。
std::vector<int> values{1, 2, 2, 3, 4};
values.erase(std::remove(values.begin(), values.end(), 2), values.end());
values.erase(std::remove_if(values.begin(), values.end(), is_invalid), values.end());C++20 以降では、std::erase(values, value) と std::erase_if(values, predicate) により、それらをサポートする標準コンテナーに対する一連の操作を表現できます。プロジェクトの言語およびライブラリのベースラインで利用できる場合は、これらのオーバーロードを優先してください。
削除アルゴリズムは保持する要素をムーブまたは代入する場合があるため、位置が変化した値への参照とイテレーターは、同じ値を指さなくなります。後続の erase には、コンテナーの無効化規則が適用されます。std::vector では、消去によって消去位置以降のイテレーター、ポインター、参照が無効になるため、コードはそれらを取得し直す必要があります。ノードベースのコンテナーには異なる規則があるため、vector に関する前提を std::list、std::map、std::unordered_map に適用しないでください。
Pimpl による表現の境界 #
Pointer to implementation イディオムは、非公開の表現を公開ヘッダーから切り離します。クライアントはすべての非公開メンバー型ではなく、安定したインターフェースに対してコンパイルされます。そのため、表現の変更による再ビルドを減らせます。また、公開 ABI のほかの部分も制御されていれば、ABI の境界を維持できます。
// widget.hpp
class Widget {
public:
Widget();
~Widget();
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
Widget(const Widget&);
Widget& operator=(const Widget&);
private:
class Implementation;
std::unique_ptr<Implementation> implementation_;
};
// widget.cpp
class Widget::Implementation {
// Private state and dependencies.
};
Widget::~Widget() = default;std::unique_ptr<Implementation> を使用するとヘッダー内で前方宣言できますが、デリーターがインスタンス化される時点では、デストラクターによる破棄のために Implementation が完全型である必要があります。Implementation の定義後にデストラクターをアウトオブラインで定義すると、その完全性を確保できます。unique_ptr はコピー不可であるため、コピー操作には明示的なディープコピー方針が必要です。Widget を意図的にムーブ専用にする場合は、コピー操作を省略してください。
Pimpl には、一般的な形式では 1 回の割り当て、ポインターの間接参照、局所性の低下、デバッグが複雑になる可能性というコストがあります。また、あらゆる実装変更を ABI セーフにするわけではありません。インライン関数、例外指定、仮想関数、基底クラス、公開されるパラメーター型は、引き続き契約の一部です。
Pimpl を使わない依存関係の削減 #
意図した契約を保護できる最も軽量な境界を使用してください。
- ヘッダーがクラスへのポインターまたは参照のみを保持する場合は、そのクラスを前方宣言します。
- 呼び出し側が表現ではなく振る舞いを必要とする場合は、限定的なインターフェースに依存します。
- コンパイル時の結合を許容できる場合に限り、テンプレート定義と値メンバーをヘッダーに置きます。
- 具象的な非公開レイアウト、高コストの依存関係グラフ、または ABI に関するコミットメントによって実行時の間接参照が正当化される場合は、Pimpl を使用します。
型エイリアスも、データメンバーと同程度に表現を露出させる可能性があります。std::vector<T>::iterator を返すと、クライアントは vector のイテレーター型に依存します。後からコンテナーを変更すると、ソース互換性が失われ、ABI も変更される可能性があります。
間接参照と値セマンティクス #
直接保持する値メンバーは、割り当て領域が 1 つで、所有権が明確であり、多くの場合はキャッシュ局所性にも優れます。std::optional<T> は、独立した割り当てを行わずに、存在しない場合もあるインプレースの値を表します。std::unique_ptr<T> は、個別に割り当てられたオブジェクトの排他的所有権を表し、ポリモーフィズム、不完全型、大きなオプション状態、または固有の識別性が必要な場合に適しています。
単にメンバーが大きいからではなく、オブジェクトにそのセマンティクスが必要であることを理由に間接参照を選択してください。間接参照を使うと、null 許容性、割り当て失敗、ポインターのライフタイム、追加のエイリアス階層が実装の一部になります。一方、値セマンティクスでは、コピーとムーブがクラスの契約の一部になります。呼び出し側が独立した値を必要とする場合に選択してください。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)