右值

右值定义

右值与左值其实并没有详细官方的定义,官方的其实是一个类似映射表(这个是左值,这个是右值)。但通常来说,下面这个图能覆盖大多数情况。
rvalue

纯右值(pure right value)

对于大多数临时变量皆为右值。这里的临时变量是指,不能被程序员读写,不存在于内存,其可能只会在寄存器中存在数秒,或者直接被优化掉的变量。

  • literal (不含string literal): 42 true nullptr
  • 返回值不是引用类型的函数
  • this指针
  • lambda

xvalue(expiring lvalue)

即原本为左值,但是程序员确定已经不会再对其读写,可以使用std::move强制让它变为右值。
因此,xvalue必定在一个变量的读写生命周期末尾。

左值

与右值相对,有内存,可以被程序员读写的为左值

  • string literal: "2132"
  • 普通变量: string s
  • 通过指针访问对象或者对象的数据成员
  • 返回是lvalue的函数
  • 通过名字访问lvalue对象的数据成员

一些使用常见错误及其提示

  1. move-from object invalid,类的move构造/拷贝或者其他使用移动语义的函数中,不是使用了move就百分百move了,move底层使用swap实现的,如果一个类的move构造函数的具体实现是copy,就完全没用。另外比如不是类,int,float这种,是没有move的,其实现就是copy,需要注意
  2. 对于一个值不能连续move,违反了生命周期末尾的原则
  3. move const object,移动语义与const矛盾。而此时如果有const重载,就会调用const重载函数。
  4. 函数同类型返回(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