web-dev-qa-db-ja.com

auto &&は何を教えてくれますか?

次のようなコードを読む場合

auto&& var = foo();

ここで、fooT型の値によって返される任意の関数です。 varは、Tへの右辺値参照型の左辺値です。しかし、これはvarに対して何を意味するのでしょうか? varのリソースを盗むことを許可されているということですか? auto&&を使用して、コードの読者にunique_ptr<>を返して排他的所有権があることを伝えるときのように伝える必要がある合理的な状況はありますか? Tがクラス型の場合、たとえばT&&はどうでしょうか?

テンプレートプログラミング以外のauto&&のユースケースがあるかどうかを理解したいだけです。この記事の例で説明したもののように niversal References Scott Meyers。

146
MWid

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&&は何にでもバインドするので、varsを破壊しようとすることはできません。これは左辺値または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&&autoauto&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
}
197

まず、ユニバーサルリファレンスのテンプレート引数の演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)とまったく同じように機能します。 varT&&の場合、右辺値が返され、T&の場合、左辺値が返されます。

それでは、トピックに戻ります:コードベースのauto&& v = f();std::forward<decltype(v)>(v)は何を教えてくれますか? 彼らはvが最も効率的な方法で取得されて渡されることを教えてくれます。ただし、そのような変数を転送した後は、移動元である可能性があるため、リセットせずにさらに使用するのは間違っているでしょう。

個人的には、modifyable変数が必要な場合、auto&&を汎用コードで使用します。移動操作はその潜在能力を盗む可能性があるため、右辺値の完全転送は変更されます。単に怠け者になりたい(つまり、それを知っていても型名をつづらない)、変更する必要がない場合(たとえば、範囲の要素を印刷するだけの場合)、auto const&に固執します。


autoは非常に異なるため、auto v = {1,2,3};vstd::initializer_listにしますが、f({1,2,3})は控除に失敗します。

11
Xeo

移動コンストラクターを持つ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が存在する場合など)。

2
Useless