C++ 的值类别与拷贝省略

https://zh.cppreference.com/w/cpp/language/expressions

https://zh.cppreference.com/w/cpp/language/explicit_cast

https://zh.cppreference.com/w/cpp/language/value_category

https://zh.cppreference.com/w/cpp/language/implicit_conversion#.E4.B8.B4.E6.97.B6.E9.87.8F.E5.AE.9E.E8.B4.A8.E5.8C.96

C++ 中的值类别分为三种:

  • 左值(lvalue)
  • 纯右值(prvalue)
  • 亡值(xvalue)

TT&T&& 分别代表了这三种类别(纯右值、左值、亡值)

有两个复合类别:

  • 泛左值(glvalue):包括左值和亡值
  • 右值(rvalue):包括纯右值和亡值
      expression
      /        \
    glvalue   rvalue
    /     \   /    \
lvalue   xvalue   prvalue

值类别的概念非常的乱,有很多历史遗留问题,建议直接去 cppreference 看例子

转型表达式

下面提到了“转型表达式”,看一下什么叫转型表达式:

  • 四种 C++ 风格的强制转换
  • 显式类型转换:
    • C 风格转换,如 (int)5.5
    • 函数风格转换,语法是 类型说明符 (初始化列表或指派初始化器) 或者使用大括号,如 int(5)int{5}std::string{}foo{.x = 1, .y = 3}
  • 用户自定义转换函数:如 operator int()

左值

以下表达式是左值:

  • 变量、函数、模板形参对象、数据成员的名字:

    int a;                    // 左值
    int& r = a;               // 左值
    int&& rr = a;             // 左值(这个是左值!)
    
    int *p = &a;
    int *p = &r;
    int *p = &rr;
    
    void foo() {}
    void (*p)() = &foo;       // 左值
    
    struct foo {};
    template <foo a>
    void func() {
        const foo* obj = &a;  // 左值
    }
    
    struct foo {
        int a;                // 左值
        int& r = a;           // 左值
        int&& rr = a;         // 左值
    };
    
  • 返回类型是左值引用的函数调用或重载运算符表达式:

    int& ref() {
        static int a{3};
        return a;
    }
    
    int&& rref() {
        static int a{3};
        return a;
    }
    
    ref() = 5;    // 左值
    
  • 内建赋值及复合赋值表达式 a = ba += ba %= b

  • 内建前置递增递减表达式 ++a--a

  • 内建间接寻址表达式 *p

  • 内建下标表达式 arr[10]

  • 对象成员表达式 a.m,除非 m 是成员枚举项或者非静态成员函数,或者 a 是右值,而 m 非静态

    struct foo {
        enum bar {
            m
        };
        void func() {};
        static void funcs() {};
    };
    
    foo a;
    a.m = 42;                        // 错误,m 是枚举,并非左值
    void (foo::*p1)() = &a.func;     // 错误,func 是非静态成员函数,这是个纯右值
    void (foo::*p2)() = &foo::func;  // 正确,成员函数指针,是左值
    void (*p3)() = &a.funcs;         // 正确,静态成员函数,是左值
    void (*p4)() = &foo::funcs;      // 正确,静态成员函数,是左值
    
  • 内建指针成员表达式 p->m,除非 m 是成员枚举项或者非静态成员函数

  • 左值对象的成员指针表达式 a.*mp 和内建的指针的成员指针表达式 p->*mp,如果 mp数据成员指针

  • 如果 b 是左值,内建的逗号表达式 a, b 就是左值

  • 对某些 bc 的条件运算符 a ? b : c,这里定义非常复杂,实际上使用时会有 IDE 提示

  • 字符串字面量

  • 到左值引用的转型表达式,如 static_cast<int&>(a)std::

  • 具有左值引用类型的非类型模板形参

    template <int& v>
    void set() {
        v = 5;           // 此时 v 是左值
    }
    

左值可以被取地址,可以放在赋值运算符左侧,可以初始化左值引用

临时量的实质化

任何完整类型 T 的纯右值,可转换成同类型 T 的亡值

此转换以该纯右值初始化一个 T 类型的临时对象(以临时对象作为求值该纯右值的结果对象),并产生一个代表该临时对象的亡值

临时量实质化在下例情况下发生:

  • 绑定引用到纯右值时,const 左值引用或者右值引用
  • 访问类纯右值的数据成员、调用类纯右值的隐式对象成员函数时(C++ 23 时区分了显式和隐式成员函数,显式的可以传 this 进去)
  • 进行数组到指针转换,或在数组纯右值上使用下标时
  • 纯右值作为弃值表达式(不使用返回值,仅仅利用它的副作用)时

纯右值

以下表达式是纯右值:

  • 除字符串字面量外的字面量

  • 返回类型是非引用的函数调用或重载运算符表达式

  • 内建后置自增自减运算符 a++a--

  • 所有内建的算术表达式比较表达式和,如 a + ba * ba << ba < b

  • 内建的取地址表达式 &a

  • 对象成员表达式 a.m 和内建指针成员表达式 p->m,如果 m 是成员枚举项或者非静态成员函数

  • 成员指针表达式 a.*mp 和内建的指针的成员指针表达式 p->*mp,如果 mp成员函数指针

  • 如果 b 是纯右值,内建的逗号表达式 a, b 就是纯右值

  • 对某些 bc 的条件运算符 a ? b : c

  • 到非引用类型的转型表达式,如 static_cast<double>(a)std::string{}

  • this 指针

  • 枚举项

  • 具有标量类型的非类型模板参数

    template <int v>
    void foo() {
        const int* a = &v; // 错误,这里 v 是纯右值
    }
    
  • lambda 表达式

    auto f = [](int x){ return x * x; };   // 右值
    
  • requires 表达式和 concept 的特化

    // 这里右侧的 requires 是一个右值
    template<typename T>
    concept Hashable = requires(T a) {
        { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
    };
    
    // 这是个右值
    Hashable<int>;
    

纯右值的性质:

  • 纯右值不具有多态

  • 纯右值本身不需要 cv 限定符,就算有的话也会被剥掉。除非它被实质化到 cv 限定类型的引用

    int x = 42;
    auto y = static_cast<const int>(x);           // auto = int,因为右侧是个右值,类型是 const int,而 const 会被忽略
    
    int& y2 = static_cast<const int>(x);          // 错误,右侧是个右值,右值不能被赋值给左值引用
    const int& y3 = static_cast<const int>(x);    // 正确,右值可以被实质化,绑定到 const 左值引用
    
  • 纯右值不能具有不完整类型

  • 纯右值不能具有抽象类类型或它的数组类型

亡值

下列表达式是亡值表达式:

  • a.m 对象成员表达式,其中 a 是右值且 m 是对象类型的非静态数据成员

  • a.*mp 对象的成员指针表达式,其中 a 是右值且 mp 是数据成员指针

  • 如果 b 是亡值,内建的逗号表达式 a, b 就是亡值

  • 对某些 bc 的条件运算符 a ? b : c

  • 返回类型是对象的右值引用的函数调用或重载运算符表达式

    std::move(x); // 这个函数返回右值引用,它是个亡值
    
    int&& ref() {
        static int x;
        return std::move(x);
    }
    
    ref();        // 亡值
    
  • 右值数组退化、使用下标

  • 转换到对象的右值引用类型的转型表达式,如 static_cast<int&&>(x)

  • 在临时量实质化后,任何指代该临时对象的表达式(如 const 左值引用?)

  • 有移动资格的表达式(C++ 23):如果表达式作为 returnthrow 等的操作数,那么这个表达式就具有移动资格,从 C++ 23 起,具有移动资格的表达式会视作亡值

亡值的性质:亡值可以绑定到右值引用,并且可以是多态的

泛左值与右值

泛左值的性质:

  • 可以通过左值到右值、数组到指针或函数到指针隐式转换转换成纯右值

    int x = 1;
    int& rx = x;
    int&& rrx = x;
    
    int y = rx;    // 左值到右值的隐式转换
    int z = rrx;   // 亡值到右值的隐式转换
    
    int arr[10];
    int (&rarr)[10] = arr;       // 左值
    // int (&&rrarr)[10] = arr;  // 错误,数组没有右值引用
    int* p = rarr;               // 左值到指针右值的隐式转换
    
  • 可以是多态的

  • 可以具有不完整类型

右值的性质:

  • 不能使用内建取地址符号
  • 不能用作内建赋值运算符及内建复合赋值运算符的左操作数
  • 可以用来初始化 const 左值引用和右值引用,这种情况下该右值所标识的对象的生存期被延长到该引用的作用域结尾

拷贝省略(Copy Elision)

当满足特定条件时,可以省略从某个源对象创建具有相同类类型(忽略 cv 限定)的对象的操作,即使选择的构造函数和/或析构函数具有副作用

这种对象创建操作的消除被称为复制消除(或者拷贝省略)

在下列情形下允许进行拷贝省略(可以合并多次消除):

  • 具名返回值优化(NRVO),当返回一个有名字的右值(return 或者 throw 语句)且返回的那个值是普通的局部变量时,可以直接构造到接收的结果对象中
  • 无名返回值优化(URVO),当返回一个没有名字的右值时,直接构造到结果对象中

C++ 17 中无名返回值优化是强制的,它叫做“传递未实质化的对象”(Passing Unmaterialized Objects)

从 C++ 17 起,非必须不会将纯右值实质化,并且它会被直接构造到其最终目标的存储中,包括:

T f() {
    return U();
}

T g() {
    return f();       // 这里直接进行了拷贝省略,并没有移动或者复制
}

T x = T(T(T(f())));   // 并没有移动或者拷贝,直接构造到了 x 中

限制:只能在已知要初始化的对象不是潜在重叠的子对象时应用此规则