次のようなコードを読む場合
auto&& var = foo();
ここで、foo
はT
型の値によって返される任意の関数です。 var
は、T
への右辺値参照型の左辺値です。しかし、これはvar
に対して何を意味するのでしょうか? var
のリソースを盗むことを許可されているということですか? auto&&
を使用して、コードの読者にunique_ptr<>
を返して排他的所有権があることを伝えるときのように伝える必要がある合理的な状況はありますか? T
がクラス型の場合、たとえばT&&
はどうでしょうか?
テンプレートプログラミング以外のauto&&
のユースケースがあるかどうかを理解したいだけです。この記事の例で説明したもののように niversal References Scott Meyers。
auto&& var = <initializer>
を使用すると、次のようになります:左辺値式でも右辺値式でも初期化子を受け入れ、そのconstnessを保持します。これは通常、forwardingに使用されます(通常はT&&
で)。これが機能する理由は、「ユニバーサル参照」、auto&&
またはT&&
がanythingにバインドするためです。
あなたは言うかもしれません、それはなぜalso何かにバインドするので、単にconst auto&
を使用しないのですか? const
参照の使用に関する問題は、それがconst
であるということです!後で非const参照にバインドしたり、const
とマークされていないメンバー関数を呼び出したりすることはできません。
例として、std::vector
を取得し、イテレータを最初の要素に移動し、そのイテレータが指す値を何らかの方法で変更することを想像してください。
auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;
このコードは、イニシャライザ式に関係なく正常にコンパイルされます。 auto&&
の代替は、次の方法で失敗します。
auto => will copy the vector, but we wanted a reference
auto& => will only bind to modifiable lvalues
const auto& => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues
そのため、auto&&
は完全に機能します!このようなauto&&
の使用例は、範囲ベースのfor
ループです。詳細については my other question を参照してください。
std::forward
参照でauto&&
を使用して、それが元々左辺値または右辺値であったという事実を保持する場合、コードは次のように言います:オブジェクトを左辺値式または右辺値式から取得した場合、元々持っていた値を保持して、最も効率的に使用できるようにします-これにより無効になる可能性があります
auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));
これにより、元の初期化子が変更可能な右辺値であった場合、use_it_elsewhere
がパフォーマンスのために(コピーを回避して)その根性を引き裂くことができます。
これは、var
からリソースを盗むことができるのか、またはいつ盗むことができるのか、という意味です。 auto&&
は何にでもバインドするので、var
sを破壊しようとすることはできません。これは左辺値またはconstである可能性があります。ただし、内部を完全に破壊する可能性のある他の関数にstd::forward
することはできます。これを行うとすぐに、var
が無効な状態にあると見なす必要があります。
質問で与えられているように、fooが値でT
を返すauto&& var = foo();
の場合にこれを適用しましょう。この場合、var
のタイプはT&&
として推定されることが確実にわかります。それが右辺値であることは確かにわかっているので、リソースを盗むためにstd::forward
の許可は必要ありません。この特定のケースでは、foo
が値で返ることを知っているので、読者は次のように読むだけです:foo
から返されたテンポラリへの右辺値参照。これにより、喜んでそこから移動できます。
補遺として、「コードが変わる可能性がある」状況以外で、some_expression_that_may_be_rvalue_or_lvalue
のような式がいつ現れるかについて言及する価値があると思います。だから、ここに不自然な例があります:
std::vector<int> global_vec{1, 2, 3, 4};
template <typename T>
T get_vector()
{
return global_vec;
}
template <typename T>
void foo()
{
auto&& vec = get_vector<T>();
auto i = std::begin(vec);
(*i)++;
std::cout << vec[0] << std::endl;
}
ここで、get_vector<T>()
は、ジェネリック型T
に応じて左辺値または右辺値のいずれかになる可能性のある素敵な式です。基本的に、foo
のテンプレートパラメータを使用して、get_vector
の戻り値の型を変更します。
foo<std::vector<int>>
を呼び出すと、get_vector
は値でglobal_vec
を返し、右辺値式を提供します。または、foo<std::vector<int>&>
を呼び出すと、get_vector
は参照によりglobal_vec
を返し、結果として左辺値式になります。
行う場合:
foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;
予想どおり、次の出力が得られます。
2
1
2
2
コードのauto&&
をauto
、auto&
、const auto&
、またはconst auto&&
のいずれかに変更した場合、結果は得られません。欲しいです。
auto&&
参照が左辺値式と右辺値式のどちらで初期化されているかに基づいてプログラムロジックを変更する別の方法は、型特性を使用することです。
if (std::is_lvalue_reference<decltype(var)>::value) {
// var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
// var was initialised with an rvalue expression
}
まず、ユニバーサルリファレンスのテンプレート引数の演ductionがどのように機能するかを段階的に説明するための副読として 私の私の答え を読むことをお勧めします。
var
のリソースを盗むことを許可されているということですか?
必ずしも。 foo()
が突然参照を返した場合、または呼び出しを変更したが、var
の使用を更新するのを忘れた場合はどうなりますか?または、汎用コードを使用しており、foo()
の戻り値の型がパラメーターに応じて変わる場合がありますか?
auto&&
は、template<class T> void f(T&& v);
のT&&
とまったく同じであると考えてください。†)まさにそれ。関数でユニバーサル参照を渡したり、何らかの方法で使用したりする必要がある場合、汎用参照を使用して何をしますか? std::forward<T>(v)
を使用して、元の値カテゴリを取得します。関数に渡される前に左辺値であった場合、std::forward
を介して渡された後も左辺値のままです。右辺値であった場合、再び右辺値になります(名前付き右辺値参照は左辺値であることを思い出してください)。
それでは、どのようにvar
を一般的な方法で正しく使用しますか? std::forward<decltype(var)>(var)
を使用します。これは、上記の関数テンプレートのstd::forward<T>(v)
とまったく同じように機能します。 var
がT&&
の場合、右辺値が返され、T&
の場合、左辺値が返されます。
それでは、トピックに戻ります:コードベースのauto&& v = f();
とstd::forward<decltype(v)>(v)
は何を教えてくれますか? 彼らはv
が最も効率的な方法で取得されて渡されることを教えてくれます。ただし、そのような変数を転送した後は、移動元である可能性があるため、リセットせずにさらに使用するのは間違っているでしょう。
個人的には、modifyable変数が必要な場合、auto&&
を汎用コードで使用します。移動操作はその潜在能力を盗む可能性があるため、右辺値の完全転送は変更されます。単に怠け者になりたい(つまり、それを知っていても型名をつづらない)、変更する必要がない場合(たとえば、範囲の要素を印刷するだけの場合)、auto const&
に固執します。
†auto
は非常に異なるため、auto v = {1,2,3};
はv
をstd::initializer_list
にしますが、f({1,2,3})
は控除に失敗します。
移動コンストラクターを持つT
型を検討し、
T t( foo() );
その移動コンストラクターを使用します。
次に、中間参照を使用して、foo
からの戻り値をキャプチャします。
auto const &ref = foo();
これにより、移動コンストラクターの使用が除外されるため、戻り値を移動する代わりにコピーする必要があります(ここでstd::move
を使用しても、実際にはconst refを移動することはできません)
T t(std::move(ref)); // invokes T::T(T const&)
ただし、使用する場合
auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)
移動コンストラクターは引き続き使用可能です。
他の質問に対処するには:
... auto &&を使用してコードのリーダーに何かを伝える必要がある合理的な状況はありますか...
Xeoが言うように、最初のことは本質的にXを可能な限り効率的に渡しているであり、Xのタイプは何でもです。したがって、auto&&
を内部で使用するコードを見ると、適切な場合は内部的に移動セマンティクスを使用することを伝える必要があります。
... unique_ptr <>を返して排他的所有権があることを伝えるときのように...
関数テンプレートがT&&
型の引数を取る場合、渡すオブジェクトを移動する可能性があるということです。unique_ptr
を返すと、呼び出し側に所有権が明示的に付与されます。 T&&
を受け入れると、remove呼び出し側からの所有権(移動ctorが存在する場合など)。