参照とポインタ、どっち使う?

2024/05/14に公開

参照とポインタ、どっち使う?

はじめに

長い歴史を持つ C++ では後方互換性のために、同一の役割を果たす複数の手段が提供されていることがあります。本記事では、その中でも特に混乱しやすい「参照」と「ポインタ」の使い分けについて整理します。

おさらい

参照のポインタに対する仕様上の違いは、以下のとおりです。C言語由来のポインタはメモリ上のアドレスを直接操作するために自由度が高く、ヌルポインタや領域外アクセスなどの不具合の原因となっていました。これを「他のオブジェクトを指し示す」というユースケースに限定し、誤用しないよう意図的に制限を加えたものが参照です。

  • 必ず初期化が必要である
  • 一度初期化すると、別のオブジェクトを参照するよう変更できない
  • nullptr のような空値を取ることができない
  • アドレスを持たない。変数 int a に対する参照 int& b{a} のアドレスは &a == &b となる
  • 「ポインタへのポインタ」(int**) と異なり、「参照への参照」という概念がない
  • ++, -= などの算術演算ができない

原則

既存のコードに従う

新規のプロジェクトではなく、既存のプロジェクトに変更を加える場合には、そのプロジェクトにおけるコーディング規約を優先するべきです。もしコーディング規約に明記されていない場合には、類似のコードから推察されるポリシーに従います。複数のポリシーを混ぜるのは避けて、一貫性を最優先すべきです。

可能な限り参照を使う

可能な限り参照を使用し、必要な場合に限りポインタを使用します。
C++の参照は、もともとCのポインタが持つ諸課題を解消するために導入されたものですので、原則として参照を優先すべきです。

isocpp.org には以下のように説明されています。

「再装着」する必要がない場合は、通常、ポインタよりも参照の方が優先されます。これは通常、参照がクラスの public インターフェイスで最も役立つことを意味します。通常、参照はオブジェクトの表面に表れ、ポインタは内部に表れます。

上記の例外は、関数のパラメーターまたは戻り値に「番兵」としての参照、つまりオブジェクトを参照しない参照が必要な場合です。これは通常、ポインタを返したり受け取ったりする際、nullptr 値にこの特別な意味を与えるときに最もよく行われます。

ただし、nullptr値に特別な意味を持たせる操作は、ポインタを使わなくとも C++17 では std::optional, C++23 では std::expected によって意図を明示することができます。

ガイドライン

C++ Core Guidelines の下図に従います。

C++ のデフォルトは値渡し・値返しです。これらで問題がある場合のみ、参照を検討します。ポインタは限られた場面でのみ使用します。

戻り値

値返し

特に明確な理由がなければ、値戻しを優先して選択すべきです。オブジェクトの所有権やスレッド間同期にまつわる問題を回避できます。速度面でもコピーの省略 によって処理コストは発生しません。

複数の値を返したい場合、出力用の非 const 参照やポインタを引数に加えるよりも、構造体や std::tuple で返却することで、可読性の高いコードにすることができます。C++17 以降は 構造化束縛 によって、より自然に複数の戻り値を扱うことができます。

参照返し

ムーブのコストが高いオブジェクトを返したいとき(巨大な std::array など)や、ポリモーフィズムのために継承の基底を返したいときに使用します。

参照を返すときには常に生存期間を考慮する必要があります。ローカル変数を戻り値としてしまうと、関数を抜けた瞬間にオブジェクトが破棄され、不定な値が返ってしまいます。また、メンバ変数への参照を返す場合も、そのオブジェクトが破棄されて以降は、呼び出し元は戻り値を使用することができません。

ポインタ返し

参照戻しと同様のケースで、オブジェクトの所有権を移転させたいとき(返したいオブジェクトの生存権を呼び出し先では管理したくないとき)に使用します。通常は std::make_unique でスマートポインタとして返します。

引数

値渡し

int, std::string_view, std::span などのコピーコストの低い型や std::unique_ptr などコピーできない型を受け取る場合には、シンプルな値渡しを選択します。

参照渡し

コピーのコストが比較的高い場合 const 参照渡しを選択します。「比較的高い」の基準は C++ Core Guideline によると「int 数個分」とあります。
呼び出し先で引数のコピーを保持する必要がある場合、右辺値参照で受け取り std::move で格納します。

ポインタ渡し

オブジェクトの所有権を移譲もしくは共同所有したいときには、スマートポインタを選択します。

ちなみにROSなどでは shared_ptr のコピーコストを回避するために const shared_ptr<X>& という渡し方がよく使われます。shared_ptr のコピーは排他制御を伴うため、呼び出しに多少の負荷がかかるためです。この構文においては参照先の shard_ptrconst のため、参照カウンタの更新は行われません。

メンバ変数

コピーもしくはムーブ可能なクラスは、参照をメンバ変数に持つべきではありません。参照は初期化のみ可能で途中の値変更ができないため、コピー・ムーブコンストラクタは実装できる一方で、コピー・ムーブ代入演算子を実装することができず、非対称な実装になってしまうためです (参考: C++ Core Guidelines)。

そのため他のオブジェクトを指し示すには、(スマート)ポインタで保持する必要があります。個人的にはコピー・ムーブの可否で参照・ポインタを使い分けるのは混乱するため、メンバ変数は一律ポインタでよいと思っています。

なお、同様の理由でメンバ変数を const にすることも非推奨です。できる限り const をつける習慣があると少し気持ち悪く感じますが、メンバ変数は殆どの場合 private なはずなので、変更不可は自クラス内部で管理すればよく、実際に誤って変更処理を実装してしまうことは稀と思われます。(参考: Data members: Never const.)

Discussion