C++レビュー|unique_ptrとshared_ptrの使い分け基準とレビュー観点
この記事のポイント
- unique_ptrとshared_ptrの使い分け基準をレビューアー視点で整理
- 所有権、共有責任、API契約、ライフサイクル管理を設計に反映するポイントが分かる
- スマートポインタの使い方で設計センスを読み取る訓練ができる
そもそもスマートポインタとは
C++におけるスマートポインタは、所有権とライフサイクル管理をコードに明示的に埋め込む仕組みです。標準ライブラリには以下の主要なスマートポインタがあります。
std::unique_ptr<T>
所有権を唯一に限定し、スコープ外で自動的に破棄します。std::shared_ptr<T>
参照カウントで複数箇所から共有所有を可能にします。std::weak_ptr<T>
shared_ptr
の循環参照対策用の非所有参照です。
生ポインタが「何も言っていない」のに対し、スマートポインタは設計意図を伝えます。
レビューアーは「スマートポインタが適切に使われているか」で設計責務を読み解くことができます。
なぜこれをレビューするのか
スマートポインタは便利ですが、何でも使えばよいわけではありません。
むしろ 「選び方にその設計者の理解度が出る」 重要な設計要素です。
- unique_ptr → そのオブジェクトは誰のものか
- shared_ptr → どこまで共有される想定か
- weak_ptr → どの共有関係を断ち切るか
これらはすべて 責務境界の明示 です。
レビューアーは以下を読み取る必要があります。
- 破棄責任が一元化できているか
- shared_ptrを安易に使っていないか(共有責務を乱立させていないか)
- API契約として破棄責任を隠蔽できているか
- 循環参照に対策しているか
レビューアー視点
- unique_ptrで済む設計をshared_ptrで複雑化していないか
- shared_ptrが本当に「複数所有が必要な責務」なのか
- APIが所有権移譲の契約になっているかどうか
- 循環参照防止のweak_ptrは入っているか
- テスト容易性は担保できているか
開発者視点
- まずunique_ptrで考える
- どうしても共有したい責務だけshared_ptrを採用
- shared_ptrを渡すAPIは所有関係まで契約する行為と認識
- コンテナ格納・移動はunique_ptr中心に組み立てる
- 自動破棄が安心できる設計に寄せる
良い実装例
良い設計例
// ApiRequestLogの定義
struct ApiRequestLog {
int requestId;
std::string endpoint;
std::string clientIp;
int responseCode;
time_t requestedAt;
};
// Loggerは非所有で読み取り専用設計
class ApiLogger {
public:
void logRequest(const ApiRequestLog& logEntry) {
std::cout << "RequestId: " << logEntry.requestId << std::endl;
}
};
// 処理フロー側で所有権を持つ
void handleRequest() {
auto logEntry = std::make_unique<ApiRequestLog>();
logEntry->requestId = 123;
ApiLogger logger;
logger.logRequest(*logEntry);
}
良いポイント
- 所有権はhandleRequestが持つ(unique_ptr)
- Loggerは読み取り専用(const&渡し)
- API契約は責務分界が明確
レビュー観点
- 所有権移譲: unique_ptrで設計できる範囲を過剰にshared_ptr化していないか
- 共有責務: shared_ptrは責任共有が必要な時だけ登場しているか
- API契約: スマートポインタをAPIに露出させた場合の契約意図が説明可能か
- ライフサイクル安全性: weak_ptrが適切に循環を断っているか
- テスト容易性: モック・スタブ挿入しやすい設計になっているか
良くない実装例: ケース1(安易なshared_ptr利用)
問題例: shared_ptr乱用
class ApiLogger {
public:
void logRequest(std::shared_ptr<ApiRequestLog> logEntry) {
if (!logEntry) {
std::cout << "Null log entry" << std::endl;
return;
}
std::cout << logEntry->requestId << std::endl;
}
};
void handleRequest() {
auto logEntry = std::make_shared<ApiRequestLog>();
logEntry->requestId = 123;
ApiLogger logger;
logger.logRequest(logEntry);
}
@Reviewerunique_ptrで所有権を一元化できる場面でshared_ptrを使用すると、不要な共有責務が発生し設計が複雑になります。logRequestはconst参照で受け取り、handleRequest側でunique_ptr管理に修正してください。
問題点
- 所有権移譲が不要な場面でもshared_ptrを使ってしまっている
- 共有所有の責務が意味なく付加されてしまう
- ライフサイクル設計が曖昧化
改善例
改善例: unique_ptr中心に整理
class ApiLogger {
public:
void logRequest(const ApiRequestLog& logEntry) {
std::cout << logEntry.requestId << std::endl;
}
};
void handleRequest() {
auto logEntry = std::make_unique<ApiRequestLog>();
logEntry->requestId = 123;
ApiLogger logger;
logger.logRequest(*logEntry);
}
「渡し方の原則」
- const&渡し:純粋な読み取りAPI
- unique_ptr渡し:所有権移譲API
- shared_ptr渡し:共有責務契約API
レビューアーはこの整理軸を必ず頭に置きます。
良くない実装例: ケース2(API契約の曖昧さ)
問題例: 移譲契約の不統一
class ApiLogger {
public:
void registerLog(std::unique_ptr<ApiRequestLog> logEntry) {
logs.push_back(std::move(logEntry));
}
void logRequest(ApiRequestLog* logEntry) {
std::cout << logEntry->requestId << std::endl;
}
private:
std::vector<std::unique_ptr<ApiRequestLog>> logs;
};
@ReviewerregisterLogは所有権移譲API、logRequestは非所有APIとなっています。契約方針が統一されていません。API方針を統一し、両APIとも所有権移譲ならunique_ptr、参照のみならconst&に統一してください。
問題点
- registerLogとlogRequestでポインタ契約方針が分かれている
- 呼び出し側の理解コストが高い
- API利用ミスを誘発しやすい
改善例
改善例: API契約方針の統一
class ApiLogger {
public:
void registerLog(std::unique_ptr<ApiRequestLog> logEntry) {
logs.push_back(std::move(logEntry));
}
void logRequest(const ApiRequestLog& logEntry) {
std::cout << logEntry.requestId << std::endl;
}
private:
std::vector<std::unique_ptr<ApiRequestLog>> logs;
};
観点チェックリスト
まとめ
スマートポインタは「自動破棄してくれる便利な道具」ではありません。
破棄責任をどこが持つのか? という設計責務をコード上に明示させるツールです。
レビューアーはポインタ選択の裏にある意図を正しく読み取り、責任が読める設計になっているかどうかを丁寧に確認していく必要があります。