当函数接受 T&& 的参数并传递左值时,模板参数 T 是否应该解析为 T&?
Should template parameter T be resolving to T& when a function takes argument of T&& and is passed an lvalue?
我正在尝试在 C++ 中实现完美转发,因此我编写了以下快速而肮脏的代码来测试它。
class CValue
{
public:
CValue(int i) :
m_i(i)
{
printf("Default constructor called!\r\n");
}
CValue(const CValue& src) :
m_i(src.m_i)
{
printf("Copy constructor called!\r\n");
}
CValue(CValue&& src) :
m_i(src.m_i)
{
printf("Move constructor called!\r\n");
}
CValue& operator=(const CValue& src)
{
m_i = src.m_i;
printf("Copy assignment called!\r\n");
return *this;
}
CValue& operator=(CValue&& src)
{
m_i = src.m_i;
printf("Move assignment called!\r\n");
return *this;
}
int m_i;
};
template <typename T>
void PerfectForwarding(T&& tValue)
{
T tValue1 = tValue;
tValue1.m_i = 10;
}
int _tmain(int argc, _TCHAR* argv[])
{
CValue v(0);
PerfectForwarding(v);
printf("%d\r\n", v.m_i);
return 0;
}
当我构建并运行此代码作为控制台应用程序时,我得到了答案 10,而我期望的是 0。
这行好像是:
T tValue1 = tValue;
在 PerfectForwarding 函数中被解析为:
CValue& tValue1 = tValue;
而不是:
CValue tValue1 = tValue;
所以编译器将 T 解析为 CValue&,这是我没想到的。
我尝试通过显式声明模板参数类型从 _tmain 调用函数,即
PerfectForwarding<CValue>(v);
但是编译失败并出现以下错误:
error C2664: 'void PerfectForwarding<CValue>(T &&)' : cannot convert
argument 1 from 'CValue' to 'CValue &&'
with
[
T=CValue
]
You cannot bind an lvalue to an rvalue reference
我可以通过将 PerfectForwarding 函数中的行更改为以下内容来强制执行所需的行为:
typename std::remove_reference<T>::type tValue1 = tValue;
但我不认为这是必要的。根据引用折叠规则,参数类型 (T&&) 应该变成 CValue&(如 T&& & -> T&),但是 T 本身应该只是 CValue,对吧?这是 VC12 编译器处理右值引用的错误,还是我误解了右值引用和模板?
我在调试中使用 Visual Studio 2013(VC12 编译器)并关闭所有优化。
行为正确。当参数是左值时,通过推导类型的左值引用来实现完美转发。
你是说
By the reference collapsing rules the type of the argument (T&&
) should become CValue&
(as T&& &
-> T&
), but T
itself should be simply CValue
, surely?
但是,如果 T
只是 CValue
,T&& &
中的 &
从何而来? T&&
作为转发引用的全部原因是 T
成为左值引用,在你的例子中是 CValue&
,然后引用折叠得到 T &&
-> CValue & &&
-> CValue &
.
标准的相关部分是C++11 14.8.2.1/3(P
是函数模板参数类型,A
是调用中参数的类型,定义在 14.8.2.1/1):
If P
is a cv-qualified type, the top level cv-qualifiers of P
’s type are ignored for type deduction. If P
is a
reference type, the type referred to by P
is used for type deduction. If P
is an rvalue reference to a cv-unqualified
template parameter and the argument is an lvalue, the type “lvalue reference to A
” is used in
place of A
for type deduction.
(强调我的)
当编译器只看到 T&&
时,它会尝试将 T
推断为允许使用您提供的任何内容调用该函数的东西。因此,当使用左值调用时,您最终 T
成为 Cvalue&
,因此引用崩溃(正如您所指出的那样)可以开始。
当你试图转发这个论点时,这很重要。 std::forward<T>(v)
将参数转发为左值或右值,具体取决于 T
是什么。如果它被推导为左值引用,它将作为左值转发,如果没有,它将作为右值转发。类型 T
是唯一的区别。
根据引用折叠规则,参数类型 (T&&) 应该变成 CValue&(如 T&& & -> T&),但 T 本身应该只是 CValue,对吗?
如果使用左值调用,它将 (T&& v)
"okay, I can't bind an rvalue reference to an lvalue, but if I make T
itself be an lvalue reference then this works." T
推导为 Cvalue&
,以便 (T&& v)
扩展为 (Cvalue& && v)
。现在引用已折叠 (Cvalue& v)
。类型 T
必须是左值引用类型才能工作。
如果您明确提供模板参数,那么您并没有真正解决问题。解决此问题的第一种方法是您发现的 remove_reference
。您还可以使用 auto
这更有意义,因为这是通用编程
auto tValue1 = tValue;
在任何情况下,auto
都不会推导出引用。对于您提供的内容,您最好使用可以绑定到左值和右值的 const
左值引用。
template <typename T>
void PerfectForwarding(const T& tValue)
{
T tValue1 = tValue;
tValue1.m_i = 10;
}
这不允许转发,但无论如何它都不是转发功能。
如果你想以不同的方式处理左值,你可以提供两个重载
template <typename T>
void PerfectForwarding(T& tValue);
template <typename T>
void PerfectForwarding(T&& tValue);
当使用左值调用时,前者将是首选。
编译器做了正确的事情。在像
这样的行中
template<typename T>
void f(T&& param){}
如果传递左值,T
将被推断为引用,如果传递右值,则将推断为简单类型。所以 int x; f(x);
将 T
推导为 int&
,而 f(1)
将 T
推导为 int
.
这就是完美转发的工作原理。基本上,您有以下参考折叠规则:
& & -> &
& && -> &
&& & -> &
&& && -> &&
另见
Syntax for universal references
了解更多详情。
我正在尝试在 C++ 中实现完美转发,因此我编写了以下快速而肮脏的代码来测试它。
class CValue
{
public:
CValue(int i) :
m_i(i)
{
printf("Default constructor called!\r\n");
}
CValue(const CValue& src) :
m_i(src.m_i)
{
printf("Copy constructor called!\r\n");
}
CValue(CValue&& src) :
m_i(src.m_i)
{
printf("Move constructor called!\r\n");
}
CValue& operator=(const CValue& src)
{
m_i = src.m_i;
printf("Copy assignment called!\r\n");
return *this;
}
CValue& operator=(CValue&& src)
{
m_i = src.m_i;
printf("Move assignment called!\r\n");
return *this;
}
int m_i;
};
template <typename T>
void PerfectForwarding(T&& tValue)
{
T tValue1 = tValue;
tValue1.m_i = 10;
}
int _tmain(int argc, _TCHAR* argv[])
{
CValue v(0);
PerfectForwarding(v);
printf("%d\r\n", v.m_i);
return 0;
}
当我构建并运行此代码作为控制台应用程序时,我得到了答案 10,而我期望的是 0。
这行好像是:
T tValue1 = tValue;
在 PerfectForwarding 函数中被解析为:
CValue& tValue1 = tValue;
而不是:
CValue tValue1 = tValue;
所以编译器将 T 解析为 CValue&,这是我没想到的。
我尝试通过显式声明模板参数类型从 _tmain 调用函数,即
PerfectForwarding<CValue>(v);
但是编译失败并出现以下错误:
error C2664: 'void PerfectForwarding<CValue>(T &&)' : cannot convert
argument 1 from 'CValue' to 'CValue &&'
with
[
T=CValue
]
You cannot bind an lvalue to an rvalue reference
我可以通过将 PerfectForwarding 函数中的行更改为以下内容来强制执行所需的行为:
typename std::remove_reference<T>::type tValue1 = tValue;
但我不认为这是必要的。根据引用折叠规则,参数类型 (T&&) 应该变成 CValue&(如 T&& & -> T&),但是 T 本身应该只是 CValue,对吧?这是 VC12 编译器处理右值引用的错误,还是我误解了右值引用和模板?
我在调试中使用 Visual Studio 2013(VC12 编译器)并关闭所有优化。
行为正确。当参数是左值时,通过推导类型的左值引用来实现完美转发。
你是说
By the reference collapsing rules the type of the argument (
T&&
) should becomeCValue&
(asT&& &
->T&
), butT
itself should be simplyCValue
, surely?
但是,如果 T
只是 CValue
,T&& &
中的 &
从何而来? T&&
作为转发引用的全部原因是 T
成为左值引用,在你的例子中是 CValue&
,然后引用折叠得到 T &&
-> CValue & &&
-> CValue &
.
标准的相关部分是C++11 14.8.2.1/3(P
是函数模板参数类型,A
是调用中参数的类型,定义在 14.8.2.1/1):
If
P
is a cv-qualified type, the top level cv-qualifiers ofP
’s type are ignored for type deduction. IfP
is a reference type, the type referred to byP
is used for type deduction. IfP
is an rvalue reference to a cv-unqualified template parameter and the argument is an lvalue, the type “lvalue reference toA
” is used in place ofA
for type deduction.
(强调我的)
当编译器只看到 T&&
时,它会尝试将 T
推断为允许使用您提供的任何内容调用该函数的东西。因此,当使用左值调用时,您最终 T
成为 Cvalue&
,因此引用崩溃(正如您所指出的那样)可以开始。
当你试图转发这个论点时,这很重要。 std::forward<T>(v)
将参数转发为左值或右值,具体取决于 T
是什么。如果它被推导为左值引用,它将作为左值转发,如果没有,它将作为右值转发。类型 T
是唯一的区别。
根据引用折叠规则,参数类型 (T&&) 应该变成 CValue&(如 T&& & -> T&),但 T 本身应该只是 CValue,对吗?
如果使用左值调用,它将 (T&& v)
"okay, I can't bind an rvalue reference to an lvalue, but if I make T
itself be an lvalue reference then this works." T
推导为 Cvalue&
,以便 (T&& v)
扩展为 (Cvalue& && v)
。现在引用已折叠 (Cvalue& v)
。类型 T
必须是左值引用类型才能工作。
如果您明确提供模板参数,那么您并没有真正解决问题。解决此问题的第一种方法是您发现的 remove_reference
。您还可以使用 auto
这更有意义,因为这是通用编程
auto tValue1 = tValue;
在任何情况下,auto
都不会推导出引用。对于您提供的内容,您最好使用可以绑定到左值和右值的 const
左值引用。
template <typename T>
void PerfectForwarding(const T& tValue)
{
T tValue1 = tValue;
tValue1.m_i = 10;
}
这不允许转发,但无论如何它都不是转发功能。
如果你想以不同的方式处理左值,你可以提供两个重载
template <typename T>
void PerfectForwarding(T& tValue);
template <typename T>
void PerfectForwarding(T&& tValue);
当使用左值调用时,前者将是首选。
编译器做了正确的事情。在像
这样的行中template<typename T>
void f(T&& param){}
如果传递左值,T
将被推断为引用,如果传递右值,则将推断为简单类型。所以 int x; f(x);
将 T
推导为 int&
,而 f(1)
将 T
推导为 int
.
这就是完美转发的工作原理。基本上,您有以下参考折叠规则:
& & -> &
& && -> &
&& & -> &
&& && -> &&
另见
Syntax for universal references
了解更多详情。