113

私はC++の初心者ですが、プログラミングの初心者ではありません。私はC++(c ++ 11)を学ぼうとしていますが、私にとって最も重要なこと、つまりパラメーターの受け渡しについては少しわかりません。

私はこれらの簡単な例を検討しました:

  • すべてのメンバーのプリミティブ型を持つクラス:
    CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)

  • メンバーとしてプリミティブ型+1個の複合型を持つクラス:
    Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)

  • メンバーとしてプリミティブ型+1個の複合型のコレクションを持つクラス: Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)

アカウントを作成するときは、次のようにします。

    CreditCard cc("12345",2,2015,1001);
    Account acc("asdasd",345, cc);

明らかに、このシナリオではクレジットカードが2回コピーされます。そのコンストラクターを次のように書き直すと

Account(std::string number, float amount, CreditCard& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(creditCard)

コピーが1つあります。書き直せば

Account(std::string number, float amount, CreditCard&& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(std::forward<CreditCard>(creditCard))

2つの動きがあり、コピーはありません。

パラメータをコピーしたい場合もあれば、そのオブジェクトを作成するときにコピーしたくない場合もあると思います。
私はC#から来ており、参照に慣れているので、少し奇妙です。パラメーターごとに2つのオーバーロードがあるはずですが、間違っていることはわかっています。
C ++でパラメーターを送信する方法のベストプラクティスはありますか?それは、些細なことではなく、実際に見つけたからです。上記の私の例をどのように処理しますか?

4

5 に答える 5

164

最初の最も重要な質問:

C ++でパラメーターを送信する方法のベストプラクティスはありますか?それは、些細なことではなく、実際に見つけたからです。

関数が渡される元のオブジェクトを変更する必要がある場合は、呼び出しが戻った後、そのオブジェクトへの変更が呼び出し元に表示されるようにするため、左辺値参照を渡す必要があります。

void foo(my_class& obj)
{
    // Modify obj here...
}

関数が元のオブジェクトを変更する必要がなく、そのコピーを作成する必要がない場合(つまり、その状態を監視するだけでよい場合)、左辺値参照をconst次のように渡す必要があります。

void foo(my_class const& obj)
{
    // Observe obj here
}

これにより、左辺値(左辺値は安定したIDを持つオブジェクト)と右辺値(右辺値は、たとえば一時的なもの、または呼び出しの結果として移動しようとしているオブジェクト)の両方で関数を呼び出すことができますstd::move()

また、基本的なタイプや、、、、などのコピーが高速なタイプの場合、int関数が単に値を監視する必要がある場合は参照で渡す必要はなく、値で渡すことを優先する必要があると主張することもできます。参照セマンティクスが必要ない場合は正しいですが、関数が同じ入力オブジェクトへのポインターをどこかに格納したい場合はどうなりますか?そのポインターを後で読み取ると、他の部分で実行された値の変更が表示されます。コード?この場合、参照渡しが正しい解決策です。boolchar

関数が元のオブジェクトを変更する必要はないが、そのオブジェクトのコピーを保存する必要がある場合おそらく、入力を変更せずに入力の変換の結果を返すため)、値で取得することを検討できます。

void foo(my_class obj) // One copy or one move here, but not working on
                       // the original object...
{
    // Working on obj...

    // Possibly move from obj if the result has to be stored somewhere...
}

上記の関数を呼び出すと、左辺値を渡すときに常に1つのコピーが生成され、右辺値を渡すときに1つの移動が発生します。関数がこのオブジェクトをどこかに格納する必要がある場合は、そこから追加の移動を実行できます(たとえば、データメンバーに値を格納する必要foo()あるメンバー関数の場合)。

タイプのオブジェクトの移動にコストがかかる場合はmy_class、オーバーロードを検討foo()し、左辺値用に1つのバージョン(への左辺値参照を受け入れるconst)と右辺値用に1つのバージョン(右辺値参照を受け入れる)を提供できます。

// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
    my_class copyOfObj = obj; // Copy!
    // Working on copyOfObj...
}

// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
    my_class copyOfObj = std::move(obj); // Move! 
                                         // Notice, that invoking std::move() is 
                                         // necessary here, because obj is an
                                         // *lvalue*, even though its type is 
                                         // "rvalue reference to my_class".
    // Working on copyOfObj...
}

上記の関数は非常に似ているため、実際には、それから1つの関数を作成できますfoo()。関数テンプレートになり、渡されるオブジェクトの移動またはコピーが内部で生成されるかどうかを判断するために完全な転送を使用できます。

template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
//       ^^^
//       Beware, this is not always an rvalue reference! This will "magically"
//       resolve into my_class& if an lvalue is passed, and my_class&& if an
//       rvalue is passed
{
    my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
    // Working on copyOfObj...
}

スコットマイヤーズによるこの講演を見て、このデザインについてもっと知りたいと思うかもしれません(彼が使用している「ユニバーサルリファレンス」という用語は非標準であるという事実に注意してください)。

覚えておくべきことの1つは、std::forward通常は右辺値の移動につながるため、比較的無害に見えても、同じオブジェクトを複数回転送すると問題が発生する可能性があります。たとえば、同じオブジェクトから2回移動する場合などです。したがって、これをループに入れないように注意してください。また、関数呼び出しで同じ引数を複数回転送しないように注意してください。

template<typename C>
void foo(C&& obj)
{
    bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}

また、コードが読みにくくなるため、正当な理由がない限り、通常はテンプレートベースのソリューションに頼らないことに注意してください。通常、明快さと単純さに焦点を当てる必要があります

上記は単純なガイドラインですが、ほとんどの場合、適切な設計上の決定に向けて指示されます。


あなたの投稿の残りの部分について:

[...]と書き直すと、2回の移動があり、コピーはありません。

これは正しくありません。CreditCardまず、右辺値参照は左辺値にバインドできないため、これは、型の右辺値をコンストラクターに渡す場合にのみコンパイルされます。例えば:

// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));

CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));

しかし、これを行おうとすると機能しません。

CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue

ccは左辺値であり、右辺値参照は左辺値にバインドできないためです。さらに、参照をオブジェクトにバインドする場合、移動は実行されません。これは単なる参照バインドです。したがって、1つの動きだけがあります。


したがって、この回答の最初の部分で提供されているガイドラインに基づいて、CreditCard値で取得するときに生成される移動の数に関心がある場合は、2つのコンストラクターオーバーロードを定義できます。1つはconstCreditCard const&)への左辺値参照を取得し、もう1つは取得します。右辺値参照(CreditCard&&)。

過負荷解決は、左辺値を渡すときに前者を選択し(この場合、1つのコピーが実行されます)、右辺値を渡すときに後者を選択します(この場合、1つの移動が実行されます)。

Account(std::string number, float amount, CreditCard const& creditCard) 
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }

Account(std::string number, float amount, CreditCard&& creditCard) 
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }

の使用法は通常、完全な転送std::forward<>を実現したいときに見られます。その場合、コンストラクターは実際にはコンストラクターテンプレートになり、多かれ少なかれ次のようになります。

template<typename C>
Account(std::string number, float amount, C&& creditCard) 
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }

ある意味で、これは前に示した両方のオーバーロードを1つの関数に結合します。左辺値を渡す場合に備えてC推定され、参照の折りたたみルールにより、この関数がインスタンス化されます。CreditCard&

Account(std::string number, float amount, CreditCard& creditCard) : 
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard)) 
{ }

これにより、必要に応じて、のコピー構築が行わcreditCardれます。一方、右辺値が渡さCれると、はと推定されCreditCard、代わりにこの関数がインスタンス化されます。

Account(std::string number, float amount, CreditCard&& creditCard) : 
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard)) 
{ }

これにより、のmove-constructionが発生しcreditCardます。これは、必要なものです(渡される値は右辺値であり、これは、そこから移動することが許可されていることを意味します)。

于 2013-03-24T15:55:02.887 に答える
11

まず、いくつかの詳細を修正させてください。あなたが次のように言うとき:

2つの動きがあり、コピーはありません。

それは誤りです。右辺値参照へのバインドは移動ではありません。動きは1つだけです。

さらに、CreditCardはテンプレートパラメータではないため、std::forward<CreditCard>(creditCard)は冗長な言い方ですstd::move(creditCard)

今...

あなたのタイプが「安い」動きをしているなら、あなたはただあなたの人生を楽にして、すべてを価値と「std::move一緒に」とることを望むかもしれません。

Account(std::string number, float amount, CreditCard creditCard)
: number(std::move(number),
  amount(amount),
  creditCard(std::move(creditCard)) {}

このアプローチでは、1つしか得られない場合に、2つの手が得られますが、手が安い場合は、許容できる場合があります。

std::string私たちがこの「安い動き」の問題に取り組んでいる間、それはいわゆる小さな文字列の最適化で実装されることが多いので、その動きはいくつかのポインターをコピーするほど安くはないかもしれないことを思い出してください。最適化の問題でいつものように、それが重要かどうかは、私ではなく、プロファイラーに尋ねるものです。

あなたがそれらの余分な動きを被りたくない場合はどうしますか?多分それらは高すぎるか、さらに悪いことに、タイプを実際に移動することができず、余分なコピーが発生する可能性があります。

問題のあるパラメーターが1つしかない場合は、とで2つのオーバーロードを指定できT const&ますT&&。これにより、コピーまたは移動が発生する実際のメンバーの初期化まで、常に参照がバインドされます。

ただし、複数のパラメーターがある場合、これにより、過負荷の数が急激に増加します。

これは、完全な転送で解決できる問題です。つまり、代わりにテンプレートを作成しstd::forward、引数の値カテゴリをメンバーとして最終的な宛先に運ぶために使用します。

template <typename TString, typename TCreditCard>
Account(TString&& number, float amount, TCreditCard&& creditCard)
: number(std::forward<TString>(number),
  amount(amount),
  creditCard(std::forward<TCreditCard>(creditCard)) {}
于 2013-03-24T16:04:16.693 に答える
6

まず第一に、std::stringと同じようにかなり重いクラスタイプですstd::vector。それは確かに原始的ではありません。

大きな移動可能な型を値でコンストラクターに取り込む場合は、std::moveそれらをメンバーに入れます。

CreditCard(std::string number, float amount, CreditCard creditCard)
  : number(std::move(number)), amount(amount), creditCard(std::move(creditCard))
{ }

これはまさに私がコンストラクターを実装することをお勧めする方法です。これにより、メンバーはコピーnumbercreditCard作成されるのではなく、移動で作成されます。このコンストラクターを使用すると、オブジェクトがコンストラクターに渡されるときに1つのコピー(または一時的な場合は移動)があり、メンバーの初期化時に1つの移動があります。

次に、このコンストラクターについて考えてみましょう。

Account(std::string number, float amount, CreditCard& creditCard)
  : number(number), amount(amount), creditCard(creditCard)

creditCardそうです、これには、最初に参照によってコンストラクターに渡されるため、のコピーが1つ含まれます。constただし、オブジェクトをコンストラクターに渡すことはできず(参照が非であるためconst)、一時オブジェクトを渡すこともできません。たとえば、これを行うことはできませんでした:

Account account("something", 10.0f, CreditCard("12345",2,2015,1001));

では、考えてみましょう。

Account(std::string number, float amount, CreditCard&& creditCard)
  : number(number), amount(amount), creditCard(std::forward<CreditCard>(creditCard))

ここでは、右辺値参照との誤解を示しましたstd::forwardstd::forward転送するオブジェクトが推定型T&&として宣言されている場合にのみ、実際に使用する必要があります。これは推測されていないので(私は推測しています)、誤って使用されています。ユニバーサルリファレンスを検索します。 TCreditCardstd::forward

于 2013-03-24T15:54:33.750 に答える
1

私にとって最も重要なこと、つまりパラメーターを渡すことは、ちょっと不明確です。

  • 関数/メソッド内で渡された変数を変更する場合
    • あなたはそれを参照によって渡します
    • ポインタとして渡します(*)
  • 関数/メソッド内で渡された値/変数を読み取りたい場合
    • const参照で渡します
  • 関数/メソッド内で渡された値を変更する場合
    • オブジェクトをコピーして通常どおり渡します(**)

(*)ポインタは動的に割り当てられたメモリを参照する場合があるため、参照が最終的にポインタとして実装されている場合でも、可能であれば、ポインタよりも参照を優先する必要があります。

(**)「通常」とは、コピーコンストラクター(同じタイプのパラメーターのオブジェクトを渡す場合)または通常のコンストラクター(クラスと互換性のあるタイプを渡す場合)を意味します。myMethod(std::string)たとえば、オブジェクトをとして渡す場合、オブジェクトにstd::string渡されるとコピーコンストラクタが使用されるため、オブジェクトが存在することを確認する必要があります。

于 2013-03-24T15:57:25.537 に答える
1

私は一般的なケースに非常に単純なルールを使用します:POD(int、bool、double、...)にはcopyを使用し、その他すべてにはconst&を使用します...

そして、コピーしたいかどうかは、メソッドのシグネチャではなく、パラメータを使って何をするかによって答えられます。

struct A {
  A(const std::string& aValue, const std::string& another) 
    : copiedValue(aValue), justARef(another) {}
  std::string copiedValue;
  const std::string& justARef; 
};

ポインタの精度:私はほとんどそれらを使用しませんでした。&に対する唯一の利点は、それらをnullにするか、再割り当てできることです。

于 2013-03-24T15:57:35.757 に答える