最初の最も重要な質問:
C ++でパラメーターを送信する方法のベストプラクティスはありますか?それは、些細なことではなく、実際に見つけたからです。
関数が渡される元のオブジェクトを変更する必要がある場合は、呼び出しが戻った後、そのオブジェクトへの変更が呼び出し元に表示されるようにするため、左辺値参照を渡す必要があります。
void foo(my_class& obj)
{
// Modify obj here...
}
関数が元のオブジェクトを変更する必要がなく、そのコピーを作成する必要がない場合(つまり、その状態を監視するだけでよい場合)、左辺値参照をconst
次のように渡す必要があります。
void foo(my_class const& obj)
{
// Observe obj here
}
これにより、左辺値(左辺値は安定したIDを持つオブジェクト)と右辺値(右辺値は、たとえば一時的なもの、または呼び出しの結果として移動しようとしているオブジェクト)の両方で関数を呼び出すことができますstd::move()
。
また、基本的なタイプや、、、、などのコピーが高速なタイプの場合、int
関数が単に値を監視する必要がある場合は参照で渡す必要はなく、値で渡すことを優先する必要があると主張することもできます。参照セマンティクスが必要ない場合は正しいですが、関数が同じ入力オブジェクトへのポインターをどこかに格納したい場合はどうなりますか?そのポインターを後で読み取ると、他の部分で実行された値の変更が表示されます。コード?この場合、参照渡しが正しい解決策です。bool
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...
}
スコットマイヤーズによるこの講演を見て、このデザインについてもっと知りたいと思うかもしれません(彼が使用している「ユニバーサルリファレンス」という用語は非標準であるという事実に注意してください)。
覚えておくべきことの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つはconst
(CreditCard 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
ます。これは、必要なものです(渡される値は右辺値であり、これは、そこから移動することが許可されていることを意味します)。