文章

C++值类别与引用

C++值类别与引用

值类别汇总

C++ 中存在的各种值类别(以及对值的引用)

  • 泛左值 - glvalue
  • 左值 - lvalue
  • 将亡左值 - xlvalue
  • 纯右值 - prvalue
  • 右值 - rvalue

C++ 中的每个表达式都会生成一个值,该值属于上面所列的五个类别之一。

C++ 语言及其工具和规则的许多方面都需要正确理解这些值类别以及对它们的引用。 这些方面包括

  • 获取值的地址
  • 复制值
  • 移动值
  • 将值转发给另一函数。

glvalue - 泛左值 (i)

泛左值(即 glvalue,全称为 generalized lvalue)有标识。 我们使用“i”作为“has identity”(有标识)的简写形式。

lvalue - 左值 (i&!m)

左值(一种泛左值)有标识,但不可移动。 这些通常都是可以传递的读-写值,传递方法包括引用传递、常量引用传递、值传递(如果复制的开销很低)。 左值不能绑定到右值引用。

xlvalue - 将亡值 (i&m)

将亡值(一种泛左值,但也是一种右值)有标识,但也不可移动。 将亡值可以是以前的某个左值。由于复制开销很高,你已经决定移动此左值,但需注意不要在以后再访问它。 下面演示如何将左值转换为将亡值。

1
2
3
struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

在上述代码示例中,我们还没有移动任何值。 我们只是将左值强制转换为未命名的右值引用,通过这种方式创建了一个将亡值。 该值仍可通过其左值名称进行标识,但作为将亡值,它现在可以移动了。

将左值强制转换为将亡值(请记住,这是一种右值)以后,该值就可以绑定到右值引用了。

下面是两个其他的将亡值示例:一个示例调用可返回未命名右值引用的函数,另一个示例访问将亡值的成员。

1
2
3
4
struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue - 纯右值 (!i&m)

纯右值(即 prvalue,全称为 pure rvalue,一种右值)没有标识,但可移动。

此类值通常为

  • 临时值,
  • 调用可返回值的函数后的结果
  • 为对任何其他不属于泛左值的表达式求值后的结果。

rvalue - 右值 (m)

右值可移动。 我们将使用“m”作为“is movable”(可移动)的简写形式。

右值引用始终引用右值(假定其内容不需保留的值)。

但是,右值引用本身是否为右值? 未命名右值引用(例如在上面的将亡值代码示例中显示的引用)为将亡值,因此属于右值。 它首选绑定到右值引用函数参数,例如移动构造函数的参数。 相反(也许违背直觉),如果右值引用有名称,则包含该名称的表达式为左值。 因此,它不能绑定到右值引用参数。 但是,让它那样做很容易,将它再次强制转换为未命名右值引用(将亡值)即可。

1
2
3
4
5
6
7
8
void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

引用折叠规则

一个表达式中有多个相同的引用(对左值引用进行左值引用,或者对右值引用进行右值引用)时,会互相取消。

  • A& & 折叠成 A&
  • A&& && 折叠成 A&&

一个表达式中有多个不同的引用时,会折叠成一个左值引用。

  • A& && 折叠成 A&
  • A&& & 折叠成 A&

转发引用

比较右值引用与转发引用的不同概念。

在“forwarding reference”(转发引用)一词被造出之前,一些人使用“universal reference”(通用引用)一词。

1
void foo(A&& a) { ... }
  • A&& 为右值引用,如前所述。 常量和易失性不适用于右值引用。
  • foo 只接受 A 类型的右值。
  • 之所以存在右值引用(例如 A&&),是因为这样可以创作一个重载,该重载经过优化,适用于需要传递临时值(或其他右值)的情况。
1
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& 是转发引用。 类型 _Ty 可以是常量/非常量(独立于易失性/非易失性),具体取决于传递给 bar 的内容。
  • bar 接受任何类型为 _Ty 的左值或右值。
  • 传递左值会导致转发引用变成 _Ty& &&,后者折叠成左值引用 _Ty&
  • 传递右值会导致转发引用变成右值引用_Ty&&
  • 使用转发引用(例如 _Ty&&)不是为了优化,而是为了获取传递的值并以透明且高效的方式对其进行转发。 只有在编写(或深入研究)库代码时,才可能遇到转发引用,例如,一个在构造函数参数基础上进行转发的工厂函数。

详细说明

左值有标识 - An lvalue has identity

如果你有(或者可以获取)某个值的内存地址,并且可以安全地使用它,则说明该值有标识。

这样,你除了比较值的内容,还可以执行其他操作:

  • 按标识比较
  • 区分它们。

“lvalue”(左值)中的“l”是“left”(例如,在赋值的 left 侧,即左侧)的缩写,但目前这只具有历史意义

在 C++ 中,左值可以出现在赋值的左侧,也可以出现在右侧。 因此,并不能根据“lvalue”(左值)中的“l”(左)来了解或确定值的实际位置。

称某个值为左值,只是表明该值有标识。

属于左值的表达式的示例包括:

  • 命名的变量或常量
  • 返回引用的函数。

不属于左值的表达式的示例包括:

  • 临时值
  • 返回值的函数。
1
2
3
4
5
6
7
8
9
10
11
12
int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

现在可以说左值有标识,而将亡值也是这样。现在,我们只需知道有一个名为“glvalue”(全称为“generalized lvalue”,即“泛左值”)的值类别。

泛左值集包含左值(也称为“经典左值”)和将亡值的超集。

因此,虽然“左值有标识”这种说法是正确的,但严格说来,应该是泛左值集有标识,如下图所示。

右值可移动,左值不能 - An rvalue is movable; an lvalue is not

有的值你不能获取其内存地址(或者说,你不能依赖该值获得有效性)。 我们在上面的代码示例中看到过一些这样的值。

没有可靠的内存地址听起来像是一个缺点。 但实际上,此类值的优势是可以移动它(通常开销很低),而不是复制它(通常开销很高)。

可移动的值称为“右值”(或“经典右值”)。

“rvalue”(右值)中的“r”是“right”(例如,在赋值的 right 侧,即右侧)的缩写。

但是,你可以在赋值外部使用右值以及右值的引用。 那么,就不需关注“rvalue”(右值)中的“r”(右)了。

称某个值为右值,只是表明该值可移动。

与之相反,左值不可移动,如下图所示。 如果要移动左值,那么就会与左值的定义相矛盾。 对于合理预期能够继续访问左值的代码而言,这可能会产生意外的问题。

因此,不能移动左值。 但是,有一种泛左值(带标识的值集)是可以移动的,前提是你知道如何操作(例如,注意不要在移动后访问它),这就是将亡值。

右值引用以及引用绑定规则 - Rvalue references, and reference-binding rules

右值引用是移动和转发问题解决方案的必要部分。

但是,在讨论右值引用之前,我们首先需要更清楚地了解 T&,我们以前直接称之为“引用”。 它实际上是“左值(非常量)引用”,引用的是允许引用用户写入的值。

1
2
template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

左值引用可以绑定到左值,但不能绑定到右值。

此外还有左值常量引用 (T const&),引用的是不可供引用用户写入的对象(例如常量)

1
2
template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

左值常量引用可以绑定到左值或右值。

类型为 T 的右值引用的语法可以表示为 T&&。 右值引用引用可移动值,即其内容在使用后不需保留的值(例如临时值)。

由于关键是移动(并修改)绑定到右值引用的值,因此不会将 constvolatile 限定符(也称 cv 限定符)应用到右值引用。

1
2
template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

右值引用绑定到右值。 事实上,在进行重载决策时,右值首选绑定到右值引用而不是左值常量引用。 但是,右值引用不能绑定到左值,因为如前所述,右值引用引用的是假定其内容不需保留的值(例如,移动构造函数的参数)。

也可在需要传值参数的情况下,通过复制构造传递右值(或者在右值是将亡值的情况下,通过移动构造来传递)。

泛左值有标识,纯右值没有 - A glvalue has identity; a prvalue does not

目前,介绍了什么值有标识。 还介绍了什么值可以移动,什么值不能移动。

但是,还没有介绍没有标识的值集。 该集称为“prvalue”(全称“pure rvalue”,即“纯右值”)。

1
2
3
4
5
6
7
8
int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

本文由作者按照 CC BY 4.0 进行授权