C ++では、値渡しまたは定数参照渡しの方が良いですか?
-
06-07-2019 - |
質問
C ++では、値渡しまたは定数参照渡しの方が良いですか?
私はどちらがより良い方法なのかと思っています。定数参照による受け渡しは、変数のコピーを作成していないため、プログラムのパフォーマンスが向上することを理解しています。
解決
一般的に推奨されるベストプラクティス 1 で、組み込み型( char
、 int
、 double
など)、反復子および関数オブジェクト(ラムダ、 std :: * _ function <から派生したクラス) / code>)。
これは、移動セマンティクスが存在する前は特にそうでした。理由は簡単です。値で渡した場合、オブジェクトのコピーを作成する必要があり、非常に小さなオブジェクトを除き、これは参照を渡すよりも常に高価です。
C ++ 11では、 セマンティクスを移動 できました。簡単に言えば、移動セマンティクスにより、場合によっては、オブジェクトを「値で」渡すことができます。コピーせずに。特に、これは、渡すオブジェクトが rvalue である場合です。
それ自体、オブジェクトの移動は、少なくとも参照渡しと同じくらいの費用がかかります。ただし、多くの場合、関数はとにかくオブジェクトを内部的にコピーします&#8212;つまり、引数の所有権が必要になります。 2
これらの状況では、次の(簡略化された)トレードオフがあります:
- 参照によってオブジェクトを渡し、内部でコピーできます。
- オブジェクトを値で渡すことができます。
&#8220;値渡し&#8221;オブジェクトが右辺値でない限り、オブジェクトがコピーされます。右辺値の場合、代わりにオブジェクトを移動できます。そのため、2番目の場合は突然&#8220;コピーして移動する必要がなくなります。ただし、&#8220;移動してから、(場合によっては)再び移動する&#8221;。
適切な移動コンストラクター(ベクトル、文字列&#8230;など)を実装するラージオブジェクトの場合、2番目のケースは最初のケースよりも非常に効率的です。したがって、関数が引数の所有権を取得し、オブジェクトの種類が効率的な移動をサポートしている場合は、値渡しを使用することをお勧めします。
歴史的なメモ:
実際、現代のコンパイラーは、値渡しが高価な場合を把握し、可能であればconst refを使用するように暗黙的に呼び出しを変換できる必要があります。
理論上。実際には、コンパイラは関数のバイナリインターフェイスを壊さずにこれを常に変更することはできません。いくつかの特別な場合(関数がインライン化されている場合)、コンパイラが関数内のアクションによって元のオブジェクトが変更されないことがわかると、コピーは実際に省略されます。
しかし、一般的にコンパイラはこれを判断できません。C++での移動セマンティクスの出現により、この最適化はあまり重要ではなくなりました。
1 例: Scott Meyersの Effective C ++ 。
2 これは、引数を取り、構築されたオブジェクトの状態の一部となるように内部に保存するオブジェクトコンストラクターの場合に特によく当てはまります。
他のヒント
編集: cpp-nextに関するDave Abrahamsによる新しい記事:
速度が必要ですか?値渡し。
コピーが安価な構造体の値渡しは、コンパイラがオブジェクトがエイリアスしない(同じオブジェクトではない)と想定するという追加の利点があります。参照渡しを使用すると、コンパイラは常にそれを想定できません。簡単な例:
foo * f;
void bar(foo g) {
g.i = 10;
f->i = 2;
g.i += 5;
}
コンパイラはそれを最適化できます
g.i = 15;
f->i = 2;
fとgが同じ場所を共有していないことがわかっているため。 gが参照(foo&amp;)である場合、コンパイラはそれを想定できませんでした。 g.iはf-&gt; iによってエイリアスされ、7の値を持つ必要があるため、コンパイラはメモリからg.iの新しい値を再取得する必要があります。
より実用的なルールについては、コンストラクタの移動の記事(読むことを強くお勧めします)。
- 関数が引数を副作用として変更する場合は、非定数参照で引数を取得します。
- 関数が引数を変更せず、引数がプリミティブ型である場合、値で取得します。
- それ以外の場合は、次の場合を除き、const参照で取得します
- 関数がconst参照のコピーを作成する必要がある場合は、値で取得してください。
&quot; Primitive&quot;上記は基本的に、数バイトの長さでポリモーフィック(イテレータ、関数オブジェクトなど)でもなく、コピーするのに高価でもない小さなデータ型を意味します。その論文には、もう1つのルールがあります。アイデアは、時には引数を変更できない場合にコピーを作成したい場合もあれば、引数が一時的なものである場合に関数内で引数自体を使用したい場合もある場合もあるというものです。 、 例えば)。このペーパーでは、その方法を詳細に説明しています。 C ++ 1xでは、この手法は言語サポートとともにネイティブに使用できます。それまでは、上記の規則に従います。
例:文字列を大文字にし、大文字のバージョンを返すには、常に値を渡す必要があります:とにかくコピーを取得する必要があります(const参照を直接変更することはできません)-透過的にする可能な限り最適化できるように、呼び出し元にできる限り早くコピーを作成します。その論文で詳しく説明されています:
my::string uppercase(my::string s) { /* change s and return it */ }
ただし、とにかくパラメーターを変更する必要がない場合は、constを参照してください:
bool all_uppercase(my::string const& s) {
/* check to see whether any character is uppercase */
}
ただし、パラメーターの目的が引数に何かを書き込むことである場合は、非定数参照で渡します
bool try_parse(T text, my::string &out) {
/* try to parse, write result into out */
}
タイプによって異なります。参照と参照解除を行う必要があるという小さなオーバーヘッドを追加しています。デフォルトのコピーアクターを使用しているポインターと同じか小さいサイズの型の場合、おそらく値渡しが高速になります。
指摘されているように、タイプによって異なります。組み込みデータ型の場合、値で渡すのが最適です。 intのペアなどの非常に小さな構造体でも、値渡しすることでパフォーマンスが向上します。
例を示します。整数値があり、それを別のルーチンに渡すと仮定します。その値がレジスタに格納されるように最適化されている場合、参照として渡したい場合は、最初にメモリに格納し、次にそのメモリへのポインタを呼び出してスタックに配置する必要があります。値で渡されていた場合、必要なのはスタックにプッシュされるレジスタのみです。 (詳細は、異なる呼び出しシステムとCPUを使用した場合よりも少し複雑です)。
テンプレートプログラミングを行う場合、渡される型がわからないため、通常は常にconst refを渡す必要があります。 -const refによるタイプ入力
答えを得たようですね。値渡しは高価ですが、必要な場合に使用するコピーを提供します。
これは、非テンプレート関数のインターフェースを設計するときに通常使用するものです:
-
関数がパラメーターを変更したくない場合は値渡しし、 値のコピーは安価です(int、double、float、char、boolなど。std:: string、std :: vector、および標準ライブラリの残りのコンテナはそうではないことに注意してください)
-
値のコピーにコストがかかり、関数がコピーする場合、constポインターで渡す 指している値を変更したくない、NULLは関数が処理する値です。
-
値のコピーに費用がかかる場合、および関数を非constポインターで渡す 指している値を変更したい場合、NULLは関数が処理する値です。
-
値のコピーに費用がかかり、関数が参照される値を変更したくない場合、const参照に渡します。ポインタが代わりに使用された場合、NULLは有効な値ではありません。
-
値がコピーするのに費用がかかり、関数が参照される値を変更したい場合、非定数参照で渡します。ポインタが代わりに使用された場合、NULLは有効な値ではありません。
規則として、const参照による受け渡しの方が優れています。 ただし、関数の引数をローカルで変更する必要がある場合は、値渡しを使用することをお勧めします。 一部の基本タイプでは、値渡しと参照渡しの両方でパフォーマンスが一般的に同じです。実際には、ポインターによって内部的に表される参照です。たとえば、ポインターの場合、両方のパスはパフォーマンスの点で同じであると期待できます。
経験則として、非クラス型の値とクラスのconst参照。 クラスが本当に小さい場合は、おそらく値で渡す方が良いでしょうが、違いは最小限です。本当に避けたいのは、巨大なクラスを値で渡し、それをすべて複製することです。たとえば、std :: vectorにいくつかの要素が含まれている場合、これは大きな違いになります。
小さなタイプの値渡し。
ビッグ型のconst参照によるパス(bigの定義はマシンによって異なる場合があります)例:
class Person {
public:
Person(std::string name) : name_(std::move(name)) {}
private:
std::string name_;
};
これで呼び出し元のコードは次のようになります。
Person p(std::string("Albert"));
そして、1つのオブジェクトのみが作成され、クラス Person
のメンバー name _
に直接移動されます。 const参照で渡す場合、 name _
に入れるためにコピーを作成する必要があります。
単純な違い:-関数には入力パラメータと出力パラメータがあるため、入力パラメータと出力パラメータが同じ場合は参照渡しを使用し、入力パラメータと出力パラメータが異なる場合は値渡しを使用することをお勧めします。
例 void amount(int account、int deposit、int total)
入力パラメータ:アカウント、預金 出力パラメーター:合計
入力と出力は、vauleによる異なる使用呼び出しです
-
void amount(int total、int deposit)
入金合計 出力合計