web-dev-qa-db-ja.com

「ユニバーサル参照」が右辺値参照と同じ構文を持つのはなぜですか?

これらの(かなりの)新機能について調査したところ、C++委員会が両方に同じ構文を導入することにしたのはなぜでしょうか。開発者はそれがどのように機能するかを理解するために不必要に時間を無駄にする必要があるようであり、1つの解決策はさらなる問題について考えることを可能にします。私の場合、これに単純化できる問題から始まりました。

#include <iostream>

template <typename T>
void f(T& a)
{
    std::cout << "f(T& a) for lvalues\n";
}

template <typename T>
void f(T&& a)
{
    std::cout << "f(T&& a) for rvalues\n";
}

int main()
{
    int a;
    f(a);
    f(int());

    return 0;
}

最初にVS2013でコンパイルしましたが、期待どおりに機能し、次の結果が得られました。

f(T& a) for lvalues
f(T&& a) for rvalues

しかし、疑わしいことが1つありました。それは、インテリセンスがf(a)に下線を引いていることです。私はいくつかの調査を行い、それは型の崩壊(Scott Meyersが名前を付けた普遍的な参照)によるものであると理解したので、g ++がそれについてどう考えているのか疑問に思いました。もちろん、コンパイルされませんでした。 Microsoftがコンパイラをより直感的な方法で動作するように実装したことは非常に素晴らしいことですが、それが標準に準拠しているかどうか、およびIDE(IDE $ ===(IDE $ ===(コンパイラとインテリセンスですが、実際には何らかの意味があるかもしれません)わかりました、問題に戻ります。私は次のように解決しました。

template <typename T>
void f(T& a)
{
    std::cout << "f(T& a) for lvalues\n";
}

template <typename T>
void f(const T&& a)
{
    std::cout << "f(T&& a) for rvalues\n";
}

これで、型の崩壊はなく、(r/l)値の通常のオーバーロードだけでした。それはg ++でコンパイルされ、インテリセンスは文句を言うのをやめ、私はほとんど満足しました。ほとんどの場合、右辺値参照によって渡されるオブジェクトの状態で何かを変更したい場合はどうなるかを考えたので?必要に応じて状況を説明することもできますが、この説明は長すぎてここでは説明できません。私はそれをこのように解決しました:

template <typename T>
void f(T&& a, std::true_type)
{
    std::cout << "f(T&& a) for rvalues\n";
}

template <typename T>
void f(T&& a, std::false_type)
{
    std::cout << "f(T&& a) for lvalues\n";
}

template <typename T>
void f(T&& a)
{
    f(std::forward<T>(a), std::is_rvalue_reference<T&&>());
}

これで、テストされたすべてのコンパイラでコンパイルされ、右辺値リファレンス実装でオブジェクトの状態を変更できますが、見栄えがよくありません。これは、ユニバーサル参照と右辺値参照の構文が同じであるためです。だから私の質問は:なぜC++委員会は普遍的な参照のためにいくつかの別の構文を導入しなかったのですか?この機能は、たとえば、T?、auto ?、または同様のものによって通知されるべきだと思いますが、右辺値参照と衝突するだけのT &&およびauto &&としては通知されません。このアプローチを使用すると、MSコンパイラだけでなく、私の最初の実装は完全に正しいでしょう。委員会の決定を誰かが説明できますか?

33
Piotrek

私はそれが逆に起こったと思います。最初のアイデアは、言語に右辺値参照を導入することでした。つまり、「ダブルアンパサンド参照を提供するコードは、参照されるオブジェクトに何が起こるかを気にしません」。これにより、移動セマンティクスが可能になります。これはいいね。

今。標準では、参照への参照を作成することは禁止されていますが、これは常に可能でした。考えてみましょう:

template<typename T>
void my_func(T, T&) { /* ... */ }

// ...

my_func<int&>(a, b);

この場合、2番目のパラメーターのタイプはint & &である必要がありますが、これは標準で明示的に禁止されています。したがって、C++ 98でも、参照を折りたたむ必要があります。 C++ 98では、参照の種類が1つしかないため、折りたたみルールは単純でした。

& & -> &

現在、2種類の参照があります。&&は「オブジェクトに何が起こるかは気にしない」を意味し、&は「オブジェクトに何が起こるかを気にする可能性があるので、自分が何であるかをよく監視する」という意味です。やってる」。これを念頭に置いて、折りたたみルールは自然に流れます。C++は、オブジェクトに何が起こるかを誰も気にしない場合にのみ、参照を&&に折りたたみます。

& & -> &
& && -> &
&& & -> &
&& && -> &&

これらのルールが整っているので、このルールのサブセットに気付いたのはスコットマイヤーズだと思います。

& && -> &
&& && -> &&

&&が参照の折りたたみに関して右中立であることを示し、型の推定が発生すると、T&&構造を使用して任意の型の参照に一致させることができ、これらの参照に対して「ユニバーサル参照」という用語を作り出しました。それは委員会によって発明されたものではありません。これは他の規則の副作用であり、委員会の設計ではありません。

したがって、この用語は、型推定が発生しない場合のREAL右辺値参照(&&であることが保証される)と、型推定されたUNIVERSAL参照(テンプレートの特殊化時に&&のままであることが保証されない)を区別するために導入されました。

37

他の人は、参照折りたたみルールがユニバーサル参照が機能するための鍵であるとすでに述べていますが、別の(おそらく)同様に重要な側面があります:テンプレートパラメータが_T&&_の形式である場合のテンプレート引数の推論。

実際、質問に関連して:

「ユニバーサル参照」が右辺値参照と同じ構文を持つのはなぜですか?

私の意見では、これはすべて構文に関するものであるため、テンプレートパラメータの形式の方が重要です。 C++ 03では、渡されたオブジェクトの値カテゴリ(右辺値または左辺値)をテンプレート関数が知る方法はありませんでした。 C++ 11は、それを説明するためにテンプレート引数の控除を変更しました:14.8.2.1 [temp.deduct.call]/p3

[...] Pがcv非修飾テンプレートパラメータへの右辺値参照であり、引数が左辺値である場合、タイプの推定にはAの代わりにタイプ「Aへの左辺値参照」が使用されます。

これは、最初に提案された表現よりも少し複雑です( n177 によって与えられます):

Pがフォームの右辺値参照型の場合 履歴書 _T&&_ここで、Tはテンプレートの型パラメーターであり、引数は左辺値であり、Tの推定テンプレート引数値は_A&_です。 【例:

_template<typename T> int f(T&&);
int i;
int j = f(i);   // calls f<int&>(i)
_

---終了例]

より詳細には、上記の呼び出しはf<int&>(int& &&)のインスタンス化をトリガーし、参照の折りたたみが適用された後、f<int&>(int&)になります。一方、f(0)f<int>(int&&)をインスタンス化します。 (_&_内に_< ... >_がないことに注意してください。)

他の形式の宣言では、Tを_int&_に推論し、f<int&>( ... )のインスタンス化をトリガーしません。 (_&_は_( ... )_の間に表示できますが、_< ... >_の間に表示できないことに注意してください。)

要約すると、型の推定が実行される場合、構文形式_T&&_は、元のオブジェクトの値カテゴリを関数テンプレート本体内で使用できるようにするものです。

この事実に関連して、元のオブジェクトの値カテゴリを記録するのはTargではない)であるため、std::forward<T>(arg)ではなくstd::forward(arg)を使用する必要があることに注意してください。 (注意の手段として、_std::forward_の定義は「人為的に」、後者のコンパイルを強制して、プログラマーがこの間違いを犯さないようにします。)

元の質問に戻る:「委員会が新しい構文を選択するのではなく、フォーム_T&&_を使用することにしたのはなぜですか?」

本当の理由はわかりませんが、推測はできます。まず、これはC++ 03との下位互換性があります。第二に、そして最も重要なことに、これは標準で述べ(1段落の変更)、コンパイラーによって実装するための非常に単純な解決策でした。誤解しないでください。私は委員会のメンバーが怠惰だと言っているのではありません(彼らは確かに怠惰ではありません)。巻き添え被害のリスクを最小限に抑えたと言っているだけです。

7
Cassio Neri

あなたはあなた自身の質問に答えました:「ユニバーサルリファレンス」は、リファレンスが崩壊する右辺値リファレンスケースの単なる名前です。参照の折りたたみに別の構文が必要な場合、それは参照の折りたたみではなくなります。参照の折りたたみとは、参照修飾子を参照型に適用することです。

だから私はg ++がそれについてどう思っているのか疑問に思いました。もちろん、コンパイルされませんでした。

最初の例は整形式です。 GCC 4.9は文句なしにコンパイルし、出力はMSVCと一致します。

ほとんどの場合、右辺値参照によって渡されているオブジェクトの状態で何かを変更したい場合はどうなるかを考えたので?

右辺値参照はconstセマンティクスを適用しません。 moveによって渡されたオブジェクトの状態はいつでも変更できます。それらの目的には可変性が必要です。 const &&のようなものがありますが、決して必要になることはありません。

6
Potatoswatter

ユニバーサル参照は、c ++ 11の参照折りたたみルールのために機能します。あなたが持っている場合

template <typename T> func (T & t)

参照の折りたたみは引き続き発生しますが、一時的には機能しないため、参照は「ユニバーサル」ではありません。ユニバーサル参照は、lvalsとrvalsを受け入れることができるため(他の修飾子も保持するため)、「ユニバーサル」と呼ばれます。 T & tはrvalを受け入れることができないため、普遍的ではありません。

つまり、ユニバーサルリファレンスはリファレンスの崩壊の産物であり、ユニバーサルリファレンスはユニバーサルであるため、そのように名付けられています。

0
aaronman

何よりもまず、最初の例がgcc 4.8でコンパイルされなかった理由は、これがgcc4.8のバグであるためです。 (これについては後で詳しく説明します)。最初の例は、4.8以降のバージョンのgcc、clang 3.3以降、およびAppleのLLVMベースのc ++コンパイラを使用して、VS2013と同じ出力をコンパイル、実行、および生成します。

ユニバーサルリファレンスについて:
Scott Meyerが「ユニバーサル参照」という用語を作り出した理由の1つは、関数テンプレート引数としての_T&&_が左辺値と右辺値に一致するためです。テンプレート内の_T&&_の普遍性は、質問の最初の例から最初の関数を削除することで確認できます。

_// Example 1, sans f(T&):
#include <iostream>
#include <type_traits>

template <typename T>
void f(T&&)   {
    std::cout << "f(T&&) for universal references\n";
    std::cout << "T&& is an rvalue reference: "
              << std::boolalpha
              << std::is_rvalue_reference<T&&>::value
              << '\n';
}

int main() {
    int a;
    const int b = 42;
    f(a);
    f(b);
    f(0);
}
_

上記は、前述のすべてのコンパイラー、およびgcc4.8でもコンパイルおよび実行されます。この1つの関数は、左辺値と右辺値を引数として普遍的に受け入れます。 f(a)およびf(b)の呼び出しの場合、関数は_T&&_が右辺値参照ではないことを報告します。 f(a)f(b)、およびf(0)の呼び出しは、それぞれ関数f<int&>(int&)f<const int&>(const int&)f<int&&>(int&&)f(0)の場合にのみ、_T&&_は右辺値参照になります。関数テンプレートの場合、引数_T&& foo_は右辺値参照である場合とそうでない場合があるため、これらを別の名前で呼び出すことをお勧めします。マイヤーズはそれらを「普遍的な参照」と呼ぶことを選びました。

これがgcc 4.8のバグである理由:
質問の最初のサンプルコードでは、関数テンプレートtemplate <typename T> void f(T&)template <typename T> void f(T&&)f<int>(int&)f<int&>(int&)になります。 f(a)の呼び出しに対して、後者はC++ 11参照折りたたみルールのおかげです。これらの2つの関数のシグネチャはまったく同じであるため、おそらくgcc 4.8が正しく、f(a)の呼び出しがあいまいです。そうではない。

呼び出しがあいまいでない理由は、セクション13.3(過負荷解決)、14.5.6.2(関数テンプレートの半順序)の規則に従って、template <typename T> void f(T&)template <typename T> void f(T&&)よりも特殊化されているためです。および14.8.2.4(半順序化中のテンプレート引数の推定)。 template <typename T> void f(T&&)template <typename T> void f(T&)を_T=int&_と比較すると、両方の積がf(int&)になります。ここでは区別できません。ただし、template<typename T> void f(T&)template <typename T> void f(T&&)を_T=int_と比較すると、前者はf(int&)f(int&&)があるため、より特殊化されています。 。 14.8.2.4段落9によると、「引数テンプレートの型が左辺値参照であり、パラメーターテンプレートの型がそうでない場合、引数型は他の型よりも特殊化されていると見なされます」。

0
David Hammen