C++レビュー|ポインタ所有権移譲の明確化とレビュー設計指針
この記事のポイント
- ポインタ所有権移譲の設計をレビューアーが読み取る技術を整理
- API契約に埋め込まれる所有権の暗黙契約を可視化
- unique_ptrとshared_ptrの移譲設計ミスをレビュー時に摘み取る
そもそも所有権移譲とは
C++ではオブジェクトの破棄責任(ライフサイクル管理)が誰にあるのかをコードが表現します。
これを 所有権(ownership) と呼びます。
そしてAPI間のオブジェクト受け渡しでは「所有権を誰に渡すのか」が常に発生します。
これを所有権移譲(ownership transfer)と言います。
基本原則
- 所有権を移譲する → unique_ptrをmoveで渡す
- 所有権は移譲せず参照だけする → const参照や非所有ポインタを渡す
- 複数責務が共有所有する → shared_ptrを渡す
レビューアーはこの契約をコードから読み解く必要があります。
なぜこれをレビューするのか
所有権移譲が不明瞭なコードは以下を引き起こします。
- 二重deleteやメモリリーク
- 寿命違反(use-after-free)
- テスト困難なライフサイクル構造
- API利用側の誤用誘発
特にスマートポインタ普及後の実務では
「移譲契約が読み取れないAPIはバグ温床」
と考えるくらいが適切です。
レビューアー視点
- unique_ptrは移譲契約かどうか常に読む
- shared_ptr移譲が共有責務契約になっているか読む
- API設計が呼び出し側負荷を減らしているか評価する
- 生ポインタ移譲は禁止されているか確認
開発者視点
- moveセマンティクスを活用して責務を押し付け合わない
- APIの引数型で契約を明文化する
- 移譲対象を「常に生成者が破棄する構造」に寄せる
- shared_ptr移譲は極力抑制する
良い実装例
想定ケース:APIリクエストログの永続保存
良い設計例
struct ApiRequestLog {
int requestId;
std::string endpoint;
std::string clientIp;
int responseCode;
time_t requestedAt;
};
class LogStorage {
public:
// 所有権移譲:unique_ptrで受け取る
void save(std::unique_ptr<ApiRequestLog> logEntry) {
logs.push_back(std::move(logEntry));
}
private:
std::vector<std::unique_ptr<ApiRequestLog>> logs;
};
void handleRequest() {
auto logEntry = std::make_unique<ApiRequestLog>();
logEntry->requestId = 123;
LogStorage storage;
storage.save(std::move(logEntry)); // 明示移譲
}
良いポイント
- 所有権は呼び出し元が持つ
- save()呼び出し時に明示的に移譲(move)
- 破棄責任の所在が読める
レビュー観点
- 所有権移譲はunique_ptr moveで統一されているか
- 呼び出し元の破棄責任が読みやすいか
- API引数型で契約意図が表現されているか
- 生ポインタ移譲は禁止できているか
- shared_ptr移譲は慎重運用になっているか
良くない実装例: ケース1(移譲契約の曖昧化)
問題例①
class LogStorage {
public:
void save(ApiRequestLog* logEntry) {
logs.push_back(std::unique_ptr<ApiRequestLog>(logEntry));
}
private:
std::vector<std::unique_ptr<ApiRequestLog>> logs;
};
void handleRequest() {
auto* logEntry = new ApiRequestLog();
logEntry->requestId = 123;
LogStorage storage;
storage.save(logEntry);
}
@Reviewer生ポインタで所有権移譲契約を行う設計は危険です。save()の引数はunique_ptrをmoveで受け取り、呼び出し側がmake_uniqueで生成・移譲する構造に修正してください。
問題点
- 移譲契約がコードから読み取れない
- 呼び出し側にnew/deleteの責任が残る
- 例外安全性が崩れる
改善例
改善例①
class LogStorage {
public:
void save(std::unique_ptr<ApiRequestLog> logEntry) {
logs.push_back(std::move(logEntry));
}
};
void handleRequest() {
auto logEntry = std::make_unique<ApiRequestLog>();
logEntry->requestId = 123;
LogStorage storage;
storage.save(std::move(logEntry));
}
良くない実装例: ケース2(shared_ptrの無秩序移譲)
問題例②
class LogStorage {
public:
void save(std::shared_ptr<ApiRequestLog> logEntry) {
logs.push_back(logEntry);
}
private:
std::vector<std::shared_ptr<ApiRequestLog>> logs;
};
void handleRequest() {
auto logEntry = std::make_shared<ApiRequestLog>();
logEntry->requestId = 123;
LogStorage storage;
storage.save(logEntry);
}
@Reviewersave()が所有権移譲契約なのにshared_ptrを使うと、責務共有の範囲が不明瞭になります。unique_ptr移譲設計へ修正し、共有責務を不要に拡大しない構造にしてください。
問題点
- 所有権移譲の契約意図が読みにくい
- shared_ptrによる責務膨張
- 循環参照の温床になりやすい
改善例
改善例②
class LogStorage {
public:
void save(std::unique_ptr<ApiRequestLog> logEntry) {
logs.push_back(std::move(logEntry));
}
};
void handleRequest() {
auto logEntry = std::make_unique<ApiRequestLog>();
logEntry->requestId = 123;
LogStorage storage;
storage.save(std::move(logEntry));
}
良くない実装例: ケース3(move忘れによるコピー失敗)
問題例③
class LogStorage {
public:
void save(std::unique_ptr<ApiRequestLog> logEntry) {
logs.push_back(logEntry); // move忘れ
}
private:
std::vector<std::unique_ptr<ApiRequestLog>> logs;
};
@Reviewerunique_ptrはコピー不可です。push_back時はstd::moveを使用し、所有権を明示的に移譲してください。
改善例
改善例③
logs.push_back(std::move(logEntry));
観点チェックリスト
まとめ
所有権移譲レビューは「API契約の読み解きレビュー」です。
スマートポインタ導入後の設計では
「引数型を見れば責務が読める」
状態に仕上げることが最大のレビューゴールです。