C++で代入禁止・コピー禁止は設計意図を明示する|レビューで所有権と責務の曖昧さを排除する技法
この記事のポイント
- コピー・代入禁止の設計意図とそのレビュー技法を整理
- delete指定の使い分けとレビューでの判断基準を徹底解説
- 実務コードの典型的問題例と改善例を提示
コピー禁止・代入禁止の設計背景
C++ではクラス設計時に何もしないと以下の特殊メンバが自動生成されます。
メンバ | 役割 | 自動生成の有無 |
---|---|---|
デフォルトコンストラクタ | 初期化 | ○ |
コピーコンストラクタ | 値コピー | ○ |
コピー代入演算子 | 代入 | ○ |
ムーブコンストラクタ | ムーブ初期化 | C++11以降 ○ |
ムーブ代入演算子 | ムーブ代入 | C++11以降 ○ |
デストラクタ | 解放 | ○ |
ここがレビュー観点の本質
コピー・代入の自動生成は極めて危険である
コピー禁止が必要になる典型パターン
代表ケース | 具体例 |
---|---|
所有権管理 | ファイルハンドル、ソケット、DB接続、排他リソース |
一意性設計 | シングルトン、ID生成器、状態保持クラス |
非コピー性構造 | std::unique_ptr保持クラス |
責務可視化 | 所有責務が曖昧にならないようにする |
コピー禁止を怠ると何が起きるか
① リソースリーク発生
FileHandle a(1);
FileHandle b = a; // fdが重複保持
- fdを二重close → 未定義動作
② 保守困難なバグ温床
- 保守者が代入可能性を誤信して使う
③ API境界責務が崩壊
- 参照と所有の境界が読み取れなくなる
C++11以降の正しい禁止手法:delete指定
class FileHandle {
public:
explicit FileHandle(int fd) : fd_(fd) {}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
private:
int fd_;
};
delete指定の効果
- コピー時にコンパイルエラー
- 明示禁止がコード上に残る
良い実装例
class DbConnection {
public:
explicit DbConnection(const std::string& url) : url_(url) {}
DbConnection(const DbConnection&) = delete;
DbConnection& operator=(const DbConnection&) = delete;
private:
std::string url_;
};
良い設計理由
- 接続一意性が設計で保証
- 責務誤用をコンパイラで封止
- 保守者が迷わないAPI設計
レビュー観点
レビューアーは以下を確認します。
- 所有権管理型でコピー禁止が適用されているか
- delete指定がコピーと代入の両方に明示されているか
- C++03流のprivate宣言禁止方式が残存していないか
- ムーブ設計を必要に応じて併用しているか
- delete指定理由がレビューで説明可能になっているか
良くない実装例: ケース1
class Logger {
public:
Logger() {}
private:
Logger(const Logger&);
Logger& operator=(const Logger&);
};
@ReviewerC++03流のコピー禁止は非推奨です。C++11以降はdelete指定に移行してください。
改善例
class Logger {
public:
Logger() {}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
ケース2: コピー禁止片側のみ適用
class FileHandle {
public:
FileHandle(const FileHandle&) = delete;
// operator= 未禁止
};
@Reviewerコピー代入演算子にもdelete指定が必要です。不整合設計になります。
改善例
FileHandle& operator=(const FileHandle&) = delete;
ケース3: リソース所有責務なのに禁止抜け
class Socket {
public:
Socket(int fd) : fd_(fd) {}
private:
int fd_;
};
@Reviewer所有権責務が明確なのでコピー・代入禁止を明示してください。
改善例
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
ケース4: ムーブ専用化設計のdelete抜け
class UniqueResource {
public:
UniqueResource(int id) : id_(id) {}
UniqueResource(UniqueResource&&) noexcept = default;
};
@Reviewerムーブのみ許容設計時もコピー禁止は必ず明示してください。
改善例
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
ケース5: delete理由説明不足
class Manager {
public:
Manager(const Manager&) = delete;
Manager& operator=(const Manager&) = delete;
};
@Reviewerなぜコピー禁止か設計理由をコメントで補足してください。
改善例
// グローバル唯一インスタンス管理のためコピー禁止
class Manager {
public:
Manager(const Manager&) = delete;
Manager& operator=(const Manager&) = delete;
};
ケース6: 意図せぬ所有権混在のAPI設計崩壊
void process(FileHandle fh);
FileHandle a(1);
process(a); // コピー呼ばれる
@Reviewer所有権移譲の責務曖昧化。引数は参照/ムーブ等に整理すべきです。
改善例
void process(FileHandle&& fh);
process(std::move(a));
API境界は「所有権転送責任」がレビュー本質
deleteレビューの防止リスク整理
危険パターン | 防止効果 |
---|---|
二重所有バグ | 資源リーク封止 |
利用誤用バグ | 利用者設計意図即時可視化 |
API責務崩壊 | 呼び出し責任整理 |
設計継承崩壊 | 誤継承時の危険封止 |
観点チェックリスト
まとめ
レビューアーが毎回確認すべき思考はこうです:
「このクラスは複製されて良い設計か?」
- delete指定は所有権設計の自己文書化
- コピー禁止は型安全の柱
「自動生成に任せない設計文化」がC++レビュー力を決定付けます。