返回本地对象的const引用时会发生什么?

9
struct A {
    A(int) : i(new int(783)) {
        std::cout << "a ctor" << std::endl;
    }

    A(const A& other) : i(new int(*(other.i))) {
        std::cout << "a copy ctor" << std::endl;
    }

    ~A() {
        std::cout << "a dtor" << std::endl;
        delete i;
    }

    void get() {
        std::cout << *i << std::endl;
    }

private:
    int* i;
};

const A& foo() {
    return A(32);
}

const A& foo_2() {
    return 6;
}

int main()
{
    A a = foo();
    a.get();
}

我知道,返回对本地值的引用是不好的。但是,另一方面,const引用应该扩展临时对象的生命周期。
这段代码会产生未定义的行为输出。所以没有生命周期延长。
为什么?我的意思是有人能够逐步解释发生了什么吗?
我的推理链哪里错了?
foo():
1. A(32) - 构造函数 2. return A(32) - 创建一个对本地对象的const引用并返回 3. A a = foo(); - a被foo()返回的值初始化,返回的值超出了作用域(超出了表达式),被销毁,但a已经初始化;
(实际上,在复制构造函数之前调用了析构函数)
foo_2():
1. return 6 - 隐式创建类型为A的临时对象,创建对该对象的const引用(扩展其生命周期)并返回 2. A a = foo(); - a被foo()返回的值初始化,返回的值超出了作用域(超出了表达式),被销毁,但a已经初始化;
(实际上,在复制构造函数之前调用了析构函数)

1
"const引用应该延长临时对象的生命周期" <- 嗯,当涉及到对象生命周期时,非const和const引用是相等的,都不会延长它。 - Giel
2
我认为这就是亚历山大所说的延长生命周期的内容:http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/ - vmpstr
3
@Giel:不对。常量引用可以延长临时对象的生命周期。在处理临时对象时,常量引用和非常量引用是非常不同的。在这种情况下,它的工作方式与OP所期望的不同。 - AnT stands with Russia
@AndreyT:没错,vmpstr提供的链接解释得很好。但在OP的情况下,引用是返回的临时对象,而不是被引用的对象。因此,如预期的那样,它不会延长临时对象的生命周期超出生成它的范围。我相信后者在所有情况下仍然是正确的:本地对象(临时或作为局部变量)不会在其范围之外存活。 - Giel
不,它们并没有超出其范围,但通常它们的尸体仍然存在并且“似乎”在运行和操作,这可能会让人感到困惑,因为在某些情况下返回引用“似乎正常工作”,而在其他情况下则不是。虽然这需要额外的工作,但为了防止这种混淆(以及使用过时指针的混淆),我喜欢在析构函数中将成员指针设置为NULL,并在某些情况下将成员变量设置为安全的无效状态,这样如果有人尝试使用已删除的对象,就会立即显现出来。 - phonetagger
2个回答

12

针对每个具体的上下文,临时生存期延长的规则在语言规范中有明确说明。规范指出:

12.2 临时对象

5 第二种情况是当引用绑定到临时对象时。[...] 在函数返回语句(6.6.3)中绑定到返回值的临时对象将持续到函数退出。 [...]

您的临时对象在函数退出时被销毁。这发生在接收方对象初始化之前。

您似乎认为您的临时对象应该比那更长寿。显然,您试图应用规则,即临时对象应该存活到完整表达式的结尾。但是,该规则不适用于在函数内创建的临时对象。这些临时对象的生命周期受其自己的专用规则控制。

如果有人试图使用返回的引用,则您的 foofoo_2 都会产生未定义行为。


但是,如果“函数返回语句(6.6.3)中返回值的临时绑定在函数退出之前持续存在”,那么在初始化A foo_3()之前,临时值不应该被销毁吗? - Alexander
1
@Alexander - 不,A foo_3() 返回一个值的副本。复制的值在函数结束时不会被销毁。当你返回一个引用时,引用仍然存在 - 它只是不再引用任何东西了。 - Bo Persson
我明白了。但是在表达式A a = foo_3()中,复制构造函数应该被调用两次:第一次是从临时局部变量复制返回值,第二次是初始化A a。但它只被调用了一次。这是优化吗? - Alexander
1
@Alexander:当你执行A foo_3(){return A(54);}时,你有两个不同的概念上的临时对象。第一个是你明确创建的——A(54)。第二个是一个特殊的内部“过渡”临时对象,在函数退出后保存结果。A(54)临时对象被复制到“过渡”临时对象中,而A(54)则被销毁。这个“过渡”临时对象存在更长时间,以用作接收对象的初始化器。 - AnT stands with Russia
对于 A a = foo();,如果返回的对象被销毁了,那么会分配给a什么?它指向哪里? - Reeonce Zeng

5
你误解了“函数退出前”的含义。如果你真的想要使用const引用来延长一个对象在foo函数之外的生命周期,可以使用以下方式:
A foo() {
    return A(32);
}
int main() {
    const A& a = foo();
}

如果您希望以您期望的方式扩展事物,则必须通过值从foo返回,然后使用const引用引用返回值。
正如@AndreyT所说,对象在具有const&的函数中被销毁。您希望您的对象在foo之外存活,因此您不应该在foofoo的返回类型中使用const&(或&)。第一次提到const&应该在main中,因为那是应该使对象保持活动状态的函数。
您可能认为这种按值返回的代码很慢,因为似乎会复制A,但这是不正确的。在大多数情况下,编译器只能构造A一次,在其最终位置(即调用函数的堆栈上),然后设置相关引用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接