为什么"universal references"具有与右值引用相同的语法?

Why "universal references" have the same syntax as rvalue references?

本文关键字:引用 语法 references universal 为什么      更新时间:2023-10-16

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

#include <iostream>
template <typename T>
void f(T& a)
{
    std::cout << "f(T& a) for lvaluesn";
}
template <typename T>
void f(T&& a)
{
    std::cout << "f(T&& a) for rvaluesn";
}
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中是否应该有这种差异(编译器与intellisense,但事实上可能有一些意义)。好的,回到问题上来。我是这样解决的:

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

现在没有任何类型崩溃,只是(r/l)值的正常重载。它是在g++上编译的,intellisense停止了抱怨,我几乎满意了。几乎,因为我想了一下,如果我想改变对象状态中由右值引用传递的东西,该怎么办?我可以在必要的时候描述一些情况,但这种描述太长了,无法在这里介绍。我是这样解决的:

template <typename T>
void f(T&& a, std::true_type)
{
    std::cout << "f(T&& a) for rvaluesn";
}
template <typename T>
void f(T&& a, std::false_type)
{
    std::cout << "f(T&& a) for lvaluesn";
}
template <typename T>
void f(T&& a)
{
    f(std::forward<T>(a), std::is_rvalue_reference<T&&>());
}

现在它在所有测试过的编译器上编译,它允许我在右值引用实现中更改对象状态,但它看起来不太好,这是因为通用引用和右值引用的语法相同。所以我的问题是:为什么C++委员会没有为通用引用引入另一种语法?我认为这个特征应该用信号来表示,例如,T?,汽车或类似的东西,但不是像T&amp;以及auto&amp;它们只是与右值引用冲突。使用这种方法,我的第一个实现将是完全正确的,不仅仅是对于MS编译器。有人能解释委员会的决定吗?

我认为情况正好相反。最初的想法是在语言中引入右值引用,这意味着"提供双符号和引用的代码不关心被引用的对象会发生什么"。这允许移动语义。这太好了。

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

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

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

& & -> &

现在,我们有两种引用,其中&&表示"我不关心对象可能发生的事情",&表示"我可能关心对象可能会发生的事情,所以你最好注意自己在做什么"。考虑到这一点,崩溃规则自然会流动:只有在没有人关心对象发生了什么的情况下,C++才应该崩溃对&&的引用:

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

有了这些规则,我想是Scott Meyers注意到了这一规则子集:

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

表明&&在引用折叠方面是右中立的,并且当类型推导发生时,T&&构造可以用于匹配任何类型的引用,并为这些引用创造了术语"通用引用"。这不是委员会发明的东西。这只是其他规则的副作用,而不是委员会的设计。

因此,引入该术语是为了区分REAL右值引用和UNIVERSAL引用,前者在没有发生类型推导时保证为&&,后者在模板专用化时不保证保持&&

其他人已经提到引用折叠规则是通用引用工作的关键,但还有另一个(可以说)同样重要的方面:当模板参数的形式为T&&时,模板参数推导。

事实上,关于这个问题:

为什么"通用引用"与右值引用具有相同的语法?

在我看来,template参数的形式更重要,因为这一切都与语法有关。在C++03中,模板函数无法知道传递对象的值类别(右值或左值)。C++11更改了模板参数推导以解释这一点:14.8.1.1〔temp.dexecute.call〕/p3

[…]如果P是对cv不合格模板参数的右值引用,并且参数是左值,则使用类型"对A的左值引用"代替A进行类型推导。

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

如果P是形式为cvT&&的右值引用类型,其中T是模板类型参数,自变量是左值,则推导出的T的模板自变量值为A&。[示例:

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

---结束示例]

更详细地说,上面的调用触发f<int&>(int& &&)的实例化,在应用引用折叠之后,CCD_18变为f<int&>(int&)。另一方面,CCD_ 20实例化CCD_。(注意< ... >中没有&。)

没有其他形式的声明会将T推导出int&并触发f<int&>( ... )的实例化。(请注意,&可以出现在( ... )之间,但不能出现在CCD29之间。)

总之,当执行类型推导时,语法形式T&&允许原始对象的值类别在函数模板主体内可用。

与此相关的是,请注意,必须使用std::forward<T>(arg)而不是std::forward(arg),因为记录原始对象的值类别的是T(而非arg)。(谨慎起见,std::forward的定义"人为地"迫使后一种编译失败,以防止程序员犯下这个错误。)

回到最初的问题:"为什么委员会决定使用T&&格式而不是选择新的语法?"

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

您回答了自己的问题:"通用引用"只是引用折叠的右值引用情况的名称。如果引用折叠需要另一种语法,那么它就不再是引用折叠了。引用折叠只是将引用限定符应用于引用类型。

所以我想知道g++是怎么想的。当然它没有编译。

你的第一个例子已经形成。GCC 4.9毫无怨言地编译了它,输出与MSVC一致。

几乎,因为我想,如果我想改变通过右值引用的对象状态,该怎么办?

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

首先,第一个示例没有使用gcc 4.8编译的原因是这是gcc 4.8中的一个错误。(我稍后会详细介绍)。第一个示例使用gcc、clang 3.3及更高版本的4.8后版本以及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 referencesn";
    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可能是也可能不是右值引用,因此最好调用其他参数。迈耶斯选择称之为"通用参考文献"

为什么这是gcc 4.8中的一个错误:
在问题中的第一个示例代码中,关于对f(a)的调用,函数模板template <typename T> void f(T&)template <typename T> void f(T&&)变为f<int>(int&)f<int&>(int&),后者得益于C++11引用折叠规则。这两个函数具有完全相同的签名,所以也许gcc 4.8是正确的,对f(a)的调用是不明确的。事实并非如此。

调用不含糊的原因是,根据第13.3节(重载解决方案)、第14.5.6.2节(函数模板的部分排序)和第14.8.2.4节(在部分排序过程中推导模板参数)的规则,template <typename T> void f(T&)template <typename T> void f(T&&)更专业。当将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段,"如果参数模板中的类型是左值引用,而参数模板中不是,则该参数类型被认为比另一个更专业"。

由于c++11中的引用折叠规则,通用引用可以工作。如果你有

template <typename T> func (T & t)

引用崩溃仍然会发生,但它不能与临时引用一起工作,因此引用不是"通用的"。通用引用被称为"通用",因为它可以接受lvals和rvals(还保留其他限定符)。T & t不是通用的,因为它不能接受rvals。

总之,通用引用是引用折叠的产物,通用引用之所以如此命名,是因为它是通用的,它可以是