C++レビュー|生ポインタ利用時の責任範囲明示とレビュー技術
生ポインタ設計レビュー:柔軟さの裏にある責務分離を読み解く
C++レビューの中でも、ある意味「一番空気感を読む必要がある」テーマが生ポインタです。
T*
という極めて単純なシンタックスの裏側に、設計者の意図が詰まっています。
この記事ではまず、「良い生ポインタ利用」をじっくり読み解き、その後に「やりがちな失敗」をレビュー観点付きで整理していきます。
技術視点での良い実装例
今回の題材は APIアクセスのログを記録するLoggerクラス にしてみます。
ログの内容は毎回API呼び出しごとに構造体としてまとめられますが、Logger自体はその情報を「読んで記録するだけ」であり、寿命管理や所有は持ちません。
まさに「読み取り専用の生ポインタ」を使うケースですね。
// ログ記録用のデータ構造
struct ApiRequestLog {
int requestId;
std::string endpoint;
std::string clientIp;
int responseCode;
time_t requestedAt;
};
// ログ記録クラス
class ApiLogger {
public:
// 読み取り専用参照として生ポインタを利用
void logRequest(const ApiRequestLog* logEntry) {
if (!logEntry) {
std::cout << "[WARN] Null log entry received" << std::endl;
return;
}
std::cout << "[LOG] RequestId: " << logEntry->requestId << std::endl;
std::cout << "[LOG] Endpoint: " << logEntry->endpoint << std::endl;
std::cout << "[LOG] ClientIp: " << logEntry->clientIp << std::endl;
std::cout << "[LOG] ResponseCode: " << logEntry->responseCode << std::endl;
}
};
ポイントを整理するとこうなります。
- 生ポインタは非所有参照用途
- 破棄責任は呼び出し元に集約
- ヌル許容は明確に考慮
- スマートポインタとも競合しない構造
さらに呼び出し側の利用例も見ておきます。
void processRequest() {
ApiRequestLog logEntry {
.requestId = 1001,
.endpoint = "/api/items",
.clientIp = "192.168.0.10",
.responseCode = 200,
.requestedAt = std::time(nullptr)
};
ApiLogger logger;
logger.logRequest(&logEntry); // 参照を渡すだけ
}
寿命はローカル変数のスコープが管理しており、Loggerは参照して読むだけです。
これが最もシンプルで安全な生ポインタ設計の典型パターンです。
生ポインタをレビューするときは 「なぜこれがスマートポインタではなく生ポインタなのか?」 という設計意図の読み解きがスタート地点になります。
良くない実装①:破棄責任が曖昧な生ポインタ設計
さて、ここからがレビューアーの本領発揮フェーズです。
よくある問題パターンから見ていきます。
問題コード例①
class ApiLogger {
public:
void logRequest(ApiRequestLog* logEntry) {
if (logEntry == nullptr) {
std::cout << "[WARN] Null log entry" << std::endl;
return;
}
std::cout << "[LOG] RequestId: " << logEntry->requestId << std::endl;
delete logEntry;
@Reviewer呼び出し元が所有権を持つべき箇所でAPI内部がdeleteすると責務が衝突します。破棄責任をAPIから呼び出し側へ移し、API側では破棄処理を削除してください。 }
};
指摘ポイント
- 生ポインタを受け取っておきながらdeleteしてしまっている
- 呼び出し側とAPI側で 破棄責任の二重管理 になる
- スマートポインタ活用の道も塞がれてしまう
改善例
class ApiLogger {
public:
void logRequest(const ApiRequestLog* logEntry) {
if (!logEntry) {
std::cout << "[WARN] Null log entry" << std::endl;
return;
}
std::cout << "[LOG] RequestId: " << logEntry->requestId << std::endl;
}
};
- 読み取り専用に限定し、破棄責任は呼び出し側に戻しています
- const指定で読み取り専用を明確化
生ポインタ+const+破棄責任は原則「セットで管理」と意識しましょう。
良くない実装②:コピー所有と参照責務の混在
次にやりがちなのが「コピーもするのに参照も残す」という設計崩壊パターンです。
問題コード例②
class ApiLogger {
public:
void logRequest(ApiRequestLog* logEntry) {
if (!logEntry) return;
internalLog = *logEntry; // コピー保持
savedPointer = logEntry; // 参照も保存
@Reviewerコピーと参照を両立させると寿命管理が二重化して危険です。savedPointerは削除し、コピー所有に責任を統一してください。もしくは参照のみ保持する設計に変更してください。 }
private:
ApiRequestLog internalLog;
ApiRequestLog* savedPointer;
};
指摘ポイント
- コピーした時点でオブジェクト所有はLogger側に移っているべき
- savedPointer保持に意味がなくなる
- 破棄責任の所在が読み取れなくなる
改善例
class ApiLogger {
public:
void logRequest(const ApiRequestLog& logEntry) {
internalLog = logEntry;
}
private:
ApiRequestLog internalLog;
};
- 参照受け取り+コピー保持に整理
- 寿命管理は完全にLoggerクラス内部のみで完結
「参照受け取り → コピー所有に切り替える」パターンは保守性・安全性が高くレビューでも高評価を受けやすい設計です。
レビュー観点チェックリスト(生ポインタ設計専用)
レビュー時は以下の観点で読み解いていきます。
観点 | 内容 |
---|---|
所有権の明示 | 誰がdelete責任を持つか一貫しているか |
スマートポインタ検討 | 生ポインタ利用が本当に妥当か再確認 |
const利用 | 読み取り専用用途ならconst指定が適切か |
ヌル許容 | nullptr許容が明示的に設計されているか |
API契約整合性 | 呼び出し側との契約が説明可能か |
寿命管理の単純化 | ライフサイクルが複雑化していないか |
テスト容易性 | モックやスタブ可能な設計になっているか |
あとがき
生ポインタは「柔軟」「軽量」「歴史的資産互換」と、良い面もたくさんあります。
しかしレビューアーが注目すべきは常に「責務の分離と契約の読みやすさ」です。
スマートポインタ全盛の今だからこそ、
「あえて生ポインタにしている理由」 を読み取る目がレビュー品質を決定づけます。