更多精彩内容,请关注微信公众号:后端技术小屋
现代C++之右值语义
在现代C++的众多特性中,右值语义(std::move和std::forward)大概是最神奇也最难懂的特性之一了。本文简要介绍了现代C++中右值语义特性的原理和使用。
1 什么是左值,什么是右值?
int a = 0; // a是左值,0是右值
int b = rand(); // b是左值,rand()是右值
直观理解:左值在等号左边,右值在等号右边
深入理解:左值有名称,可根据左值获取其内存地址,而右值没有名称,不能根据右值获取地址。
2 引用叠加规则
左值引用A&
和右值引用A&&
可相互叠加, 叠加规则如下:
A& + A& = A&
A& + A&& = A&
A&& + A& = A&
A&& + A&& = A&&
举例说明,在模板函数void foo(T&& x)
中:
- 如果
T
是int&
类型,T&&
为int&
,x
为左值语义 - 如果
T
是int&&
类型,T&&
为int&&
,x
为右值语义
也就是说,不管输入参数x
为左值还是右值,都能传入函数foo
。区别在于两种情况下,编译器推导出模板参数T
的类型不一样。
3 std::move
3.1 What?
在C++11中引入了std::move
函数,用于实现移动语义
。它用于将临时变量(也有可能是左值)的内容直接移动给被赋值的左值对象。
3.2 Why?
知道了std::move
是干什么的,他能给我们的搬砖工作带来哪些好处呢? 举例说明:
如果类X包含一个指向某资源的指针,在左值语义下,类X的复制构造函数定义如下:
X::X()
{
// 申请资源(指针表示)
}
X::X(const X& other)
{
// ...
// 销毁资源
// 克隆other中的资源
// ...
}
X::~X()
{
// 销毁资源
}
假设应用代码如下。其中,对象tmp被赋给a之后,便不再使用。
X tmp;
// ...经过一系列初始化...
X a = tmp;
在上面的代码中,执行步骤:
- 先执行一次默认构造函数(默认构造tmp对象)
- 再执行一次复制构造函数(复制构造a对象)
- 退出作用域时执行析构函数(析构tmp和a对象)
从资源的视角来看,上述代码中共执行了2次资源申请和3次资源释放。
那么问题来了,既然对象tmp只是一个临时对象,在执行X a = tmp;
时,对象a
能否将tmp
的资源'偷'过来,直接为我所用,而不影响原来的功能? 答案是可以。
X::X(const X& other)
{
// 使用std::swap交换this和other的资源
}
通过'偷'对象tmp的资源,减少了资源申请和释放的开销。而std::swap
交换指针代价极小,可忽略不计。
3.3 How?
到现在为止,我们明白了std::move
将要达到的效果,那么它究竟是怎么实现的呢?
template
typename remove_reference::type&&
std::move(T&& a) noexcept
{
typedef typename remove_reference::type&& RvalRef;
return static_cast(a);
}
不管输入参数为左值还是右值,都被remove_reference
去掉其引用属性,RvalRef
为右值类型,最终返回类型为右值引用。
3.4 Example
在实际使用中,一般将临时变量作为std::move
的输入参数,并将返回值传入接受右值类型的函数中,方便其'偷取'临时变量中的资源。需要注意的是,临时变量被'偷'了之后,便不能对其进行读写,否则会产生未定义行为。
#include
#include
#include
#include
void foo(const std::string& n)
{
std::cout << "lvalue" << std::endl;
}
void foo(std::string&& n)
{
std::cout << "rvalue" << std::endl;
}
void bar()
{
foo("hello"); // rvalue
std::string a = "world";
foo(a); // lvalue
foo(std::move(a)); // rvalue
}
int main()
{
std::vector a = {"hello", "world"};
std::vector b;
b.push_back("hello"); // 开销:string复制构造
b.push_back(std::move(a[1])); // 开销:string移动构造(将临时变量a[1]中的指针偷过来)
std::cout << "bsize: " << b.size() << std::endl;
for (std::string& x: b)
std::cout << x << std::endl;
bar();
return 0;
}
4 std::forward
4.1 What?
std::forward
用于实现完美转发。那么什么是完美转发呢?完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
简单来说,std::move
用于将左值或右值对象强转成右值语义,而std::forward
用于保持左值对象的左值语义和右值对象的右值语义。
4.2 Why?
#include
#include
void bar(const int& x)
{
std::cout << "lvalue" << std::endl;
}
void bar(int&& x)
{
std::cout << "rvalue" << std::endl;
}
template
void foo(T&& x)
{
bar(x);
}
int main()
{
int x = 10;
foo(x); // 输出:lvalue
foo(10); // 输出:lvalue
return 0;
}
执行以上代码会发现,foo(x)
和foo(10)
都会输出lvalue
。foo(x)
输出lvalue
可以理解,因为x
是左值嘛,但是10
是右值,为啥foo(10)
也输出lvalue
呢?
这是因为10
只是作为函数foo
的右值参数,但是在foo
内部,10
被带入了形参x
,而x
是一个有名字的变量,即右值,因此foo
中bar(x)
还是输出lvalue
。
那么问题来了,如果我们想在foo
函数内部保持x
的右值语义,该怎么做呢?std::forward
便派上了用场。
只需改写foo
函数:
template
void foo(T&& x)
{
bar(std::forward(x));
}
4.3 How?
std::forward
听起来有点神奇,那么它到底是如何实现的呢?
template
shared_ptr factory(Arg&& arg)
{
return shared_ptr(new T(std::forward(arg)));
}
template
S&& forward(typename remove_reference::type& a) noexcept
{
return static_cast(a);
}
X x;
factory(x);
如果factory
的输入参数是一个左值,那么Arg = X&
,根据叠加规则,std::forward
。因此,在这种情况下,std::forward
仍然是左值。
相反,如果factory输入参数是一个右值,那么Arg = X
,std::forward
。这种情况下,std::forward
是一个右值。
恰好达到了保留左值or右值语义的效果!
4.4 Example
直接上代码。如果前面都懂了,相信这段代码的输出结果也能猜个八九不离十了。
#include
#include
void overloaded(const int& x)
{
std::cout << "[lvalue]" << std::endl;
}
void overloaded(int&& x)
{
std::cout << "[rvalue]" << std::endl;
}
template void fn(T&& x)
{
overloaded(x);
overloaded(std::forward(x));
}
int main()
{
int i = 10;
overloaded(std::forward(i));
overloaded(std::forward(i));
overloaded(std::forward(i));
fn(i);
fn(std::move(i));
return 0;
}
推荐阅读
- STL源码分析--vector
- STL源码分析--hashtable
- STL源码分析--algorithm
- zookeeper client原理总结
- redis实现分布式锁
- 推荐几个好用的效率神器
- C/C++关键字之restrict
更多精彩内容,请扫码关注微信公众号:后端技术小屋。如果觉得文章对你有帮助的话,请多多分享、转发、在看。