値渡しはC ++ 11の妥当なデフォルトですか?


142

従来のC ++では、値による関数やメソッドへの受け渡しは、ラージオブジェクトの場合は遅く、一般に不満を抱きます。代わりに、C ++プログラマーは参照を渡す傾向がありますが、これは高速ですが、所有権、特にメモリ管理(オブジェクトがヒープに割り当てられている場合)に関するあらゆる種類の複雑な質問が生じます。

これで、C ++ 11には、Rvalue参照と移動コンストラクターがあります。つまりstd::vector、値を関数に渡したり、関数から渡したりするのが簡単な(のような)ラージオブジェクトを実装することができます。

では、これは、デフォルトでは、std::vectorやなどの型のインスタンスの値で渡す必要があることを意味しますstd::stringか?カスタムオブジェクトについてはどうですか?新しいベストプラクティスは何ですか?


22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated)。所有権がどのように複雑で問題があるのか​​わかりませんか?私は何かを逃したかもしれませんか?
iammilind

1
@iammilind:個人的な経験からの例。1つのスレッドには文字列オブジェクトがあります。これは別のスレッドを生成する関数に渡されますが、呼び出し元には不明であり、関数は文字列をconst std::string&コピーではなく取得しました。その後、最初のスレッドが終了しました...
Zan Lynx

12
@ZanLynx:スレッド関数として呼び出されるように設計されていない関数のように思えます。
Nicol Bolas、2011

5
iammilindに同意しても問題はありません。const参照による受け渡しは、「大きな」オブジェクトではデフォルトであり、小さなオブジェクトでは値による受け渡しです。大小の制限を約16バイト(32ビットシステムでは4つのポインタ)に設定します。
JN、2011

3
ハーブサッターの基本に戻る!CppCon での最新のC ++プレゼンテーションの要点は、これについてかなり詳細に説明されています。ビデオはこちら
Chris Drew

回答:


138

本文内にコピーを作成する必要がある場合は、これが妥当なデフォルトです。これがDave Abrahamsの主張です。

ガイドライン:関数の引数をコピーしないでください。代わりに、それらを値で渡し、コンパイラーにコピーを行わせます。

コードでは、これを行わないことを意味します。

void foo(T const& t)
{
    auto copy = t;
    // ...
}

しかし、これを行います:

void foo(T t)
{
    // ...
}

これには、呼び出し元が次のfooように使用できるという利点があります。

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

最小限の作業のみが行われます。あなたは参照して同じことを行うには2つのオーバーロードを必要とする、と思いますvoid foo(T const&);void foo(T&&);

それを念頭に置いて、私は今、自分の大切なコンストラクタをそのように書きました:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

それ以外の場合は、リファレンスへの参照をconstまだ渡すことは妥当です。


29
+1、特に最後のビットについて:)移動元のオブジェクトが後で変更されないことが予期されSomeProperty p; for (auto x: vec) { x.foo(p); }ない場合(たとえば、適合しない場合)にのみ、Moveコンストラクターを呼び出すことができることを忘れないでください。また、Moveコンストラクターはコストがかかります(オブジェクトが大きいほどコストが高くなります)const&が、基本的には無料です。
Matthieu M.

25
@MatthieuM。ただし、移動の「オブジェクトが大きいほど、コストが高くなる」が実際に何を意味するかを知ることは重要です。「大きい」は、実際には「より多くのメンバー変数」を意味します。たとえばstd::vector、100万個の要素を持つを移動すると、ベクトル内のすべてのオブジェクトではなく、ヒープ上の配列へのポインターのみが移動するため、5個の要素を持つものを移動するのと同じコストがかかります。したがって、実際にはそれほど大きな問題ではありません。
Lucas

+1 C ++ 11を使い始めてから、pass-by-value-then-move構文も使用する傾向があります。これは私が私のコードは、今持っているので、多少不安かかわらを感じさせるstd::moveあらゆる場所に...
stijn

1
にはリスクが1つありconst&ます。 void foo(const T&); int main() { S s; foo(s); }。これは、型が異なっていても、Sを引数として取るTコンストラクターがある場合にコンパイルできます。大きなTオブジェクトが構築される可能性があるため、これは遅くなる可能性があります。コピーせずに参照を渡すと思うかもしれませんが、そうするかもしれません。詳細を尋ねた質問に対するこの回答をご覧ください。基本的に、&通常は左辺値のみにバインドしますが、には例外がありrvalueます。代替案があります。
Aaron McDaid 2013年

1
@AaronMcDaidこれは古いニュースです。これは、C ++ 11の前でさえも、あなたが常に注意しなければならないことです。そして、それに関しては何も変わっていません。
Luc Danton

71

ほとんどすべての場合、セマンティクスは次のいずれかである必要があります。

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

他のすべての署名は控えめに使用し、正当な理由がある必要があります。コンパイラーは、ほとんどの場合、これらを最も効率的な方法で処理します。あなたはあなたのコードを書くことで始めることができます!


2
引数を変更する場合は、ポインターを渡すことをお勧めします。Googleスタイルガイドに同意します。これにより、関数のシグネチャ(google-styleguide.googlecode.com/svn/trunk/…)を確認する必要なく引数が変更されることがより明確になります。
Max Lybbert 2011

40
ポインタを渡すのが嫌いなのは、関数に障害状態が発生する可能性があるためです。私は、彼らが証明可能正しいように、それは非常に隠れるにバグのためのスペースを減少させるので、私のすべての関数を記述してみてください。foo(bar& x) { x.a = 3; }より信頼性の高い(と読める!)たくさんの一体であるfoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjayは

22
@Max Lybbert:ポインタパラメータを使用すると、関数のシグネチャを確認する必要はありませんが、ドキュメントを確認して、nullポインタを渡すことが許可されているかどうか、関数が所有権を取得するかどうかなどを確認する必要があります。ポインタパラメータは、非const参照よりもはるかに少ない情報を伝達します。ただし、引数が変更される可能性があることを(refC#のキーワードのように)呼び出しサイトで視覚的に知ることができればすばらしいと思います。
Luc Touraille、2009

値による受け渡しと移動のセマンティクスに依存することに関しては、これらの3つの選択がパラメーターの意図された使用法を説明する上でより良い仕事をすると感じます。これらは私が常に従うガイドラインでもあります。
Trevor Hickey 2013

1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?これらの仮定はどちらも正しくありません。unique_ptrそしてshared_ptrヌル/保持できるnullptr値を。null値について心配したくない場合は、nullになることはないため、参照を使用する必要があります。また->、を入力する必要はありません。これは煩わしいものです:)
Julian

10

関数本体内でオブジェクトのコピーが必要な場合、またはオブジェクトを移動するだけの場合は、値でパラメーターを渡します。const&オブジェクトへの非変更アクセスのみが必要な場合は、渡します。

オブジェクトコピーの例:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

オブジェクト移動の例:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

変化しないアクセスの例:

void read_pattern(T const& t) {
    t.some_const_function();
}

根拠については、Dave AbrahamsXiang Fanによるこれらのブログ投稿を参照してください。


0

関数のシグネチャは、その使用目的を反映する必要があります。可読性は、オプティマイザにとっても重要です。

これは、オプティマイザが最速のコードを作成するための最良の前提条件です。理論的には、少なくとも実際にはそうでなくても、数年後には現実になります。

パフォーマンスの考慮事項は、多くの場合、パラメーターの受け渡しのコンテキストで過大評価されています。完全転送がその一例です。のような関数emplace_backは、ほとんどが非常に短く、とにかくインライン化されています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.