この記事のポイント

  • unique_ptrとshared_ptrの役割と設計責務を整理できる
  • 責務の線引きが不明瞭なコードをレビューで正しく読み解ける
  • スマートポインタ設計の失敗パターンを防止する観点が身につく

そもそも責務切り分けとは何を言っているのか

C++のスマートポインタは「メモリリークを防ぐ道具」として語られることが多いですが、本質は 「責務の明示」 です。

  • 誰がそのオブジェクトを所有するのか?
  • いつ解放されるべきか?
  • 何箇所から所有されうるのか?

この問いに対する答えがコードに埋め込まれているかどうか──
それをレビューアーは確認します。

unique_ptrの責務

  • 所有権を一元化する
  • 明確に所有権を移譲(move)できる
  • 所有者がスコープ終了と共に破棄する

shared_ptrの責務

  • 複数の責務が所有権を共有する必要がある時だけ使用
  • 参照カウントで寿命を延長する
  • 循環参照リスクが発生しうる

責務切り分けレビューとは、「unique_ptrで済むところにshared_ptrを使っていないか?」 を読み解く仕事とも言えます。

なぜこれをレビューするのか

責務境界が曖昧な設計は、以下の設計事故を引き起こします。

  • 破棄責任の衝突(ダブルdelete・リーク)
  • ユースアフターフリーの発生
  • テスト容易性の低下
  • 循環参照による不具合検知困難化

スマートポインタは「自動破棄の道具」ではなく、責任を埋め込む言語機能です。
レビューアーはその責任分布を正確に読み解く必要があります。

レビューアー視点

  • 常にunique_ptrから設計意図を読む
  • shared_ptr使用理由は必ず説明可能かを確認
  • API契約上の所有権移譲が明示されているか確認
  • 循環参照の封じ込め策(weak_ptr活用)が組み込まれているか確認

開発者視点

  • 「とりあえずshared_ptr」はNG
  • コンテナ格納・処理フローはunique_ptr主体で設計
  • shared_ptrを導入するなら共有責務の理由を文書化
  • weak_ptrで生存期間循環を防ぐ設計を怠らない

良い実装例

良い設計例
// ログデータ定義
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 << "[LOG] RequestId: " << logEntry.requestId << std::endl;
    }
};

// Controllerは所有責務を一元管理
class RequestController {
public:
    void process() {
        auto logEntry = std::make_unique<ApiRequestLog>();
        logEntry->requestId = 123;

        logger.logRequest(*logEntry);
        storeLog(std::move(logEntry));
    }

private:
    void storeLog(std::unique_ptr<ApiRequestLog> entry) {
        logs.push_back(std::move(entry));
    }

    ApiLogger logger;
    std::vector<std::unique_ptr<ApiRequestLog>> logs;
};

良いポイント

  • 所有権はControllerが一元保持(unique_ptr)
  • Loggerは読み取り専用(const参照)
  • API契約が読みやすく、破棄責任が明確化

レビュー観点

  • unique_ptrを設計の基本単位にしているか
  • shared_ptrは正当な理由があって採用されているか
  • 所有権移譲APIは明示的か
  • 非所有APIはconst参照で統一しているか
  • 共有責務発生時は必ずweak_ptrで循環制御しているか
  • テスト可能性(mock挿入・ライフサイクル制御)が保たれているか

良くない実装例: ケース1(shared_ptrの乱用)

問題例①
class ApiLogger {
public:
    void logRequest(std::shared_ptr<ApiRequestLog> logEntry) {
        if (!logEntry) {
            std::cout << "[WARN] Null log entry" << std::endl;
            return;
        }
        std::cout << "[LOG] RequestId: " << logEntry->requestId << std::endl;
    }
};

class RequestController {
public:
    void process() {
        auto logEntry = std::make_shared<ApiRequestLog>();
        logEntry->requestId = 123;

        logger.logRequest(logEntry);
        logs.push_back(logEntry);
    }

private:
    ApiLogger logger;
    std::vector<std::shared_ptr<ApiRequestLog>> logs;
};
@Reviewer
所有権の共有が不要な場面でshared_ptrを使用しています。責務分離が曖昧になります。Controller内でunique_ptrに統一し、Loggerにはconst参照で渡す構造に修正してください。

問題点

  • 共有所有の必要がないのにshared_ptrを使用
  • 所有責務の可読性が崩れる
  • 将来的に循環参照を誘発しやすくなる

改善例

改善例①
class ApiLogger {
public:
    void logRequest(const ApiRequestLog& logEntry) {
        std::cout << "[LOG] RequestId: " << logEntry.requestId << std::endl;
    }
};

class RequestController {
public:
    void process() {
        auto logEntry = std::make_unique<ApiRequestLog>();
        logEntry->requestId = 123;

        logger.logRequest(*logEntry);
        storeLog(std::move(logEntry));
    }

private:
    void storeLog(std::unique_ptr<ApiRequestLog> entry) {
        logs.push_back(std::move(entry));
    }

    ApiLogger logger;
    std::vector<std::unique_ptr<ApiRequestLog>> logs;
};

良くない実装例: ケース2(API契約の切り分け不在)

問題例②
class ApiLogger {
public:
    void registerLog(std::unique_ptr<ApiRequestLog> logEntry) {
        logs.push_back(std::move(logEntry));
    }

    void backupLogs(std::vector<std::shared_ptr<ApiRequestLog>> backups) {
        for (auto& entry : backups) {
            logs.push_back(std::move(entry));
        }
    }

private:
    std::vector<std::unique_ptr<ApiRequestLog>> logs;
};
@Reviewer
所有権移譲API(unique_ptr)と共有API(shared_ptr)が混在し契約が破綻しています。統一方針に整理してください。全て所有移譲に統一するならbackupLogsもunique_ptr受取に修正してください。

問題点

  • 複数APIで所有契約がブレている
  • 呼び出し側がどの契約で呼べばよいか混乱
  • スマートポインタ設計の破綻

改善例

改善例②
class ApiLogger {
public:
    void registerLog(std::unique_ptr<ApiRequestLog> logEntry) {
        logs.push_back(std::move(logEntry));
    }

private:
    std::vector<std::unique_ptr<ApiRequestLog>> logs;
};

観点チェックリスト


まとめ

unique_ptrとshared_ptrの責務切り分けを読み解くのは、
設計レビューの「読解力」そのもの です。

  • unique_ptrは責務集中の道具
  • shared_ptrは責務分散の道具

レビューアーは
「なぜunique_ptrで済ませなかったのか?」
を常に問う姿勢が重要になります。

UML Diagram