为什么“通用引用”与rvalue引用具有相同的语法?

时间:2013-12-04 00:06:57

标签: c++ c++11 reference universal rvalue

我刚刚对这些(非常)新功能进行了一些研究,我想知道为什么C ++委员会决定为它们引入相同的语法?似乎开发人员不必浪费一些时间来理解它是如何工作的,并且一个解决方案让我们考虑进一步的问题。在我的情况下,它从问题开始,可以简化为:

#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

但有一个可疑的事情:intellisense强调了f(a)。我做了一些研究,我明白这是因为类型崩溃(Scott Meyers将它命名为通用引用),所以我想知道g ++对它的看法。当然它没有编译。微软实现他们的编译器以更直观的方式工作是非常好的,但我不确定它是否符合标准,是否应该在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 ++上编译,intellisense停止抱怨,我几乎满意。差不多,因为我想过如果我想要改变对象状态中通过右值引用传递的东西怎么办?我可以在必要的时候描述一些情况,但是这个描述太长了,无法在这里提出。我用这种方式解决了它:

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&&>());
}

现在它编译所有经过测试的编译器,它允许我在rvalue参考实现中更改对象状态,但它看起来不太好,这是因为通用引用和右值引用的语法相同。所以我的问题是:为什么C ++委员会没有为通用引用引入另一种语法?我认为这个功能应该通过T?,auto?或类似的东西发出信号,但不能作为T&amp;&amp; amp;和汽车&amp;&amp;它只是与右值参考碰撞。使用这种方法,我的第一个实现是完全正确的,不仅适用于MS编译器。任何人都可以解释委员会的决定吗?

5 个答案:

答案 0 :(得分:37)

我认为它恰好相反。最初的想法是将rvalue-references引入语言,这意味着“提供双&符号引用的代码并不关心引用对象会发生什么”。这允许移动语义。这很好。

现在。标准禁止构建对引用的引用,但这始终是可行的。考虑:

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

// ...

my_func<int&>(a, b);

在这种情况下,第二个参数的类型应为int & &,但在标准中明确禁止。因此,即使在C ++ 98中,也必须折叠引用。在C ++ 98中,只有一种引用,因此折叠规则很简单:

& & -> &

现在,我们有两种引用,其中&&表示“我不关心对象可能发生的事情”,而&表示“我可能会关心可能发生的事情”对象,所以你最好看看你在做什么。考虑到这一点,崩溃规则自然而然地流出:只有当没有人关心对象发生什么时,C ++才会将引用归咎于&&

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

根据这些规则,我认为是Scott Meyers注意到这一部分规则:

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

显示&&在参考折叠方面是正确的,并且,当发生类型推导时,T&&结构可用于匹配任何类型的引用,并创造了术语“通用”参考“对于这些参考。这不是委员会发明的东西。这只是其他规则的副作用,而不是委员会的设计。

因此引入该术语是为了区分REAL rvalue-references,当没有发生类型推导时,保证为&&,以及那些不保证保留的类型推导的UNIVERSAL引用{ {1}}在模板专业化时间。

答案 1 :(得分:7)

其他人已经提到参考折叠规则是普遍参考工作的关键,但还有另一个(可论证)同样重要的方面:当模板参数的形式为T&&时,模板参数推断。

实际上,就问题而言:

  
    

为什么“通用引用”与rvalue引用具有相同的语法?

  

在我看来,模板参数的形式更重要,因为这完全是关于语法的。在C ++ 03中,模板函数无法知道传递对象的值类别(rvalue或lvalue)。 C ++ 11更改了模板参数推导以解释:14.8.2.1 [temp.deduct.call] / p3

  
    

[...]如果P是对cv-unqualified模板参数的右值引用,并且参数是左值,则使用“A的左值引用”代替{ {1}}用于类型扣除。

  

这比最初提出的措辞(由n1770给出)复杂一点:

  
    

如果A是形式 cv 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&&允许原始对象的值类别在函数模板体内可用。

与此相关,请注意必须使用std::forward<T>(arg)而不是std::forward(arg),因为它是T(不是arg),它记录了原始对象的值类别。 (作为一种谨慎的说法,std::forward“人为地”的定义迫使后者的编译无法阻止程序员犯这个错误。)

回到原来的问题:“为什么委员会决定使用T&&形式而不是选择新语法?”

我无法说出真正的原因,但我可以推测。首先,它向后兼容C ++ 03。其次,最重要的是,这是一个非常简单的解决方案,可以在标准中说明(一个段落更改)并由编译器实现。拜托,别误会我的意思。我不是说委员会成员是懒惰的(他们当然不是)。我只是说他们将附带损害的风险降到最低。

答案 2 :(得分:6)

您回答了自己的问题:“通用引用”只是引用折叠的右值引用案例的名称。如果参考折叠需要另一种语法,则不会再引用折叠。参考折叠只是将参考限定符应用于引用类型。

  

所以我想知道g ++对它的看法。当然它没有编译。

你的第一个例子是格式良好的。 GCC 4.9无需投诉即可编译,输出与MSVC一致。

  

差不多,因为我想过如果我想改变对象状态中通过右值引用传递的东西会怎么样?

Rvalue引用不适用const语义;您始终可以更改move传递的对象的状态。可变性对他们的目的是必要的。虽然有const &&这样的东西,但你永远不需要它。

答案 3 :(得分:0)

由于c ++ 11中的参考折叠规则,通用引用有效。如果你有

template <typename T> func (T & t)

参考折叠仍然会发生,但它不能用于临时,因此引用不是“通用”。通用引用称为“通用”,因为它可以接受lvals和rvals(也保留其他限定符)。 T & t不是普遍的,因为它无法接受rvals。

总而言之,通用引用是引用折叠的产物,并且通用引用被命名为因为它是通用的它可以是什么

答案 4 :(得分:0)

首先,第一个例子没有用gcc 4.8编译的原因是这是gcc 4.8中的一个错误。 (我稍后会对此进行扩展)。第一个示例使用post-4.8版本的gcc,clang 3.3及更高版本以及Apple基于LLVM的c ++编译器编译,运行和生成与VS2013相同的输出。

关于通用引用:
Scott Meyer创造术语“通用引用”的一个原因是因为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);
}

以上编译并在所有上述编译器上运行,并在gcc 4.8上运行。这个函数通常接受左值和右值作为参数。如果调用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在函数模板的情况下可能是或不是rvalue引用,因此最好将其他内容称为其他内容。迈耶斯选择称他们为“普遍参考”。

为什么这是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参考折叠规则。这两个函数具有完全相同的签名,因此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段,“如果参数模板中的类型是左值引用而参数模板中的类型不是,则参数类型被认为比其他类型更专业”。