即将超出范围的变量是左值还是x值?

Mag*_*ero 4 c++ lvalue language-lawyer move-semantics xvalue

当一个即将超出范围的变量被返回或抛出时,它的资源可以被重用,即它可以从 中移出,如下面的 C++ 程序所示:

#include <iostream>

struct X {
    X() {
        std::cout << "X()\n";
    }
    
    X(X&&) {
        std::cout << "X(X&&)\n";
    }
    
    ~X() {
        std::cout << "~X()\n";
    }
};

X f() {
    X x;
    std::cout << "return\n";
    return x;
}

int main() {
    try {
        X x = f();
        std::cout << "throw\n";
        throw x;
    } catch (...) {
        std::cout << "catch\n";
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

编译(我们使用标志关闭复制/移动省略-fno-elide-constructors)、链接和执行:

clang++ -std=c++17 -O2 -Wall -pedantic -pthread -fno-elide-constructors\
main.cpp && ./a.out
Run Code Online (Sandbox Code Playgroud)

输出:

X()
返回
X(X&&)
~X()
抛出
X(X&&)
~X()
catch
~X()

x在上面的语句中return x;, 和throw x;表示一个资源可以重用的对象。

工作草案,编程语言 C++ 标准[basic.lval-1]中,我们对值类别有以下定义:

  • 是一个表达式,其计算确定对象或函数的身份。
  • xvalue是一个泛左,表示其资源可以重用的对象(通常是因为它已接近其生命周期的终点)。
  • 是不是 xvalue 的左值。

那么是x左值还是x值呢?

son*_*yao 6

x本身就是一个左值

\n
\n

以下表达式是左值表达式:

\n
    \n
  • 变量、函数或数据成员的名称, a template parameter object (since C++20),无论类型如何,例如 std::cin 或 std::endl。即使变量的类型是右值引用,由其名称组成的表达式也是左值表达式;
  • \n
\n
\n

对于局部变量 as x,在return 语句throw 表达式中,初始化的重载决策分两阶段进行;首先,好像x是一个右值表达式(然后可以选择移动构造函数)。

\n

回报声明

\n
\n

or, for co_return, to select the overload of promise.return_value() (since C++20)(C++11 起) 然后执行两次重载解析来选择用于初始化返回值的构造函数:

\n
    \n
  • 首先,就好像表达式是右值表达式(因此它可以选择移动构造函数),并且
  • \n
\n
\n

抛出表达式中:

\n
\n
    \n
  • 如果左值表达式命名局部变量或函数或 catch 子句参数,其范围不超过最内层封闭的 try 块(如果有),则也可以通过与 return 语句中相同的两步重载决策来调用左值表达式的移动构造函数(自 C++17 起)
  • \n
\n
\n

作为效果,在这两种情况下都选择了移动构造函数。这只是returnand的特殊之处throw,并不意味着xrvaluexvalue完全。如果您像X x2(x);in那样编写f(),则将选择复制构造函数(并导致错误,因为复制构造函数被隐式删除)。

\n

根据标准,[class.copy.elision]/3

\n
\n

隐式可移动实体是具有自动存储持续时间的变量,它可以是非易失性对象,也可以是对非易失性对象类型的右值引用。在以下复制初始化上下文中,在尝试复制操作之前首先考虑移动操作:

\n

(3.1) -\n如果 return ([stmt.return]) 或 co_\xc2\xadreturn ([stmt.return.coroutine]) 语句中的表达式是一个(可能带括号的)id 表达式,它命名了声明的隐式可移动实体在最内层封闭函数或 lambda 表达式的主体或参数声明子句中,或者

\n

(3.2) -\n如果 throw 表达式 ([expr.throw]) 的操作数是一个(可能带括号的)id 表达式,它命名了一个隐式可移动实体,该实体属于不包含该复合语句的作用域最里面的 try 块或函数 try 块(如果有),其复合语句或构造函数初始化器包含 throw 表达式,

\n

首先执行重载解析,以选择要复制的构造函数或要调用的 return_\xc2\xadvalue 重载,就好像表达式或操作数是右值一样。如果第一次重载决策失败或未执行,则会再次执行重载决策,并将表达式或操作数视为左值。

\n
\n

[expr.prim.id.unqual]/3 :

\n
\n

如果实体是函数、变量、结构化绑定、数据成员或模板参数对象,则表达式是左值,否则是纯右值 ([basic.lval]);

\n
\n

[基本.lval]

\n
\n

(1.1) -\n泛左值是一个表达式,其计算确定对象或函数的身份。

\n

(1.2) -\n纯右值是一个表达式,其求值初始化一个对象或计算一个运算符的操作数的值(由它出现的上下文指定),或者是一个类型为 cv void 的表达式。

\n

(1.3) -\nxvalue 是一个泛左值,表示其资源可以重用的对象(通常是因为它已接近其生命周期的末尾)。

\n

(1.4) -\n左值是不是 x 值的左值。

\n

(1.5) -\n右值是右值或x值。

\n

[注 3:表达式是 x 值,如果它是:

\n

(4.1) -\n调用函数的结果,无论是隐式还是显式,其返回类型是对对象类型的右值引用 ([expr.call]),

\n

(4.2) -\na 转换为对象类型的右值引用 ([expr.type.conv], [expr.dynamic.cast], [expr.static.cast] [expr.reinterpret.cast], [expr.const .cast], [expr.cast]),

\n

(4.3) -\na 使用 xvalue 数组操作数 ([expr.sub]) 进行下标操作,

\n

(4.4) -\na 指定非引用类型的非静态数据成员的类成员访问表达式,其中对象表达式是 xvalue ([expr.ref]),或者

\n

(4.5) -\na .* 指向成员的指针表达式,其中第一个操作数是 xvalue,第二个操作数是指向数据成员 ([expr.mptr.oper]) 的指针。

\n
\n

  • @songyuanyao 是完全正确的,它是一个左值,但是在特殊情况下左值的资源可以被重用,请参阅[class.copy.elision]。左值的资源在某些特定情况下可以重用这一事实并不意味着它是 xvalue。 (2认同)