C++では、値渡しまたは定数参照渡しの方が良いですか?
私はどちらがより良い練習であるのだろうかと思っています。定数参照による受け渡しは、変数のコピーを作成していないため、プログラムのパフォーマンスが向上することを理解しています。
以前は一般的に推奨されるベストプラクティスでした1 組み込み型(char
、int
、double
などを除く、 all types のconst refによるパスを使用します。 )、イテレータおよび関数オブジェクトの場合(ラムダ、std::*_function
から派生したクラス)。
これは、 moveセマンティクスが存在する前は特に当てはまりました。理由は簡単です。値で渡した場合、オブジェクトのコピーを作成する必要があり、非常に小さなオブジェクトを除き、これは常に参照を渡すよりも高価です。
C++ 11では、 moveセマンティクス が得られました。簡単に言えば、移動セマンティクスにより、場合によっては、オブジェクトをコピーせずに「値で」渡すことができます。特に、これは、渡すオブジェクトが rvalue の場合です。
それ自体、オブジェクトを移動することは、少なくとも参照渡しと同じくらい高価です。ただし、多くの場合、関数はとにかくオブジェクトを内部的にコピーします。つまり、引数の ownership を取ります。2
これらの状況では、次の(簡略化された)トレードオフがあります。
「値渡し」では、オブジェクトが右辺値でない限り、オブジェクトがコピーされます。右辺値の場合、代わりにオブジェクトを移動できるため、2番目のケースは突然「コピー、移動」ではなく、「移動、(場合によっては)再び移動」します。
適切な移動コンストラクター(ベクトル、文字列など)を実装するラージオブジェクトの場合、2番目のケースは vastly よりも効率的です。したがって、関数が引数の所有権を取得し、オブジェクトタイプが効率的な移動をサポートしている場合は、値渡しを使用するをお勧めします。
歴史的なメモ:
実際、最新のコンパイラーは、値渡しが高価な場合を把握し、可能であればconst refを使用するように暗黙的に呼び出しを変換できる必要があります。
理論上。実際には、コンパイラは関数のバイナリインターフェイスを壊さずにこれを常に変更できるわけではありません。いくつかの特別な場合(関数がインライン化されている場合)、関数のアクションによって元のオブジェクトが変更されないとコンパイラーが判断できる場合、コピーは実際に省略されます。
しかし、一般的にコンパイラはこれを判断できません。C++での移動セマンティクスの出現により、この最適化の関連性ははるかに低くなりました。
1 例えば。 Scott Meyersの Effective C++ 。
2 これは、引数を取り、構築されたオブジェクトの状態の一部となるように内部に保存する可能性のあるオブジェクトコンストラクターに特に当てはまります。
Edit: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&)の場合、コンパイラはそれを想定できませんでした。 g.iはf-> iによってエイリアスされ、7の値を持つ必要があるため、コンパイラはメモリからg.iの新しい値を再フェッチする必要があります。
より実用的なルールについては、ここに Move Constructors の記事(強くお勧めの読書)にあるルールの良いセットがあります。
上記の「プリミティブ」とは、基本的には数バイトの長さで、ポリモーフィック(イテレータ、関数オブジェクトなど)でもなく、コピーの費用もかからない小さなデータ型を意味します。その論文には、もう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 */
}
タイプに依存します。参照と逆参照を行う必要があるという小さなオーバーヘッドを追加しています。デフォルトのコピーアクターを使用しているポインターと同じか小さいサイズの型の場合、おそらく値渡しが高速になります。
指摘されているように、タイプに依存します。組み込みデータ型の場合、値で渡すのが最適です。 1組のintなどの非常に小さな構造体でも、値を渡すことでパフォーマンスが向上します。
以下に例を示します。整数値があり、それを別のルーチンに渡すと仮定します。その値がレジスタに格納されるように最適化されている場合、参照として渡したい場合は、最初にメモリに格納し、次にそのメモリへのポインタを呼び出してスタックに配置する必要があります。値で渡されていた場合、必要なのはスタックにプッシュされるレジスタのみです。 (詳細は、異なる呼び出しシステムとCPUを使用した場合よりも少し複雑です)。
テンプレートプログラミングを行う場合、渡される型がわからないため、通常は常にconst refを渡す必要があります。値で不良なものを渡すことに対するペナルティの受け渡しは、組み込み型を渡すペナルティよりもはるかに悪いです。 const ref。
これは、非テンプレート関数のインターフェイスを設計するときに通常使用するものです:
関数がパラメーターを変更することを望まず、値のコピーが安価な場合(int、double、float、char、boolなど)、値渡しします。std:: string、std :: vector、およびその他に注意してください標準ライブラリのコンテナの
値のコピーにコストがかかり、関数が指す値を変更したくない場合、およびNULLが関数が処理する値である場合、constポインターで渡します。
値がコピーするのにコストがかかり、関数が指す値を変更する必要があり、NULLが関数が処理する値である場合は、非constポインターで渡します。
値のコピーに費用がかかり、関数が参照する値を変更したくない場合、const参照で渡します。ポインタが代わりに使用された場合、NULLは有効な値ではありません。
値のコピーにコストがかかり、関数が参照する値を変更する場合、非定数参照を渡します。代わりにポインターが使用された場合、NULLは有効な値ではありません。
答えを得たようですね。値渡しは高価ですが、必要な場合に使用するコピーを提供します。
規則として、const参照を渡す方が優れています。ただし、関数の引数をローカルで変更する必要がある場合は、値渡しを使用することをお勧めします。一部の基本タイプでは、値渡しと参照渡しの両方でパフォーマンスが一般的に同じです。実際にはポインターによって内部的に表される参照です。たとえば、ポインターの場合、両方のパスがパフォーマンスの点で同じであると期待できます。
経験則として、非クラス型の値とクラスのconst参照。クラスが本当に小さい場合は、おそらく値で渡す方が良いでしょうが、違いは最小限です。あなたが本当に避けたいのは、巨大なクラスを値で渡し、それをすべて複製することです-これは、たとえば、かなりの数の要素を含むstd :: vectorを渡す場合、大きな違いをもたらします。
小さい型には値で渡します。
ビッグ型のconst参照による受け渡し(bigの定義はマシンによって異なる場合があります)ただし、C++ 11では、データを消費する場合は値によって受け渡します。これは、移動セマンティクスを活用できるためです。例えば:
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_
に入れるためにコピーを作成する必要があります。