C++ インターフェイスは、型とシグネチャに所有権、ライフタイム、置換可能性を表現すべきです。C++ Core Guidelines の Rule of Zero はこの設計から導かれます。RAII メンバーから構成されるドメイン値は、コピー、移動、破棄の動作を手動で再現すべきではありません。
リソース所有者とドメイン値 #
リソース所有型は 1 つのリソースを取得し、デストラクターで解放し、コピーまたは転送が許可されるかどうかを明示します。ハンドルはリソースへのアクセスを表し、所有ハンドルは解放の責任も負います。標準の所有者がリソースの取得、解放、エラーポリシーを表現できない場合に適しています。
#include <cstdio>
#include <utility>
class File {
public:
explicit File(std::FILE* owned_handle) noexcept : handle_(owned_handle) {}
~File() { if (handle_) std::fclose(handle_); }
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
File& operator=(File&& other) noexcept {
if (this != &other) {
if (handle_) std::fclose(handle_);
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
private:
std::FILE* handle_;
};スタック巻き戻し中にデストラクターは例外を投げてはなりません。呼び出し元が解放の失敗を観測する必要がある場合は、明示的な close() 相当の操作を提供し、デストラクターには非 throw のクリーンアップだけを行わせます。カスタム移動操作がリソースを移し、移動元を安全に破棄できる状態にすることを確認してください。デフォルト化されたポインター移動は生のポインターをコピーするため、所有する生ハンドルには不正です。所有権を表現できる場合は、カスタムデリーター付きの std::unique_ptr を優先します。
ドメイン値は正しい所有者を組み合わせるべきです。
#include <string>
#include <utility>
#include <vector>
class Entry {
public:
explicit Entry(std::string name) : name_(std::move(name)) {}
private:
std::string name_;
std::vector<std::string> labels_;
};std::string と std::vector はすでに値セマンティクスを提供するため、Entry にはカスタムデストラクターやコピー・移動操作は必要ありません。
インターフェイスの継承 #
public 継承は、インターフェイスレベルの「is-a」関係をモデル化します。基底型で有効なすべての操作が派生型でも有効な場合にのみ使用し、置換可能性を伴わない実装の再利用にはコンポジションを使用します。
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};多相基底では、基底ポインターを通じて派生オブジェクトを破棄できるようにする場合、仮想デストラクターが必要です。意図したオーバーライドには override を付け、拡張境界を意図的に閉じる場合にのみ final を適用します。表現は非公開にしてください。可変な表現へのアクセスは、public データと同様に不変条件を弱めます。
所有権契約としてのパラメーター #
コピーコストを考える前に、所有権とライフタイムの要件からパラメーター形式を選択します。
| パラメーター | 契約 |
|---|---|
T | 呼び出し先が独自の値を受け取る。安価な値、または呼び出し先がコピーを保持する場合に適する。 |
const T& | 非所有の読み取り専用借用。呼び出し中のライフタイムに対する責任は呼び出し元が保持する。 |
T& | NULL ではない可変借用。呼び出し先は参照されたオブジェクトを変更できるが、所有しない。 |
T* | NULL に意味が文書化されている場合に NULL を許容できる借用。 |
値渡しの std::unique_ptr<T> | 所有権が呼び出し先に移る。 |
値渡しの std::shared_ptr<T> | 共有所有権が確立または延長される。共有ライフタイムが必要な場合に限り使用する。 |
std::string を値で受け取り、ストレージへ移動する方法は、左辺値と右辺値の呼び出し元の両方に対して、独立して保持される値を効率的に表現します。
#include <string>
#include <utility>
class User {
public:
explicit User(std::string name) : name_(std::move(name)) {}
private:
std::string name_;
};所有権の移転を隠すために生のポインターを使用しないでください。呼び出し元が責任を持つ場合は std::unique_ptr<T> などの所有値を返し、API が借用オブジェクトのライフタイムを明示して保証できる場合にのみ参照または非所有ポインターを返します。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)