出于对临时性的引用,该归咎于谁?


15

乍一看,以下代码看起来无害。用户使用该功能bar()与某些库功能进行交互。(自从bar()返回对非临时值或类似值的引用以来,这甚至可能已经工作了很长时间。)但是现在,它只是返回的新实例BB再次具有一个函数a(),该函数返回对可迭代类型的对象的引用A。用户希望查询该对象,这会导致段错误,因为迭代开始之前销毁了B返回的临时对象bar()

我不确定是谁(图书馆或用户)对此负责。所有库提供的类对我来说看起来都很干净,并且肯定没有做任何其他事情(返回对成员的引用,返回堆栈实例等)。用户似乎也没有做错任何事情,他只是在迭代某个对象而没有做任何与该对象寿命有关的事情。

(一个相关的问题可能是:应该建立一个通用规则,即代码不应对循环头中多个链接调用所检索的内容进行“基于范围的迭代”,因为这些调用中的任何一个都可能返回a右值?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
当您找出应该归咎于谁时,下一步将是什么?大吼大叫他/她?
JensG 2014年

7
不,我为什么呢?我实际上更想知道开发此“程序”的思想过程在将来未能避免此问题的地方。
hllnll 2014年

这与右值或基于范围的for循环无关,但是与用户无法正确理解对象的生存期有关。
2014年

站点备注:这是CWG 900,已作为“无缺陷”关闭。会议记录可能包含一些讨论。
dyp 2014年

8
谁应该为此负责?首先是Bjarne Stroustrup和Dennis Ritchie。
梅森惠勒

Answers:


14

我认为基本问题是C ++语言功能(或缺少语言功能)的组合。库代码和客户端代码都是合理的(问题远非显而易见的事实证明)。如果临时生存期可以B适当延长(到循环结束),那将没有问题。

使临时人员的寿命足够长而不再困难了。甚至没有一个临时的“所有创建基于范围的实时范围的临时对象,直到循环结束”都没有副作用。考虑B::a()返回B按值独立于对象的范围的情况。然后B可以立即丢弃该临时文件。即使可以准确地确定需要延长生命周期的情况,因为这些情况对于程序员而言并不明显,但这种影响(析构函数在以后要被调用)会令人惊讶,并且可能是同样微妙的错误源。

仅检测并禁止这种废话,迫使程序员将其显式提升bar()为局部变量,将是更可取的。这在C ++ 11中是不可能的,并且可能永远都不可能,因为它需要注释。Rust会这样做,其中的签名为.a()

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

'x是生命周期变量或区域,它是资源可用期间的符号名称。坦白说,一生难于解释-或我们还没有找到最好的解释-因此我将自己限制在此示例所必需的最低限度,并向倾向于阅读本文档的读者推荐官方文档

借用检查器会注意到,bar().a()只要循环运行,需求的结果就存在。表述为对寿命的限制'x,我们写:'loop <= 'x。还应注意,方法调用的接收者bar()是临时的。这两个指针具有相同的生存期,因此'x <= 'temp是另一个约束。

这两个约束是矛盾的!我们需要'loop <= 'x <= 'temp但是'temp <= 'loop,它可以准确地捕获问题。由于冲突的要求,错误代码被拒绝。请注意,这是一次编译时检查,Rust代码通常与相同的C ++代码产生相同的机器代码,因此您无需为此付出运行时成本。

尽管如此,这是添加到语言中的一项重要功能,并且仅在所有代码都使用该功能时才起作用。API的设计也会受到影响(某些在C ++中过于危险的设计变得实用,而另一些则无法使其与使用寿命完美结合)。las,这意味着追溯地添加到C ++(或实际上是任何一种语言)是不切实际的。总而言之,错误在于成功的语言所具有的惯性,以及1983年的Bjarne缺乏水晶球和远见卓识,无法吸收最近30年研究和C ++经验的教训;-)

当然,这对将来避免该问题完全没有帮助(除非您切换到Rust且不再使用C ++)。可以通过多个链接的方法调用避免较长的表达式(这是相当有限的,甚至不能远程解决所有生命周期问题)。或者人们可以尝试采用无编译援助更有纪律的所有权政策:文件明确是bar由价值和结果回报B::a()绝不能活得比在B其上a()被调用。在将函数更改为按值返回而不是寿命更长的引用时,请注意这是合同更改。仍然容易出错,但是在发生原因时可能会加快识别原因的过程。


14

我们可以使用C ++功能解决此问题吗?

C ++ 11增加了成员函数ref-qualifiers,它允许限制可以调用成员函数的类实例(表达式)的值类别。例如:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

调用begin成员函数时,我们知道很可能还需要调用end成员函数(或类似的东西size,以获取范围的大小)。这需要我们对左值进行运算,因为我们需要对其进行两次寻址。因此,您可以争辩说这些成员函数应该是左值引用限定的。

但是,这可能无法解决根本问题:别名。的beginend成员函数别名对象,或由对象管理的资源。如果我们更换beginend由一个单一的功能range,我们应该提供一个可以在右值被称为:

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

这可能是有效的用例,但是上面的定义range不允许使用。由于在成员函数调用之后我们无法解决临时问题,因此返回一个容器(即拥有范围)可能更合理:

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

将其应用于OP的案例,并进行少量代码审查

struct B {
    A m_a;
    A & a() { return m_a; }
};

此成员函数更改表达式的值类别:B()是prvalue,但是B().a()是lvalue。另一方面,B().m_a是一个右值。因此,让我们从保持一致开始。有两种方法可以做到这一点:

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

如上所述,第二个版本将在OP中解决该问题。

此外,我们可以限制B的成员函数:

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

这不会对OP的代码产生任何影响,因为:基于范围的for循环中位于the 之后的表达式结果将绑定到参考变量。这个变量(作为用于访问其beginend成员函数的表达式)是一个左值。

当然,问题是默认规则是否应为“对rvalues的成员函数进行别名处理应返回拥有所有资源的对象,除非有充分的理由不这样做”。它返回的别名可以合法使用,但是在您遇到它时会很危险:它不能用于延长其“父”临时对象的寿命:

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

在C ++ 2a中,我认为您应该按以下方法解决此(或类似问题)问题:

for( B b = bar(); auto i : b.a() )

代替OP的

for( auto i : bar().a() )

解决方法手动指定的生存期b为for循环的整个块。

引入此初始声明的提案

现场演示


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.