この記事のポイント

  • ポインタ所有権移譲の設計をレビューアーが読み取る技術を整理
  • 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);
}
@Reviewer
save()が所有権移譲契約なのに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;
};
@Reviewer
unique_ptrはコピー不可です。push_back時はstd::moveを使用し、所有権を明示的に移譲してください。

改善例

改善例③
logs.push_back(std::move(logEntry));

観点チェックリスト


まとめ

所有権移譲レビューは「API契約の読み解きレビュー」です。
スマートポインタ導入後の設計では
「引数型を見れば責務が読める」
状態に仕上げることが最大のレビューゴールです。

UML Diagram