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


437

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

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

回答:


788

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

基本的に、式が与えられた場合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_cast「S 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つの解決策は、既存の型のテンプレート控除規則を変更しますが、これは潜在的に大量のコードを壊します。したがって、別の方法を見つける必要があります。

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

参照への参照が与えられた場合(参照はT&との両方を意味する包括的用語であることに注意T&&)、次のルールを使用して、結果のタイプを把握します。

「[指定された]タイプTへの参照であるタイプTR。タイプ「lvalue参照to cv TR」を作成しようとすると、タイプ「lvalue参照to T」が作成され、タイプ「右辺値参照to 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);

1
ではないだろうf機能、およびない表現になりますか?
マイケル・フカラキス2010

1
あなたの最後の試みは問題ステートメントに関して正しくありません:const値を非constとして転送するため、まったく転送しません。また、最初の試行では、const int iが受け入れられることに注意してください。Aはに推定されconst intます。失敗は右辺値リテラルに関するものです。また、への呼び出しではdeduced(1)、xはint&&であることに注意してくださいint(完全転送では、x値渡しパラメーターの場合のようにコピーが作成されることはありません)。ただTですintxフォワーダーで左辺値に評価される理由は、名前付き右辺値参照が左辺値式になるためです。
ヨハネスシャウブ-litb 2010

5
forwardまたはmoveここでの使用に違いはありますか?それとも単なる意味の違いですか?
0x499602D2 2013

28
@David:std::move明示的なテンプレート引数なしで呼び出す必要があり、常に右辺値になりますstd::forwardが、どちらかになる場合があります。使用std::moveあなたはもはやあなたを知っていない値を必要とし、別の場所に移動したい、使用がstd::forward行うことあなたの関数テンプレートに渡された値に応じました。
GManNickG 2013

5
最初に具体的な例から始めて、問題の動機を与えてくれてありがとう。非常に役立ちます!
ShreevatsaR 2015年

60

std :: forwardを実装する概念的なコードが議論に追加できると思います。これは、スコットマイヤーズの講演からのスライドです。効果的なC ++ 11/14サンプラー

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

moveコード内の関数はstd::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頃。


2
2番目のリンクは、完全に異なる場所を指してしまいました。
Pharap、2018年

32

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


これは一種の骨抜きの説明ですが、非常によく行われた機能的な説明です。人々は最初にこの回答を読んでから、必要に応じて深く
理解

11

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

インスタンス化後、T1の型charでありT2、クラスの場合、t1コピーt2ごとおよびconst参照ごとに渡したい場合。まあ、inner()それは非const参照ごとにそれらをとらない限り、つまり、その場合、あなたもそうしたいのです。

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

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

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

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


4

明確にされていない点は、適切に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)'として定義されているだけの場合、これは機能しません。


良い点なので、静的キャストのT &&も参照の折りたたみルールに従っていますよね?
barney

2

forwardは、forwarding / universal参照を使用する外部メソッドと組み合わせて使用​​する必要があることを強調する価値があるかもしれません。次のステートメントとしてforwardを単独で使用することは許可されていますが、混乱を招くこと以外は何の役にも立ちません。標準委員会はそのような柔軟性を無効にしたいと思うかもしれません。そうでなければ、なぜ代わりにstatic_castを使用しないのですか?

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

私の意見では、移動と前進は、r値参照タイプが導入された後の自然な結果である設計パターンです。不正な使用が禁止されていない限り、メソッドが正しく使用されていることを前提としてメソッドを指定しないでください。


C ++委員会は、言語イディオムを「正しく」使用する責任があると感じているとは思わず、「正しい」使用法を定義することさえしません(ただし、ガイドラインを提供することはできます)。そのために、人の教師、上司、および友人はそれらを何らかの方法で操縦する義務があるかもしれませんが、C ++委員会(したがって標準)にはその義務がないと思います。
SirGuy 2017

ええ、私はN2951を読んだだけで、標準委員会が機能の使用に関して不必要な制限を追加する義務を負わないことに同意します。しかし、これらの2つの関数テンプレート(移動と転送)の名前は、ライブラリファイルまたは標準ドキュメント(23.2.5転送/移動ヘルパー)での定義だけを見ると、少し混乱します。標準の例は、概念の理解に間違いなく役立ちますが、もう少し明確にするために注釈をさらに追加すると役立つ場合があります。
コリン2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.