【问题标题】:What is the rationale for extending the lifetime of temporaries?延长临时工寿命的理由是什么?
【发布时间】:2013-12-12 15:05:02
【问题描述】:

在 C++ 中,可以通过将临时值绑定到引用来延长其生命周期:

Foo make_foo();

{
    Foo const & r1 = make_foo();
    Foo && r2 = make_foo();

    // ...
}             // both objects are destroyed here

为什么允许这样做?这解决了什么问题?

我在 Design and Evolution 中找不到对此的解释(例如 6.3.2:临时生命周期)。我也找不到任何关于此的先前问题(this one 最接近)。

此功能有些不直观,并且具有微妙的故障模式。例如:

Foo const & id(Foo const & x) { return x; }  // looks like a fine function...

Foo const & r3 = id(make_foo());             // ... but causes a terrible error!

为什么可以如此轻易地、默默地滥用语言的一部分?


更新:这一点可能足够微妙,需要澄清一下:我不反对使用“引用绑定到临时对象”的规则。这一切都很好,并且允许我们在绑定到引用时使用隐式转换。我要问的是为什么临时的 lifetime 会受到影响。说白了,我可以声称现有的“直到完整表达式结束的生命周期”规则已经涵盖了使用临时参数调用函数的常见用例。

【问题讨论】:

  • 我假设如果你有一个返回 std::string 的函数和一个返回 std::string const& 的函数,你不必关心返回的是哪个函数;你可以写std::string const& x = foo()
  • 注意:一个非常有趣的例子:long const& l = std::min<long>(0, 1); 是不安全的,因为存在从intlong const&min 的隐式转换,然后返回此引用。 叹息
  • @MatthieuM.: 是的,准确地说:-)
  • @DavidRodríguez-dribeas 这也符合我们的政策。我们有几个程序员经常这样做;我们只是逐渐摆脱它。 (除了所有其他潜在问题之外,它实际上会导致代码稍微变慢,至少在某些构建中,因为额外的间接性。)
  • @DavidRodríguez-dribeas 条件多态性是延长寿命有意义的一种情况。但是尽我所能,我无法想出一个相关的初始化表达式。可能是Base const& obj = cond ? Base() : Derived(); 之类的东西。但是一旦它变成cond ? Derived1() : Derived2()(几乎总是这样),你就需要一个static_cast<Base&>,并且用static_cast<Base&>的结果初始化一个引用并不会延长生命周期演员的对象。

标签: c++ language-design


【解决方案1】:

简单的答案是您需要能够将临时值与 const 引用绑定,没有该功能将需要大量的代码重复,函数将 const& 用于左值或值参数或按值对于右值参数。 一旦你需要,语言需要定义一些语义,以保证临时的生命周期至少与引用的生命周期一样长。

一旦您接受引用可以在一个上下文中绑定到右值,为了保持一致性,您可能希望扩展规则以允许在其他上下文中进行相同的绑定,并且语义实际上是相同的。临时生命周期会延长,直到引用消失(无论是函数参数还是局部变量)。

替代方法是允许在某些上下文(函数调用)但不是所有上下文(本地引用)中绑定的规则,或者允许两者并在后一种情况下始终创建悬空引用的规则。


从答案中删除了引号,留在这里以便 cmets 仍然有意义:

如果您查看标准中的措辞,就会发现一些关于此预期用途的提示:

12.2/5 [段落中间] [...] 在函数调用(5.2.2)中临时绑定到引用参数会一直存在,直到包含调用的完整表达式完成为止。 [...]

【讨论】:

  • “直到引用消失”...好吧,在函数调用中,这不是一个严格的界限,因为临时的生命比引用长
  • 编辑后:“总是悬挂”对我来说似乎是更好的规则。它使我的r1r2r3 示例的行为方式都相同。易于学习、易于教授并且非常易于静态诊断。
  • @KerrekSB:编辑最初是对您在已删除的另一个问题中添加的一条评论的回答
  • 这似乎根本没有解决问题。临时的生命周期是直到完整表达式的结尾,not 直到它在函数调用中绑定到的引用结束。当绑定到引用并增加临时的生命周期时,初始化函数参数是一次。
  • @JamesKanze:那个特定的引用实际上是相反的,它是临时和引用具有相同生命周期的规则的一个例外[在这种情况下,临时的生命周期是 longer 比参考]。我越来越相信我应该避免使用这句话。
【解决方案2】:

正如Bjarne Stroustrup (the original designer) explained it 在 2005 年的 clc++ 帖子中一样,它是为了统一规则。

引用的规则简直是我最通用和统一的 能找到。在参数和局部引用的情况下, 临时的生命与它所绑定的引用一样长。一 明显的用途是作为 a 中复杂表达式的简写 深度嵌套循环。例如:

for (int i = 0; i<xmax; ++i)
    for (int j = 0; j< ymax; ++j) { 
        double& r = a[i][j]; 
        for (int k = 0; k < zmax; ++k) { 
           // do something with a[i][j] and a[i][j][k] 
        }
    } 

这可以提高可读性以及运行时性能。

事实证明,它对于存储从引用类型派生的类的对象很有用,例如如the original Scopeguard implementation


a clc++ posting in 2008,James Kanze 提供了更多细节:

标准明确说明了何时必须调用析构函数。前 然而,标准是 ARM(和更早的语言规范) 相当宽松:析构函数可以在之后的任何时间调用 临时被“使用”并且在下一个右大括号之前。

(“ARM”是 (IIRC) Bjarne Stroustrup 和 Margareth Ellis 的注释参考手册,在第一个 ISO 标准之前的最后十年中,它是一个事实上的标准。不幸的是,我的副本被埋在一个盒子里,在很多其他盒子下面,在外屋里。所以我无法验证,但我相信这是正确的。)

因此,与许多其他方面一样,延长寿命的细节在标准化过程中得到了磨练和完善。

由于 James 在 cmets 中提出了这一点来回答这个问题:完美无法及时回溯以影响 Bjarne 延长寿命的理由。


类似 Scopeguard 的代码示例,其中临时绑定到引用的是派生类型的完整对象,其派生类型析构函数在最后执行:

struct Base {};

template< class T >
struct Derived: Base {};

template< class T >
auto foo( T ) -> Derived<T> { return Derived<T>(); }

int main()
{
    Base const& guard = foo( 42 );
}

【讨论】:

    【解决方案3】:

    我在 SO 的某处发现了一个有趣的延长寿命的应用程序。 (忘记在哪了,等找到了再补充。)

    生命周期延长允许我们使用固定类型的纯右值。

    例如:

    struct Foo
    {
        Foo(int, bool, char);
        Foo(Foo &&) = delete;
    };
    

    Foo 类型无法复制或移动。然而,我们可以有一个返回 Foo 类型的纯右值的函数:

    Foo make_foo()
    {
        return {10, false, 'x'};
    }
    

    然而我们不能构造一个用返回值make_foo初始化的局部变量,所以一般来说,调用该函数会创建一个立即销毁的临时对象。生命周期扩展允许我们在整个范围内使用临时对象:

    auto && foo = make_foo();
    

    【讨论】:

      猜你喜欢
      • 2011-06-07
      • 1970-01-01
      • 1970-01-01
      • 2016-01-12
      • 2011-11-26
      • 1970-01-01
      • 1970-01-01
      • 2011-08-17
      相关资源
      最近更新 更多