C++0x 右值引用模板参数推导
Posted
技术标签:
【中文标题】C++0x 右值引用模板参数推导【英文标题】:C++0x rvalue reference template argument deduction 【发布时间】:2010-10-27 13:49:15 【问题描述】:鉴于GMan's 美味邪恶auto_cast
实用函数炮制here,我一直试图弄清楚为什么当我试图从右值auto_cast
时它不能为我编译(在MSVC 上10.0)。
这是我正在使用的代码:
template <typename T>
class auto_cast_wrapper : boost::noncopyable
public:
template <typename R>
friend auto_cast_wrapper<R> auto_cast(R&& pX);
template <typename U>
operator U() const
return static_cast<U>( std::forward<T>(mX) );
private:
//error C2440: 'initializing': cannot convert from 'float' to 'float &&'
auto_cast_wrapper(T&& pX) : mX(pX)
T&& mX;
;
template <typename R>
auto_cast_wrapper<R> auto_cast(R&& pX)
return auto_cast_wrapper<R>( std::forward<R>(pX) );
int main()
int c = auto_cast( 5.0f ); // from an rvalue
尽我所能,我尝试遵循 C++0x 引用折叠规则和here 概述的模板参数推导规则,据我所知,上面给出的代码应该可以工作。
回想一下,在 0x 之前的 C++ 中,不允许对引用进行引用:像 A& & 之类的东西会导致编译错误。相比之下,C++0x 引入了以下引用折叠规则:
A&&变成A& A& && 变成 A& A&& & 变成 A& A&& && 变为 A&&第二条规则是一个特殊的模板参数推导规则,用于通过对模板参数的右值引用获取参数的函数模板:
template<typename T> void foo(T&&);
这里适用以下规则:
当在 A 类型的左值上调用 foo 时,T 解析为 A&,因此,根据上面的引用折叠规则,参数类型实际上变为 A&。 当在 A 类型的右值上调用 foo 时,T 解析为 A,因此参数类型变为 A&&。
现在,当我将鼠标悬停在对auto_cast( 5.0f )
的调用上时,工具提示正确地将其返回值显示为auto_cast_wrapper<float>
。这意味着编译器正确地遵循了规则 2:
当在 A 类型的右值上调用 foo 时,T 会解析为 A。
既然我们有一个auto_cast_wrapper<float>
,构造函数应该实例化一个float&&
。但是错误消息似乎暗示它实例化了一个float
的值。
这是完整的错误消息,再次显示 T=float 正确但 T&& 参数变为 T?
main.cpp(17): error C2440: 'initializing' : cannot convert from 'float' to 'float &&' You cannot bind an lvalue to an rvalue reference main.cpp(17) : while compiling class template member function 'auto_cast_wrapper<T>::auto_cast_wrapper(T &&)' with [ T=float ] main.cpp(33) : see reference to class template instantiation 'auto_cast_wrapper<T>' being compiled with [ T=float ]
有什么想法吗?
【问题讨论】:
你有没有机会把问题集中一点?与其将编辑内容作为单独的段落放在底部,不如将它们编辑到问题本身。作为读者,我不需要知道您将内容附加到问题的顺序,我只想阅读问题的最佳版本。如果您想自己发布答案,请将其作为实际答案,而不是作为问题底部的另一段。事实上,如果我什至想知道这个问题是关于什么的,我就必须阅读大量的文本和代码。 @jalf:是的,对不起,jalf。我不想创建单独的答案,因为 DeadMG 已经正确回答了。我想我只是认为我这样做是不礼貌的,尽管我认为对它的一些澄清不会让其他人在未来读到这篇文章。额外的问题可能应该是一个完全独立的问题,因为它与原始问题没有真正的关系。 在 SO 上,对已经回答的问题发布新的、更好的答案并没有错。如果您对此感觉不好,请接受 DeadMG 的回答,但在其下方发布您自己的回答。 :) @jalf:好的,我创建了一个单独的答案并将其恢复为原始问题。还是有点长,但说明了我的谬误思路。 【参考方案1】:您忘记将 std::forward T&& 参数传递给 auto_cast_wrapper 构造函数。这打破了转发链。编译器现在发出警告,但它似乎工作正常。
template <typename T>
class auto_cast_wrapper
public:
template <typename R>
friend auto_cast_wrapper<R> auto_cast(R&& pX);
template <typename U>
operator U() const
return static_cast<U>( std::forward<T>(mX) );
private:
//error C2440: 'initializing': cannot convert from 'float' to 'float &&'
auto_cast_wrapper(T&& pX) : mX(std::forward<T>(pX))
auto_cast_wrapper(const auto_cast_wrapper&);
auto_cast_wrapper& operator=(const auto_cast_wrapper&);
T&& mX;
;
template <typename R>
auto_cast_wrapper<R> auto_cast(R&& pX)
return auto_cast_wrapper<R>( std::forward<R>(pX) );
float func()
return 5.0f;
int main()
int c = auto_cast( func() ); // from an rvalue
int cvar = auto_cast( 5.0f );
std::cout << c << "\n" << cvar << "\n";
std::cin.get();
打印一对五。
【讨论】:
那么你的意思是构造函数应该是auto_cast_wrapper(T&& pX) : mX(std::forward<T>(pX))
?我认为你是对的,这是有道理的。但现在我认为它表现出未定义的行为。我认为在转换运算符被触发之前,文字超出了范围。所以这似乎无论如何都不能从右值工作?
@dvide:我用一些右值对其进行了测试——例如,由函数的值返回——它似乎有效。
@DeadMG:你的警告是什么?在什么编译器上?我收到警告:引用成员被初始化为一个临时的,在构造函数退出后不会持续存在。当我测试它时,c
的值被搞砸了。鉴于它在技术上未定义,它可能只是看起来有效?
@DeadMG:正如我所说,我也在 MSVC10 上。我相信警告是正确的,不应该因为它似乎在您的计算机上工作而被忽略。完全相同的代码在调试模式下为我打印一对垃圾数字,在发布模式下为我打印一对五。
我会接受这一点,因为它确实回答了原始问题,但我已经扩展了我的问题,并就这种潜在的未定义行为进行了跟进。【参考方案2】:
很抱歉发布未经测试的代码。 :)
DeadMG 是正确的,该参数也应该被转发。我相信警告是错误的,MSVC 有一个错误。从电话中考虑:
auto_cast(T()); // where T is some type
T()
将持续到完整表达式的末尾,这意味着 auto_cast
函数、auto_cast_wrapper
的构造函数和用户定义的转换都引用了一个仍然有效的对象。
(由于包装器除了转换或破坏之外不能做任何事情,它不能比传递给auto_cast
的值长。)
我修复的可能是让成员只是一个T
。不过,您将进行复制/移动,而不是直接投射原始对象。但也许随着编译器优化它消失了。
不,转发不是多余的。它维护了我们自动转换的值类别:
struct foo
foo(int&) /* lvalue */
foo(int&&) /* rvalue */
;
int x = 5;
foo f = auto_cast(x); // lvalue
foo g = auto_cast(7); // rvalue
如果我没记错的话,转换运算符不应该(当然不需要)标记为const
。
【讨论】:
感谢 GMan。我不知道它应该持续到整个表达式的结尾。这就说得通了。我想这只是一个错误。并感谢完美的转发澄清。这很酷。 @dvide:没有问题。不要承诺 100% 这是一个错误,但我很确定。它仍然只是一个参考(如const&
版本),只能是左值或右值,所以我认为它应该是相同的。感谢您查看。
是的,我对两个版本之间临时性的终生不一致感到有些困惑。据我所知,它们应该是相同的,但我仍然习惯于对引用进行右值,所以我非常谨慎地避免得出任何结论 =)【参考方案3】:
它不编译的原因与它不编译的原因相同:
float rvalue() return 5.0f
float&& a = rvalue();
float&& b = a; // error C2440: 'initializing' : cannot convert from 'float' to 'float &&'
由于a
本身是一个左值,它不能绑定到b
。在auto_cast_wrapper
构造函数中,我们应该再次在参数上使用std::forward<T>
来解决这个问题。请注意,我们可以在上面的特定示例中只使用std::move(a)
,但这不会涵盖也应该与左值一起使用的通用代码。所以auto_cast_wrapper
构造函数现在变成了:
template <typename T>
class auto_cast_wrapper : boost::noncopyable
public:
...
private:
auto_cast_wrapper(T&& pX) : mX( std::forward<T>(pX) )
T&& mX;
;
不幸的是,现在这似乎表现出未定义的行为。我收到以下警告:
警告 C4413: 'auto_cast_wrapper::mX' : 引用成员被初始化为在构造函数退出后不会持续存在的临时对象
在触发转换运算符之前,文字似乎超出了范围。虽然这可能只是 MSVC 10.0 的编译器错误。从 GMan's answer 开始,临时的生命周期应该一直持续到完整表达式的结尾。
【讨论】:
以上是关于C++0x 右值引用模板参数推导的主要内容,如果未能解决你的问题,请参考以下文章
C++进阶第二十五篇——C++11(列表初始化+变量类型推导+右值引用和移动语义+新的类功能+可变模板参数)
C++进阶第二十五篇——C++11(列表初始化+变量类型推导+右值引用和移动语义+新的类功能+可变模板参数)