パラメータを正しく渡す方法は?


108

私は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 ++でパラメーターを送信する方法のベストプラクティスはありますか?上記の例をどのように処理しますか?


9
メタ:誰かが良いC ++の質問をしただけだなんて信じられません。+1。

23
FYI std::stringは、のようなクラスCreditCardであり、プリミティブ型ではありません。
chris

7
文字列の混乱のため、無関係ではありますが、文字列リテラル"abc"はタイプstd::stringではなくタイプではなく(3文字とnullのためにこの場合はN = 4である)ことを知っておく必要char */const char *がありconst char[N]ます。これは、邪魔にならないためのよくある一般的な誤解です。
chris

10
@chuex:Jon SkeetはC#の問題をすべて網羅しています。
Steve Jessop 2013年

回答:


158

最も重要な質問は最初に:

C ++でパラメーターを送信する方法のベストプラクティスはありますか?

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

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

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

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

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

一つは、また、と主張している可能性があり、コピーが速くなるための基本的な種類やタイプについてなど、intbool、またはchar、関数は単に値を観察する必要があり、場合参照渡しする必要はありません値で渡すと、好まれるべきですが参照セマンティクスが必要ない場合は正しいですが、関数がそのまったく同じ入力オブジェクトへのポインターをどこかに格納し、将来そのポインターを読み取ると、他の一部で実行された値の変更が表示される場合はどうでしょうか。コード?この場合、参照渡しは正しい解決策です。

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

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...
}

このデザインについては、スコットマイヤーズの講演をご覧になることをお勧めします(彼が使用している「Universal References」という用語が非標準であることを覚えておいてください)。

覚えておくべきことの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つだけです。


したがって、この回答の最初の部分で提供されたガイドラインに基づいて、CreditCardby値を取るときに生成される移動の数に関心がある場合は、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)) 
{ }

これが原因となり、移動建設のをcreditCard(渡される値が右辺値であり、その手段は、我々はそこから移動することが許可されているので)何をしたいです。


非プリミティブパラメータータイプの場合、常にテンプレートバージョンをforwardで使用してもよいと言うのは間違っていますか?
Jack Willson 2013年

1
@JackWillson:ムーブのパフォーマンスに本当に懸念がある場合にのみ、テンプレートバージョンに頼るべきだと思います。詳細については、良い答えを持っている私の私の質問を参照してください。一般に、コピーを作成する必要がなく、観察するだけでよい場合は、を参照してくださいconst。元のオブジェクトを変更する必要がある場合は、 `const以外のものを参照してください。コピーを作成する必要があり、移動が安い場合は、値を取得してから移動します。
Andy Prowl 2013年

2
3番目のコードサンプルに移動する必要はないと思います。obj移動するローカルコピーを作成する代わりに、単に使用できますか?
juanchopanza 2013年

3
@AndyProwl:あなたは私の+1時間前に取得しましたが、あなたの(優れた)回答をもう一度読んで、2つのことを指摘したいと思います。a)値によると、常にコピー作成されるわけではありません(移動されない場合)。オブジェクトは呼び出し元のサイトでインプレースで構築できます。特にRVO / NRVOを使用すると、これは最初に考えるよりも頻繁に機能します。b)最後の例でstd::forwardは、一度しか呼び出されない場合があることを指摘してください。私は人々がそれをループなどに入れているのを見ました、そしてこの答えは多くの初心者に見られるので、私は彼らがこの罠を避けるのを助けるために太った「警告!」ラベルがあるべきです。
Daniel Frey

1
@SteveJessop:それとは別に、転送関数(特に1つの引数を取るコンストラクター)には技術的な問題(必ずしも問題ではない)があり、ほとんどの場合、任意の型の引数を受け入れ、std::is_constructible<>適切にSFINAEでない限り型の特性を無効にする可能性があります制約付き-一部の人にとってそれは些細なことではないかもしれません。
Andy Prowl 2013年

11

まず、いくつかの詳細を訂正させてください。次のように言うと:

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

それは誤りです。右辺値参照へのバインドは移動ではありません。たった一つの動きがあります。

さらに、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いわゆる小さな文字列の最適化で実装されることが多いので、その動きはいくつかのポインタをコピーするほど安くないかもしれません。最適化の問題ではいつものように、重要かどうかは、私ではなくプロファイラーに尋ねる必要があります。

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

一つだけ問題のパラメータがある場合は、あなたが、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)) {}

テンプレートのバージョンに問題があります:ユーザーはAccount("",0,{brace, initialisation})もう書き込むことができません。
ipc 2013年

@ipcああ、本当。それは本当に迷惑であり、簡単に拡張可能な回避策があるとは思いません。
R.マルティーニョフェルナンデス

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::forward。実際に使用std::forwardしているのは、転送するオブジェクトがT&&いくつかの推定型 として宣言されている場合のみですT。ここCreditCardでは推定されていません(私は推測std::forwardしています)。したがって、これは誤って使用されています。ユニバーサルリファレンスを検索します


1

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

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

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

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


2
「私は一般的なケースでは非常に単純なルールを使用します。POD(int、bool、double、...)のコピーを使用し、その他のすべてにはconst&を使用します。」第ジャスト号

&(constなし)を使用して値を変更する場合は、追加する場合があります。そうでなければ、私にはわかりません...シンプルさで十分です。しかし、あなたがそう言ったなら...
David Fleury

参照プリミティブ型で変更したい場合もあれば、オブジェクトのコピーを作成する必要がある場合や、オブジェクトを移動する必要がある場合もあります。すべてをPODに>値で> UDT>参照で削減することはできません。

OK、これは私が後に追加したものです。答えが速すぎるかもしれません。
David Fleury 2013年

1

最も重要なことは、パラメーターを渡すことです。

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

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

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

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