Value Categories

Value categories - cppreference.com 有更详细的说明。

分类

lvalue / rvalue / xvalue 是互斥的,C++ 表达式只能是三者之一。glvalue 和 rvalue 都是复合类别:glvalue 包括 lvalue 和 xvalue,rvalue 包括 prvalue 和 xvalue。在翻译上,xvalue 叫(将)亡值,glvalue 叫泛左值。

一部分规则(一些理所当然的就在这里略去):

类型规则
lvalue具名的数据是 lvalue,比如变量名、函数名、template parameter object 名(C++ 20,见下方代码块)、左值引用类型的 NTTP 名。还有字符串字面量。
prvalue字面量、枚举名、lambda 表达式等。标量类型的 NTTP 名。C++ 20 后 requires 表达式和 concept 的特化也是 prvalue。
xvaluervalue 的成员访问(. / .* / -> / ->*)是 xvalue。

Template Parameter Object

在 C++ 20 中被改为 lvalue。

#include <iostream>
struct Foo {};
template <Foo v>
void set()
{
    // 这里要求 v 是 lvalue,C++20 可以编译,C++17 不行
    std::cout << &v << "\n";
}
void foo()
{
    set<Foo{}>();
}

Concept 特化

template <typename T>
struct is_int { enum { value = false }; };

template <>
struct is_int<int> { enum { value = true }; };

static_assert(is_lvalue<decltype((std::is_scalar_v<int>))>::value);
static_assert(is_prvalue<decltype((std::equality_comparable<int>))>::value); // concept 特化
static_assert(is_prvalue<decltype((is_int<int>::value))>::value);

注意 concept 的特化是 prvalue,但是以往 trait 类中的静态成员(一般写成 static inline constexpr)却是 lvalue!(除非 trait 类的 value 是用 enum 实现的,就像上面代码块的 is_int 一样)。

如何得到 xvalue

xvalue 是比较特殊的一种:给人的感觉是要么是转换过来的(人为标记),要么是直接访问右值成员(不是通过成员函数访问)得到的。用右值引用转换得到的是 xvalue

template<typename T>
struct value_category {
    // Or can be an integral or enum value
    static constexpr auto value = "prvalue";
};

template<typename T>
struct value_category<T&> {
    static constexpr auto value = "lvalue";
};

template<typename T>
struct value_category<T&&> {
    static constexpr auto value = "xvalue";
};

// Double parens for ensuring we inspect an expression,
// not an entity
#define VALUE_CATEGORY(expr) value_category<decltype((expr))>::value

这里照着写了一下:

struct str {
    str() = default;
    explicit str(const char *s) : start(strdup(s)) {}

    // overloaded operators
    char &operator[size_t index](size_t index) { return start.get()[index]; }
    str &operator=(const char *s) { start.reset(strdup(s)); return *this; }

    // data
    struct freer { void operator()(void *p) { free(p); } };
    std::unique_ptr<char, freer> start;
};

结果:

str{}       => prvalue
str{} = ""  => lvalue
str{}[0]    => lvalue
str{}.start => xvalue

也可见经过运算符重载的访问不遵循 xvalue 的生成规则。

其他

如果是函数调用或者强制类型转换,则尊重其返回类型。如果用的是未重载的逗号操作符,则返回最右边的类型。如果是三元条件表达式,则返回两个分支的公共类型。

Pending member function call 是纯右值,但是除非用来调用,否则不能出现在表达式中,比如 a.f 不合法,而 a.f() 合法,其中 f 是类型 decltype(a) 的一个成员函数。

Void expressions 是 prvalue。

左值对象的 Bit-fields 是泛左值,能被赋值,但不能取地址,也不能绑定在非常量的引用上。对位域的常量引用或者右值引用会创建一份临时拷贝,不会真正绑定在位域上。

Move-eligible expressions 指的是在 return / co_return / throw 等位置出现的左值,它们视情况被当作 lvalue 或者 rvalue 看待(C++ 23 之前),或者被当作 rvalue 看待(C++ 23 及之后)。见 P2266R3 Simpler implicit move 对 C++23 函数返回表达式值类别的改变

C++ 20 之后,template parameter object 也是 lvalue,注意得是 object,标准类型不行。

左右值的转换要用 static_cast 来实现,reinterpret_cast 做不到。