この記事のポイント

  • 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);
}
@Reviewer
shared_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演算子の排除状況を監視しながら、
設計責任の可読性と例外安全性を読み取る訓練が求められます。

UML Diagram