4

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

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;
}

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

clang++ -std=c++17 -O2 -Wall -pedantic -pthread -fno-elide-constructors\
main.cpp && ./a.out

输出:

X()
返回
X(X&&)
~X()
抛出
X(X&&)
~X()
捕获
~X()

x在上述语句中return x;throw x;表示其资源可以重用的对象。

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

  • 泛左值是一个表达式,它的求值决定了一个对象或函数的身份。
  • xvalue是一个glvalue,它表示一个对象的资源可以被重用(通常是因为它接近其生命周期的尽头)。
  • 左值是不是 xvalue的左值。

那么是x左值还是xvalue?

4

2 回答 2

6

x本身就是一个左值

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

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

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

回报声明

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

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

抛出表达式中

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

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

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

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

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

(3.2) - 如果 throw 表达式 ([expr.throw]) 的操作数是一个(可能带括号的)id 表达式,它命名一个隐式可移动实体,该实体属于不包含最里面的复合语句的范围try-block 或 function-try-block(如果有),其复合语句或 ctor-initializer 包含 throw 表达式,

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

[expr.prim.id.unqual]/3

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

[基本.lval]

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

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

(1.3) - 一个xvalue是一个glvalue,它表示一个对象的资源可以被重用(通常是因为它接近其生命周期的尽头)。

(1.4) - 左值是不是 xvalue 的左值。

(1.5) - rvalue 是 prvalue 或 xvalue。

[注 3:表达式是一个 xvalue,如果它是:

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

(4.2) - 对对象类型的右值引用的强制转换 ([expr.type.conv], [expr.dynamic.cast], [expr.static.cast] [expr.reinterpret.cast], [expr.const.演员], [expr.cast]),

(4.3) - 具有 xvalue 数组操作数 ([expr.sub]) 的下标操作,

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

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

于 2021-09-08T08:35:07.143 回答
2

那么 x 是左值还是 x 值?

这是一个左值。

[class.copy.elision] 允许左值像 xvalues 一样被移动,但这并不意味着它们实际上xvalues。它们可以在特定上下文中以与 xvalues 类似的方式处理,例如 return 语句的操作数或 throw 表达式。

x在 #1 #2 和 #3 中仍然是左值:

    try {
        X x = f();   // #1
        auto x2 = x; // #2
        std::cout << "throw\n";
        throw x;     // #3
    } catch (...) {
        std::cout << "catch\n";
    }
于 2021-09-08T23:55:00.660 回答