web-dev-qa-db-ja.com

フォワードを使用する利点

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

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

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

基本的に、式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);
}

これは上記の問題を修正しますが、フリップフロップを反転します。 Eに非const引数を持たせることができなくなりました:

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

3番目の試みはconst参照を受け入れますが、その後const_castconstを離れます:

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!

最終的なソリューションは、すべてを正しく処理しますが、維持するのは不可能です。 fのオーバーロードを提供し、constとnon-constのallの組み合わせで:

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)

次に、テンプレート引数の演with:引数が左辺値Aである場合、テンプレート引数にAへの左辺値参照を指定します。それ以外の場合、通常の演duceです。これにより、いわゆるユニバーサル参照(用語 forwarding reference は現在は公式のものです)。

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

コード内:

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関数の中にいて、左辺値が渡されていると考えてください。これは、TA&であることを意味するため、静的キャストのターゲットタイプはA& &&、または単にA&です。 xはすでにA&であるため、何もせず、左辺値参照が残っています。

右辺値が渡されたとき、TAであるため、静的キャストのターゲットタイプは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);
722
GManNickG

Std :: forwardを実装する概念的なコードが議論に追加できると思います。これはスコット・マイヤーズの講演のスライドです An Effective C++ 11/14 Sampler

conceptual code implementing std::forward

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

ユーザーの観点から見ると、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。

49
user7610

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

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に転送する必要があります。ただし、式inner(t1,t2);t1t2は左辺値であるため、#2ではなく#1を呼び出すことになります。そのため、std::forwardを使用して参照を名前のない参照に戻す必要があります。したがって、outert1は常に左辺値式であり、forward<T1>(t1)T1に応じて右辺値式になる場合があります。 T1が左辺値参照である場合、後者は左辺値式のみです。また、T1は、outerの最初の引数が左辺値式である場合にのみ左辺値参照であると推定されます。

24
sellibitze

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

インスタンス化後、T1char型であり、T2がクラスの場合、コピーごとにt1を、const参照ごとにt2を渡します。まあ、inner()const以外の参照ごとにそれらを受け取らない限り、つまり、その場合はあなたもそうしたいです。

右辺値参照なしでこれを実装するouter()関数のセットを作成して、inner()の型から引数を渡す正しい方法を推測してください。それらのうちの2 ^ 2、引数を推測するためのかなり大きなテンプレートメタのもの、そしてすべての場合にこれを正しくするための多くの時間が必要になると思います。

そして、誰かがポインターごとに引数を取るinner()を見つけます。今では3 ^ 2になると思います。 (または4 ^ 2。地獄、constポインターが違いを生むかどうかを考えることはできません。)

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

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

11
sbi

明確にされていない点は、static_cast<T&&>const T&を適切に処理することです。
プログラム:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

生産物:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

'f'はテンプレート関数でなければならないことに注意してください。 「void f(int && a)」として定義されている場合、これは機能しません。

3
lalawawa

転送/ユニバーサル参照を伴う外部メソッドと一緒に転送を使用する必要があることを強調する価値があるかもしれません。次の文としてforwardを単独で使用することは許可されていますが、混乱を招く以外の効果はありません。標準委員会は、このような柔軟性を無効にしたい場合があります。そうしないと、代わりにstatic_castを使用しないのはなぜですか。

     std::forward<int>(1);
     std::forward<std::string>("Hello");

私の意見では、moveとforwardは、r値参照型が導入された後の自然な結果である設計パターンです。不適切な使用が禁止されていない限り、メソッドが正しく使用されていると仮定して、メソッドに名前を付けるべきではありません。

1
colin