523

完全転送では、名前付き右辺値参照を名前なし右辺std::forward値参照に変換するために使用されます。それを行う目的は何ですか?&を左辺値のままにしておくと、呼び出された関数にどのような影響がありますか?t1t2innert1t2

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
4

7 に答える 7

938

転送の問題を理解する必要があります。問題全体を詳細に読むことができますが、要約します。

基本的に、式 が与えられた場合、式が同等であることE(a, b, ... , c)を望みます。f(a, b, ... , c)C++03 では、これは不可能です。多くの試みがありますが、それらはすべて何らかの点で失敗します。


最も簡単なのは、左辺値参照を使用することです。

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

しかし、これは一時的な値を処理f(1, 2, 3);できません: 、それらは左辺値参照にバインドできないためです。

次の試行は次のようになります。

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

上記の問題は修正されますが、フリップフロップになります。Econst 以外の引数を持つことを許可しないようになりました。

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

3 番目の試行では、const-references を受け入れますが、次const_castのようになりconstます。

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

これはすべての値を受け入れ、すべての値を渡すことができますが、未定義の動作につながる可能性があります。

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

最終的な解決策は、すべてを正しく処理しますが、保守が不可能になるという犠牲を払います。const と non-const のすべてのf組み合わせで、のオーバーロードを提供します。

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N 個の引数には、悪夢のような 2 N 個の組み合わせが必要です。これを自動的に行いたいと思います。

(これは事実上、C++11 でコンパイラに実行させることです。)


C++11 では、これを修正する機会があります。1 つの解決策は、既存の型のテンプレート推論規則を変更しますが、これにより大量のコードが壊れる可能性があります。ですから、別の方法を見つけなければなりません。

解決策は、代わりに新しく追加されたrvalue-referencesを使用することです。右辺値参照型を推測するときに新しいルールを導入し、必要な結果を作成できます。結局のところ、今はコードを壊すことはできません。

参照への参照が与えられた場合 (参照は と の両方T&を意味する包括的な用語であることに注意してくださいT&&)、次の規則を使用して結果の型を計算します。

「[与えられた] 型 T への参照である型 TR を作成しようとすると、型「cv TR への左辺値参照」を作成しようとすると、型「T への左辺値参照」が作成されますが、型「への右辺値参照」を作成しようとすると、 cv TR" はタイプ TR を作成します。"

または表形式で:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

次に、テンプレート実引数推定を使用します。引数が左辺値 A の場合、テンプレート実引数に A への左辺値参照を指定します。それ以外の場合は、通常どおり推定します。これにより、いわゆるユニバーサル リファレンスが提供されます (フォワーディング リファレンスという用語は現在、公式のものです)。

なぜこれが役立つのですか?結合されているため、型の値カテゴリを追跡する機能が維持されます。左辺値の場合は左辺値参照パラメーターがあり、そうでない場合は右辺値参照パラメーターがあります。

コード内:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

最後に、変数の値カテゴリを「転送」します。関数内に入ると、パラメーターは左辺値として何にでも渡すことができることに注意してください。

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

それは良くないね。E は、取得したのと同じ種類の値カテゴリを取得する必要があります! 解決策は次のとおりです。

static_cast<T&&>(x);

これは何をしますか?関数内にいてdeduce、左辺値が渡されたとします。これはTが であることを意味するA&ため、静的キャストのターゲット型はA& &&、または単にA&です。xはすでに であるため、A&何もせず、左辺値参照が残ります。

右辺値Tisが渡された場合A、静的キャストのターゲット型は ですA&&。キャストの結果は右辺値式になり、左辺値参照に渡すことができなくなります。パラメータの値カテゴリを維持しました。

これらを組み合わせると、「完全な転送」が得られます。

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

左辺値をf受け取ると、左辺値をE取得します。右辺値をf受け取ると、右辺値をE取得します。完全。


そしてもちろん、私たちは醜いものを取り除きたいと思っています. static_cast<T&&>不可解で覚えておくのが奇妙です。forward代わりに、同じことを行う というユーティリティ関数を作成しましょう。

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
于 2010-08-27T07:59:47.757 に答える
89

std::forward を実装する概念的なコードが理解に役立つと思います。これは、Scott Meyers の講演「効果的な C++11/14 サンプラー」からのスライドです。

std::forward を実装する概念コード

コード内の関数movestd::move. そのトークの前半で、そのための (動作する) 実装があります。libstdc++ のファイル move.h で std::forward の実際の実装を見つけましたが、まったく有益ではありません。

ユーザーの観点からはstd::forward、右辺値への条件付きキャストを意味します。パラメータに左辺値または右辺値のいずれかを期待し、右辺値として渡された場合にのみ右辺値として別の関数に渡したい関数を書いている場合に役立ちます。パラメータを std::forward でラップしなかった場合、常に通常の参照として渡されます。

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

案の定、印刷されます

std::string& version
std::string&& version

コードは、前述の講演の例に基づいています。スライド 10、開始から 15:00 頃。

于 2014-05-01T15:44:45.977 に答える
44

完全転送では、 std::forward を使用して名前付き右辺値参照 t1 および t2 を名前なし右辺値参照に変換します。それを行う目的は何ですか?t1 と t2 を左辺値のままにしておくと、呼び出された関数 inner にどのような影響がありますか?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

式で名前付き右辺値参照を使用する場合、実際には左辺値です (オブジェクトを名前で参照するため)。次の例を検討してください。

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

さて、outerこのように呼び出すと

outer(17,29);

17 と 29 は整数リテラルであり、右辺値であるため、17 と 29 を #2 に転送したいと考えています。しかし、式のt1andは左辺値であるため、#2 ではなく #1 を呼び出すことになります。そのため、 を使用して参照を名前のない参照に戻す必要があります。したがって、inは常に左辺値式ですが、 によっては右辺値式になる場合があります。が左辺値参照の場合、後者は左辺値式のみです。andは、outer への最初の引数が左辺値式であった場合にのみ、左辺値参照であると推定されます。t2inner(t1,t2);std::forwardt1outerforward<T1>(t1)T1T1T1

于 2010-08-29T20:34:06.257 に答える
12

t1 と t2 を左辺値のままにしておくと、呼び出された関数 inner にどのような影響がありますか?

インスタンス化後、T1が typecharであり、が class である場合、コピーごとおよび参照ごとT2に渡す必要があります。まあ、非参照ごとにそれらを取得しない限り、つまり、その場合もそうする必要があります。t1t2constinner()const

outer()の型から引数を渡す正しい方法を推測して、右辺値参照なしでこれを実装する一連の関数を作成してみてくださいinner()。引数を推測するには、2 ^ 2 個のものが必要であり、引数を推測するにはかなりの量のテンプレート メタが必要であり、すべてのケースでこれを正しく行うには多くの時間が必要になると思います。

そして、誰かがinner()ポインタごとに引数を取る an を持ってきます。これで 3^2 になると思います。const(または 4^2 です。ポインタが違いを生む かどうかを考えるのは面倒です。)

そして、これを 5 つのパラメーターに対して実行したいとします。または7。

これで、一部の頭脳明晰な人が「完全な転送」を思いついた理由がわかりました。これは、コンパイラーがこれらすべてを実行するようにします。

于 2010-08-27T07:44:43.293 に答える