C++ 左值、右值、将亡值

分享
C++ 左值、右值、将亡值
Photo by Ed Leszczynskl / Unsplash

考虑以下代码:

class DiskFile {
public:
  DiskFile(const std::string file_path_) :file_path(file_path_) {}

protected:
  std::string file_path;
};

我们可以这样实例化:

DiskFile df_1("./test.df");
DiskFile df_2(std::string("./test.df"));

这其中的问题是,file_path被复制了一次,file_path_被复制一次,有性能损失。我们可以通过std::move进行优化:

class DiskFile {
public:
  DiskFile(const std::string file_path) :file_path(std::move(file_path)) {}

protected:
  std::string file_path;
};

现在file_path_被复制一次,被移动一次,省去一次复制。注意,移动后file_path_无法再正常使用,只能使用file_path

这是多数初学者认识std::move的典型场景。


表达式的划分

expression被C++Draft分为glvaluervalue。其中glvalue被分为lvaluexvaluervalue被分为prvaluexvalue

注意这里讨论的是“表达式的划分”而不是“值的划分”。

glvalue

glvalue is an expression whose evaluation determines the identity of an object, function, non-static data member, or a direct base class relationship.

这类表达式重点在于“定位一个对象”,而非简单的数学运算。比如:

int a = 10;
int b[2] = {10,20};
int* p = a;

a;     //指向变量a
*p;    //指向变量a
b[1];  //指向数组b的第1号元素

这个被定为的对象,通常有自己的生命周期,通常可以被取地址,非const通常可以被修改。

prvalue

prvalue is an expression whose evaluation initializes an object or computes the value of an operand of an operator, as specified by the context in which it appears, or an expression that has type cv void.

这类表达值重点在于计算出一个值,这个值可以被用来初始化一个对象,或被用来继续计算。比如:

42
a + b
a * 2
std::string("hello")
[]() -> int { return 1; }() //一个返回1的Lambda表达式

这类被计算出的值通常不能被取地址,更进一步说,他们通常不能对应到内存中一个长期存在的对象。

xvalue

An xvalue is a glvalue that denotes an object whose resources can be reused (usually because it is near the end of its lifetime).

这类值又被称为“将亡值”,通常指向一个确定的对象,但这个对象的资源可以被重新利用。比如:

std::string s = "hello";

std::move(s);  // 将亡值
std::string s2 = std::move(s); //资源被重新利用

这里我们需要理解std::move做了什么,其源码:

template <class _Tp>
[[__nodiscard__]] inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR __libcpp_remove_reference_t<_Tp>&&
move(_LIBCPP_LIFETIMEBOUND _Tp&& __t) _NOEXCEPT {
  using _Up _LIBCPP_NODEBUG = __libcpp_remove_reference_t<_Tp>;
  return static_cast<_Up&&>(__t);
}

重点在函数体内的两行代码(看返回值类型也行),先是移除了参数的引用属性,然后强制转换为“右值引用”。可见,std::move没有真正发生数据上的“move”,只是改变了表达式的类型成为xvalue,进而引导编译器在赋值时在“复制构造”前先尝试“移动构造”。

注意,在std::string s2 = std::move(s);后,s在下一次构造前已经不能再使用。

lvalue

An lvalue is a glvalue that is not an xvalue.

lvalue中排除了xvalue就是lvalue

参考

  1. 7 Expressions