为啥“通用引用”与右值引用具有相同的语法?
Posted
技术标签:
【中文标题】为啥“通用引用”与右值引用具有相同的语法?【英文标题】:Why "universal references" have the same syntax as rvalue references?为什么“通用引用”与右值引用具有相同的语法? 【发布时间】:2013-12-20 06:52:03 【问题描述】:我刚刚对那些(相当)新特性进行了一些研究,我想知道为什么 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
但是有一件可疑的事情:智能感知在 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++ 上编译,智能感知停止抱怨,我几乎满意。几乎,因为我想如果我想改变由右值引用传递的对象状态中的某些东西怎么办?我可以在必要时描述一些情况,但是这个描述太长了,不能在这里展示。我是这样解决的:
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 编译器。谁能解释委员会的决定?
【问题讨论】:
因为右值引用和通用引用密切相关,T &
引用也会发生折叠,但你不能有一个需要 lval 的临时 (&&
)
通用引用并不是一个“真实”的东西,因为它是一个特定的语言术语。所以委员会给他们一个语法的前提根本没有意义,因为委员会对语言中的东西没有概念。通用引用是指围绕右值引用和模板推导的行为,并非正式地引用它。语法相同的原因是因为它们是相同的东西:右值引用。通用引用只是引用右值引用行为的一个子集。
@Piotrek 不应该在第一个函数中说“f(T& a) for rvalues\n”吗?
到目前为止的答案并没有真正回答这个问题(Cassio Neri 的除外)。 “通用参考”不仅仅是正常参考行为的一个子集。区别在于只有当参数是T&&
,那么T
才可以推导出为引用类型。与参数 T
或 T&
的情况相比,这是一种新行为,例如,在这种情况下,T
永远不会被推导出为引用类型。 (类型推导后发生引用折叠)。 OP 正在询问为什么相同的旧语法突然有一个新案例的理由,而不是引入新语法。
在 C++11 的开发过程中,有些人认为应该使用新的语法,因为令人困惑的是,T
可以根据参数列表中T
的使用方式推断出引用类型.但他们失去了选票。
【参考方案1】:
我认为情况正好相反。最初的想法是在语言中引入右值引用,这意味着“提供双与号引用的代码不关心被引用对象会发生什么”。这允许移动语义。这个不错。
现在。该标准禁止构建对引用的引用,但这始终是可能的。考虑:
template<typename T>
void my_func(T, T&) /* ... */
// ...
my_func<int&>(a, b);
在这种情况下,第二个参数的类型应该是int & &
,但这在标准中是明确禁止的。所以必须折叠引用,即使在 C++98 中也是如此。在 C++98 中,只有一种引用,所以折叠规则很简单:
& & -> &
现在,我们有两种引用,其中&&
表示“我不关心对象可能发生的事情”,&
表示“我可能关心对象可能发生的事情,所以你最好注意你在做什么”。考虑到这一点,折叠规则自然而然地流动:只有当没有人关心对象会发生什么时,C++ 才应该折叠对&&
的引用:
& & -> &
& && -> &
&& & -> &
&& && -> &&
有了这些规则,我认为是 Scott Meyers 注意到了这部分规则:
& && -> &
&& && -> &&
表明&&
在引用折叠方面是中性的,并且当发生类型推导时,T&&
构造可用于匹配任何类型的引用,并为这些创造了术语“通用引用”参考。这不是委员会发明的东西。这只是其他规则的副作用,而不是委员会的设计。
因此引入了该术语来区分真正的右值引用,当没有发生类型推导时,保证为&&
,以及那些类型推导的通用引用,不保证保持&&
在模板专业化时。
【讨论】:
Lauren,折叠规则很明确。我不明白为什么类型扣除很重要。好吧,类型是直接或间接指定的。为什么折叠规则应该不同?你最好修改你的答案。 @KirillKobelev:感谢您的编辑。看来我的语法需要它。我不明白你在评论中的建议。你是说折叠规则有什么不同?我提到了 3 组折叠规则,依次为:C++98 的无数折叠规则、C++11 的折叠规则和右侧有&&
的 C++ 折叠规则的子集。【参考方案2】:
其他人已经提到引用折叠规则是通用引用工作的关键,但还有另一个(可以说)同样重要的方面:当模板参数的形式为 T&&
时,模板参数推导。
其实,关于这个问题:
为什么“通用引用”与右值引用具有相同的语法?
在我看来,模板参数的形式更重要,因为这都是关于语法的。在 C++03 中,模板函数无法知道传递对象的值类别(右值或左值)。 C++11 更改了模板参数推导以解决此问题:14.8.2.1 [temp.deduct.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& &&)
的实例化,在应用了引用折叠后,它变成了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。其次,最重要的是,这是一个非常简单的解决方案,可以在标准中声明(一个段落更改)并由编译器实现。请不要误会我的意思。我并不是说委员会成员很懒(他们当然不是)。我只是说他们将附带损害的风险降到最低。
【讨论】:
【参考方案3】:您回答了自己的问题:“通用引用”只是引用折叠的右值引用案例的名称。如果引用折叠需要另一种语法,则不再是引用折叠。引用折叠只是将引用限定符应用于引用类型。
所以我想知道 g++ 对此有何看法。当然它没有编译。
您的第一个示例格式正确。 GCC 4.9编译无怨无悔,输出与MSVC一致。
几乎,因为我想如果我想改变对象状态中的某些东西,它是通过右值引用传递的?
右值引用不适用const
语义;您始终可以更改move
传递的对象的状态。可变性对于它们的目的是必要的。虽然有 const &&
这样的东西,但你永远不需要它。
【讨论】:
如果“你的第二个例子”,你指的是带有void f(const T&& a)
的那个,那么我不确定引用崩溃发生在哪里。类型推导使用无引用的类型,因此对于第一次重载,T
被推导为非引用类型;第二个重载 f(const T&&)
不包含通用引用(因为 c 限定符),所以这里的 T
也没有推导出为引用类型。
@DyP 你是对的; const
排除 T
作为参考。这样的重载只能匹配实际的const &&
引用,如果不将move
应用于常量左值,则很难获得。 (const
会自动从任何纯右值的类型中删除。)
就我而言(我已经在 GCC 4.7.3 和 4.8.1 上测试过),编译器给出了这个编译错误:test.cpp: In function 'int main()': test.cpp :18:8: 错误: 重载 'f(int&)' 的调用不明确 test.cpp:18:8: 注意: 候选人是: test.cpp:4:6: 注意: void f(T&) [with T = int] test.cpp:10:6: note: void f(T&&) [with T = int&] 我知道 const 语义会关闭通用引用,而这正是我想要实现的。但我希望能够在非 const 模板的情况下也将其关闭,这就是重点。
投反对票,因为第一个示例无法使用 gcc 4.8 编译。正确编译它的第一个 g++ 版本是 gcc 4.9。 gcc-5 和 gcc-6 系列也可以正确编译。
@DavidHammen 已修复。我一定一直在使用主干构建,并且版本号没有被碰撞。【参考方案4】:
首先,第一个示例没有用 gcc 4.8 编译的原因是这是 gcc 4.8 中的一个错误。 (稍后我将对此进行扩展)。第一个示例使用 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
可能是也可能不是右值引用,因此最好将它们称为其他东西。 Meyers 选择称它们为“通用参考”。
为什么这是 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)
的调用是模棱两可的。不是。
调用没有歧义的原因是template <typename T> void f(T&)
比template <typename T> void f(T&&)
更专业,根据第 13.3 节(重载解决)、14.5.6.2(函数模板的部分排序)和 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 段,“如果参数模板中的类型是左值引用,而参数模板中的类型不是,则认为参数类型比其他类型更专业”。
【讨论】:
“对 T& 的调用是完美匹配 (...)。对 T&& 的调用需要左值到右值的转换”。根据偏序规则,我认为T&
比T&&
更专业,这不是转化排名的问题
为什么你认为T&&
需要左值到右值的转换??
@PiotrSkotnicki - 你是对的。我会编辑答案。【参考方案5】:
由于 c++11 中的引用折叠规则,通用引用有效。如果你有
template <typename T> func (T & t)
引用折叠仍然会发生,但它不适用于临时,因此引用不是“通用的”。通用引用被称为“通用”,因为它可以接受 lvals 和 rvals(也保留其他限定符)。 T & t
不是通用的,因为它不能接受 rvals。
总而言之,通用引用是引用折叠的产物,通用引用被命名是因为它是通用的,它可以是什么
【讨论】:
以上是关于为啥“通用引用”与右值引用具有相同的语法?的主要内容,如果未能解决你的问题,请参考以下文章