C++レビュー|make_uniqueとmake_shared利用統一ルールとレビュー観点
この記事のポイント
- make_uniqueとmake_sharedの利用ルールをレビューアー視点で整理
- new演算子の直接使用禁止ルールを設計責務で理解できる
- 生成と所有の分離をレビューで読み取る訓練ができる
そもそもmake_unique/make_sharedとは
C++11から導入されたスマートポインタ生成ヘルパー関数です。
基本的には 「newを書くな」 を実現する道具になります。
std::make_unique<T>(...)
→ unique_ptr生成専用(C++14以降)std::make_shared<T>(...)
→ shared_ptr生成専用(C++11以降)
これにより次のようなメリットが得られます。
- 例外安全(メモリリークを防ぐ)
- 所有権移譲の意図を明示
- 型推論による記述簡素化
- 重複所有管理の一括化(shared_ptr時)
注意
C++11ではmake_uniqueは標準未提供だが、C++14以降は必須。
現代のレビュー基準では C++14以上前提 で評価するのが通例です。
なぜこれをレビューするのか
実務では以下のようなコードが依然多く残ります。
悪い例
std::unique_ptr<Foo> p(new Foo());
もっと悪い例
auto p = std::shared_ptr<Foo>(new Foo());
レビューアーは次の論点を見逃してはなりません。
- 例外発生時のリソースリーク
- スマートポインタ生成責務のばらつき
- メモリ割当コストの不透明化(特にshared_ptr)
- new演算子の不要露出
レビューアー視点
- newは原則禁止
newはスマートポインタの内側に封じるべき - 生成責務と所有権を同時に埋め込め
make系関数によりコード読み取りコストを削減 - 例外安全確保
生成と所有移譲の原子性を担保
開発者視点
- newを素で書かず、make_unique/make_sharedを常用
- 生成と所有者責務を1行に統合する
- スコープベース寿命管理を徹底
- コンストラクタ側にnew渡しを押し付けない
良い実装例
良い設計例
struct ApiRequestLog {
int requestId;
std::string endpoint;
std::string clientIp;
int responseCode;
time_t requestedAt;
};
class ApiLogger {
public:
void logRequest(const ApiRequestLog& logEntry) {
std::cout << "[LOG] RequestId: " << logEntry.requestId << std::endl;
}
};
void handleRequest() {
auto logEntry = std::make_unique<ApiRequestLog>();
logEntry->requestId = 123;
ApiLogger logger;
logger.logRequest(*logEntry);
}
- 所有権移譲はmake_uniqueが担う
- newは登場しない
- 所有者とライフサイクル管理者が一致
レビュー観点
- new演算子は封じ込められているか
- make_unique/make_sharedで生成統一されているか
- 所有権移譲が暗黙でなくコードに書き込まれているか
- 例外安全が保たれているか
- ライフサイクルがスコープで読めるか
良くない実装例: ケース1(newによる生成バラバラ)
問題例①
class ApiLogger {
public:
void logRequest(const ApiRequestLog& logEntry) {
std::cout << "[LOG] RequestId: " << logEntry.requestId << std::endl;
}
};
void handleRequest() {
std::unique_ptr<ApiRequestLog> logEntry(new ApiRequestLog());
logEntry->requestId = 123;
ApiLogger logger;
logger.logRequest(*logEntry);
}
@Reviewerスマートポインタ生成にnewを使わずmake_uniqueを使用してください。所有権移譲と生成を一行に統合し、例外安全性も確保できます。
問題点
- 例外安全性が損なわれる(new後にunique_ptr代入前に例外発生するとリーク)
- 所有権移譲責務が不明確になる
- 生成責務が散らばる
改善例
改善例①
void handleRequest() {
auto logEntry = std::make_unique<ApiRequestLog>();
logEntry->requestId = 123;
ApiLogger logger;
logger.logRequest(*logEntry);
}
良くない実装例: ケース2(shared_ptr生成責務の分離)
問題例②
void handleRequest() {
auto raw = new ApiRequestLog();
raw->requestId = 123;
std::shared_ptr<ApiRequestLog> logEntry(raw);
ApiLogger logger;
logger.logRequest(*logEntry);
}
@Reviewershared_ptrの生成もmake_sharedを使用してください。newを隠蔽し、共有所有の意図もコード上で明示できます。
問題点
- newの露出でライフサイクル責任が読めない
- 例外安全性も損なわれる
- 共有所有契約がコード上で曖昧化
改善例
改善例②
void handleRequest() {
auto logEntry = std::make_shared<ApiRequestLog>();
logEntry->requestId = 123;
ApiLogger logger;
logger.logRequest(*logEntry);
}
良くない実装例: ケース3(ライブラリ統一不足)
問題例③
class ApiFactory {
public:
std::unique_ptr<ApiRequestLog> createLog() {
return std::unique_ptr<ApiRequestLog>(new ApiRequestLog());
}
};
@Reviewerファクトリ内でもmake_uniqueを使用してください。統一規約に従うことでコード全体の生成方針が読みやすくなります。
問題点
- チーム内統一ルールが崩れる
- 例外安全対策のばらつき発生
- 他者レビュー時に確認コスト上昇
改善例
改善例③
class ApiFactory {
public:
std::unique_ptr<ApiRequestLog> createLog() {
return std::make_unique<ApiRequestLog>();
}
};
観点チェックリスト
まとめ
make_unique / make_sharedは単なる短縮記法ではありません。
生成責務+所有移譲+例外安全性を統合する設計ツールです。
レビューアーは new演算子の排除状況を監視しながら、
設計責任の可読性と例外安全性を読み取る訓練が求められます。