【问题标题】:Member rvalue references and object lifetime成员右值引用和对象生存期
【发布时间】:2023-03-22 23:06:02
【问题描述】:

自从我上次查看临时生命周期规则以来已经有一段时间了,我不记得成员右值引用如何影响 终生。

以下面两段代码为例:

int main() 
{
    std::get<0>(std::forward_as_tuple(
        [](int v){ std::cout << v << std::endl; }
    ))(6);
    return 0;
}

,

int main() 
{
    auto t = std::forward_as_tuple(
        [](int v){ std::cout << v << std::endl; }
    );
    std::get<0>(t)(6);
    return 0;
}

如果成员右值引用不影响生命周期规则,我希望第一个示例表现良好,而第二个示例 未定义(因为包含 lambda 对象的完整表达式 以第一个分号结束)。

C++11、C++14 和 C++17 如何处理给定的示例?三者有区别吗?

【问题讨论】:

    标签: c++ c++11 c++14 language-lawyer c++17


    【解决方案1】:

    自 98 年以来,在任何版本的 C++ 中,临时生命周期延长的规则都没有改变。我们可能有新的方式来表示临时变量,但一旦它们存在,它们的生命周期就会被很好地理解。

    应该注意的是,您的示例并不真正适用于任何类型的成员引用。您正在使用临时调用函数,并且参数是转发引用。因此,有问题的临时对象绑定到 函数参数 引用,而不是成员引用。该临时变量的生命周期将在调用该函数的表达式之后结束,就像传递给引用参数的任何临时变量一样。

    这个函数 (forward_as_tuple) 最终将该引用存储在 tuple 中的事实是无关紧要的。你对引用所做的事情不会改变它的生命周期。

    再一次,那是 C++98,后来的版本都没有改变它。

    【讨论】:

    • C++14 中(在 C++17 中将再次出现)关于将引用绑定到临时子对象的更改
    • @M.M 引用(不是双关语)?我以为只是形式改变了,但实际效果是一样的?
    • @M.M 是的,但是函数调用始终是一个例外,并且该例外一直保持不变。我在回答中加入了实际措辞,但这里一切都是正确的。
    【解决方案2】:

    当您将临时对象直接绑定到除构造函数初始化程序列表之外的任何位置的引用时,生命周期扩展适用。 (注意:聚合初始化不是构造函数)

    std::forward_as_tuple 是一个函数。任何临时传递给它的生命周期都不能延长到当前行之外。

    基本上,临时文件默认持续到当前行的末尾。 (那个地方实际上并不是当前行的结尾)。在您的两种情况下,它是当前行的结尾(;),临时结束其生命周期。对于第一种情况,这已经足够长了;在第二种情况下,临时代码已失效,您的代码表现出未定义的行为。

    同时:

    struct foo {
      int&& x;
    };
    
    int main() {
      foo f{3};
      std::cout << f.x << "\n";
    }
    

    定义完美。没有构造函数,我们将一个临时对象绑定到一个(右值)引用,从而延长了生命周期。

    添加这个:

    struct foo {
      int&& x;
      foo(int&& y):x(y) {}
    };
    

    struct foo {
      int&& x;
      foo(int y):x((int)y) {}
    };
    

    现在是 UB。

    第一个是因为我们在调用 ctor 时将临时对象绑定到了右值引用。构造函数的内部是无关紧要的,因为那里没有直接绑定的临时对象。然后函数的参数和临时参数都超出了main 的范围。

    第二个是因为在构造函数初始化列表中将临时 (int)y0 绑定到 int&amp;&amp;x 的规则不会像在其他地方一样延长生命周期。

    【讨论】:

    • 使用临时对象的生命周期来检测意外临时对象并抛出异常或断言:stackoverflow.com/a/13168668/34509。可能有助于理解各种规则。
    • @JohannesSchaub-litb 这个答案的一部分是(几乎?)C++17 中的新规则已经过时了。
    • 可能会在其中抛出一些std::moves/casts,因为构造函数中的 y 是左值。
    【解决方案3】:

    因为这是一个 问题。延长寿命的规则在 [class.temporary] 中。从 C++11 到 C++14 到 C++17 的措辞并没有以与这个特定问题相关的方式发生变化。规则是:

    有 [two/three] 上下文,其中临时对象在与完整表达式末尾不同的点被销毁。第一个上下文是调用默认构造函数来初始化数组元素时 [...]

    [second/third] 上下文是引用绑定到临时的。引用的临时对象 绑定或作为引用所绑定的子对象的完整对象的临时对象仍然存在 在引用的生命周期内,除了:
    — 在函数调用 (5.2.2) 中绑定到引用参数的临时对象将持续存在直到完成 包含调用的完整表达式。

    这个表达式:

    std::forward_as_tuple([](int v){ std::cout << v << std::endl; })
    

    涉及将引用(forward_as_tuple 中的参数)绑定到纯右值(lambda 表达式),在 C++11/14 中明确提到它作为创建临时的上下文:

    类类型的临时对象是在各种上下文中创建的:将引用绑定到纯右值,[...]

    在 C++17 中是这样写的:

    创建临时对象
    (1.1) — 当一个纯右值被具体化以便它可以用作一个左值时 (4.4),

    无论哪种方式,我们都有一个临时对象,它绑定到函数调用中的引用,因此临时对象会一直存在,直到包含调用的完整表达式完成为止。

    所以没关系:

    std::get<0>(std::forward_as_tuple(
        [](int v){ std::cout << v << std::endl; }
    ))(6);
    

    但这通过一个悬空引用调用:

    auto t = std::forward_as_tuple(
        [](int v){ std::cout << v << std::endl; }
    );
    std::get<0>(t)(6);
    

    因为临时函数对象的生命周期在初始化t的语句结束时结束。

    请注意,这与成员右值引用无关。如果我们有类似的东西:

    struct Wrapper {
        X&& x;
    };
    
    Wrapper w{X()};
    

    那么临时X 的生命周期将持续到w 的生命周期,并且w.x 不是悬空引用。但那是因为没有函数调用。


    C++17 引入了第三个上下文,它涉及复制一个数组,这里不相关。

    【讨论】:

    • 不错的答案。关于“这与成员右值引用无关”的注释 - 我假设问题作者正在考虑由std::forward_as_tuple 返回的tuple&lt;TT&amp;&amp;...&gt; 的成员。我想知道,如果您的 Wrapper 对象之一被创建为 Wrapper w{std::forward_as_tuple([](int v){ std::cout &lt;&lt; v &lt;&lt; std::endl; })};,那么像 std::get&lt;0&gt;(w.t)(6); 这样的调用是否安全?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-14
    • 1970-01-01
    • 2012-11-15
    • 1970-01-01
    • 2012-02-10
    • 1970-01-01
    相关资源
    最近更新 更多