我们知道,常量左值引用既可以绑定左值,又可以绑定右值,这是C++98就存在的特性。对于如下模板函数,使用了常量左值引用,并在函数中调用了std::vector的emplace_back函数:
#include <iostream>
#include <vector>
struct MyStruct {
MyStruct() { std::cout << "construct is called\n"; }
~MyStruct() { std::cout << "destruct is called\n"; }
MyStruct(const MyStruct&) {std::cout << "copy construct is called\n";}
MyStruct(MyStruct&&) { std::cout << "move construct is called\n"; }
};
std::vector<MyStruct> g_v;
template<typename T>
void MyFunc(const T& param) noexcept
{
std::cout << "MyFunc is called\n";
g_v.emplace_back(param);
}
int main()
{
g_v.reserve(100);
MyStruct a;
MyFunc(a);
MyFunc(MyStruct());
return 0;
}
运行的效率不怎么样,因为函数入参是常量左值引用,在调用emplace_back函数的内部发生了拷贝动作,可以看看运行结果:
construct is called
MyFunc is called
copy construct is called
construct is called
MyFunc is called
copy construct is called
destruct is called
destruct is called
destruct is called
destruct is called
为了解决这个冗余拷贝的问题,我们首先想到了前面讨论过的Universal Reference:
#include <iostream>
#include <vector>
struct MyStruct {
MyStruct() { std::cout << "construct is called\n"; }
~MyStruct() { std::cout << "destruct is called\n"; }
MyStruct(const MyStruct&) {std::cout << "copy construct is called\n";}
MyStruct(MyStruct&&) { std::cout << "move construct is called\n"; }
};
std::vector<MyStruct> g_v;
template<typename T>
void MyFunc(T&& param) noexcept
{
std::cout << "MyFunc is called\n";
g_v.emplace_back(param);
}
int main()
{
g_v.reserve(100);
MyStruct a;
MyFunc(a);
MyFunc(MyStruct());
return 0;
}
Universal Reference可以把右值转成右值引用,左值转成左值引用。测试结果出乎预料:
construct is called
MyFunc is called
copy construct is called
construct is called
MyFunc is called
copy construct is called
destruct is called
destruct is called
destruct is called
destruct is called
和前面没有区别,查看emplace_back的定义:
template< class... Args >
void emplace_back( Args&&... args );
是一个Universal Reference,左值推导出左值,因此不能触发移动构造,这个可以理解,但是右值引用最后也不能触发移动构造,这个就有点说不过去了。据说是这个原因:虽然是右值引用,但是因为是函数的命名参数,所以编译器仍然把这个作为左值来看待,因此不触发移动构造(编译器比较保守),只有调用std::move或者std::forward或者使用static_cast显示类型强转为右值引用,编译器才会触发移动构造,那么我们先加std::move试试看:
#include <iostream>
#include <vector>
struct MyStruct {
MyStruct() { std::cout << "construct is called\n"; }
~MyStruct() { std::cout << "destruct is called\n"; }
MyStruct(const MyStruct&) {std::cout << "copy construct is called\n";}
MyStruct(MyStruct&&) { std::cout << "move construct is called\n"; }
};
std::vector<MyStruct> g_v;
template<typename T>
void MyFunc(T&& param) noexcept
{
std::cout << "MyFunc is called\n";
g_v.emplace_back(std::move(param));
}
int main()
{
g_v.reserve(100);
MyStruct a;
MyFunc(a);
MyFunc(MyStruct());
return 0;
}
测试结果令人满意,两次拷贝构造调用都变成了移动构造调用:
construct is called
MyFunc is called
move construct is called
construct is called
MyFunc is called
move construct is called
destruct is called
destruct is called
destruct is called
destruct is called
如果改成std::forward:
#include <iostream>
#include <vector>
struct MyStruct {
MyStruct() { std::cout << "construct is called\n"; }
~MyStruct() { std::cout << "destruct is called\n"; }
MyStruct(const MyStruct&) {std::cout << "copy construct is called\n";}
MyStruct(MyStruct&&) { std::cout << "move construct is called\n"; }
};
std::vector<MyStruct> g_v;
template<typename T>
void MyFunc(T&& param) noexcept
{
std::cout << "MyFunc is called\n";
g_v.emplace_back(std::forward<T>(param));
}
int main()
{
g_v.reserve(100);
MyStruct a;
MyFunc(a);
MyFunc(MyStruct());
return 0;
}
测试结果是传入右值对象那个调用触发了移动构造:
construct is called
MyFunc is called
copy construct is called
construct is called
MyFunc is called
move construct is called
destruct is called
destruct is called
destruct is called
destruct is called
这个其实也就是std::forward和std::move的区别,std::move一律强转为右值引用,而std::forward只是转发,把右值或右值引用强转为右值引用,把左值和左值引用强转为左值引用。可以看一下gcc的std::forward的源码:
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value,
"std::forward must not be used to convert an rvalue to an lvalue");
return static_cast<_Tp&&>(__t);
}
两个局部特化的模板,一个用于强转为左值引用,一个用于强转为右值引用。其实相当于static_cast<T&&>(param);实现成两个特化版本,主要是为了防止使用者不当使用场景下,通过static_assert断言,引起编译失败。当然std::forward目的就是为了转发,因此不应该在所有场景下都去做右值类型转换。
小结一下:
1)仅仅通过Universal Reference的类型推导获得的结果类型,还不足以实现完美转发,还要和std::forward相结合才行,并且std::forward仅适合在模板函数中使用;
2)std::move会对所有类型强制转换成右值引用,而std::forward仅对右值或者右值引用强制转换成右值引用,对左值或左值引用则会强转为左值引用。
3)在模板函数中,通过Universal Reference的隐式类型推导得到的右值引用,编译器不会触发移动构造,必须要通过std::move或者std::forward或者static_cast显示类型转换为右值引用,编译器才会触发移动构造。
参考资料:
《深入理解C++11》文章来源:https://www.toymoban.com/news/detail-469912.html
《Effective Modern C++》文章来源地址https://www.toymoban.com/news/detail-469912.html
到了这里,关于现代C++技术研究(8)---完美转发的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!