右值
右值定义
右值与左值其实并没有详细官方的定义,官方的其实是一个类似映射表(这个是左值,这个是右值)。但通常来说,下面这个图能覆盖大多数情况。
纯右值(pure right value)
对于大多数临时变量皆为右值。这里的临时变量是指,不能被程序员读写,不存在于内存,其可能只会在寄存器中存在数秒,或者直接被优化掉的变量。
- literal (不含string literal): 42 true nullptr
- 返回值不是引用类型的函数
- this指针
- lambda
xvalue(expiring lvalue)
即原本为左值,但是程序员确定已经不会再对其读写,可以使用std::move强制让它变为右值。
因此,xvalue必定在一个变量的读写生命周期末尾。
左值
与右值相对,有内存,可以被程序员读写的为左值
- string literal: "2132"
- 普通变量: string s
- 通过指针访问对象或者对象的数据成员
- 返回是lvalue的函数
- 通过名字访问lvalue对象的数据成员
一些使用常见错误及其提示
- move-from object invalid,类的move构造/拷贝或者其他使用移动语义的函数中,不是使用了move就百分百move了,move底层使用swap实现的,如果一个类的move构造函数的具体实现是copy,就完全没用。另外比如不是类,int,float这种,是没有move的,其实现就是copy,需要注意
- 对于一个值不能连续move,违反了生命周期末尾的原则
- move const object,移动语义与const矛盾。而此时如果有const重载,就会调用const重载函数。
- 函数同类型返回(return 类型和函数定义返回类型)不要move,因为会自动优化。因此不同类型可以move,会有收益。
move 构造/拷贝函数写法
class Widget {
private:
int i { 0 };
std::string s;
int* p { nullptr };
public:
// Move constructor
explicit Widget(Widget&& other) noexcept
: i(std::move(other.i))
, s(std::move(other.s))
, p(std::move(other.p))
{
other.p = nullptr;
}
Widget& operator=(Widget&& other) noexcept {
if (this == &other) { return *this; }
delete p;
i = std::move(other.i);
s = std::move(other.s);
p = std::move(other.p);
other.p = nullptr;
return *this;
}
};
完美转发(Perfect Forwarding)
为了解决的问题
假如foo 函数想接收一个string 类型的参数,它得写两个函数:
void foo(conststring& str);
void foo(string&& str);
假如foo 函数想接收两个string 类型的参数,它得写四个函数:
void foo(conststring& str1, conststring& str2);
void foo(conststring& str1, string&& str2);
void foo(string&& str1, conststring& str2);
void foo(string&& str1, string&& str2);
完美转发写法
template <typename T>
void foo(T&& value) {
bar(std::forward<T>(value));
}
为什么有用? —— 引用折叠
实际类型 | 折叠后 | |
---|---|---|
& & | -> | & |
&& & | -> | & |
& && | -> | & |
&& && | -> | && |
完美转发条件
格式对 + 有推导
- 格式就是上面的写法格式,不允许变化
- 有推导就是类型推导。
有推导反例
template<typename T>
class MyVec {
public:
void push_back(T&& x); // 没有推导,因为类定义的时候T已经定了
};
需要注意的地方
- 不能转发多次,这和不能move多次是一个道理。也会遇到和move一样的问题(传进来两个名字不同的参数,单实际上是同一个)
- 完美转发的重载会带来一定的问题,因为完美转发的范围实在太广了。(详见effective modern c++, item26 & 27)
小trick —— noexcept
对于可能存在异常的函数,编译器会比较保守,但如果程序员已经说了noexcept,那么编译器会放心优化,大胆move。推荐使用的4个地方。
- move拷贝
- move移动
- swap
- 内存分配器的deallocate