【问题标题】:Is a variable that is about to go out of scope an lvalue or an xvalue?即将超出范围的变量是左值还是值?
【发布时间】:2021-11-05 00:22:41
【问题描述】:

当一个即将超出范围的变量被返回或抛出时,它的资源可以被重用,即它可以被移出,如下面的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()

上述语句中的xreturn x;throw x;表示一个资源可以重复使用的对象。

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

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

x 是左值还是 x 值?

【问题讨论】:

    标签: c++ language-lawyer move-semantics lvalue xvalue


    【解决方案1】:

    x 本身就是一个lvalue

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

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

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

    return statement:

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

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

    throw expression:

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

    在这两种情况下都选择了移动构造函数作为效果。这只是returnthrow 的特殊情况,并不意味着x 完全是rvaluexvalue。如果你在f()中写X x2(x);之类的东西,复制构造函数将被选中(并导致错误,因为复制构造函数被隐式删除)。

    来自标准,[class.copy.elision]/3

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

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

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

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

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

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

    [basic.lval]:

    (1.1) - 泛左值是一个表达式,它的求值决定了一个对象或函数的身份。

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

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

    (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.cast], [ expr.cast]),

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

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

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

    【讨论】:

    • 谢谢。您能否引用 C++ 标准而不是辅助来源(cppreference 很棒,但我想要一个仅来自主要来源的答案)?
    • @Maggyero 答案已修改。
    • 谢谢。 x 正是:“一个(可能用括号括起来的)id-expression,它命名了一个隐式可移动的实体”。并且 [[expr.prim.id.unqual#3]](timsong-cpp.github.io/cppwp/expr.prim.id.unqual#3) 对不合格的 id 表达式指定了这一点:'如果实体是函数、变量、结构化绑定、数据成员或模板,则表达式是左值参数对象和纯右值([basic.lval]);如果标识符指定一个位域,它就是一个位域。所以x 似乎是一个左值。但是x 似乎也符合 xvalues 的定义,因为它的资源可以重复使用。
    • @Maggyero 在特殊的returnthrow 情况下,它只是被视为右值表达式。由于它没有列在xvalue 的定义中,所以它不是严格意义上的xvalue。是的,我同意这些描述有点令人困惑。
    • @songyuanyao 是完全正确的,它是一个左值,但是有一些特殊情况下,左值的资源可以被重用,参见[class.copy.elision]。 在某些特定情况下可以重用左值的资源这一事实并不意味着它是一个 xvalue。
    【解决方案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";
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-07-18
      • 1970-01-01
      • 1970-01-01
      • 2016-04-30
      • 1970-01-01
      • 1970-01-01
      • 2015-04-21
      相关资源
      最近更新 更多