首页 > 解决方案 > 是否允许打印悬空引用的地址?

问题描述

考虑这段代码,从这里稍作修改:

#include <iostream>

void foo() {
    int i;
    static auto f = [&i]() { std::cout << &i << "\n";};
    f();
}

int main() {
    foo();
    foo();
}

lambdaf仅在第一次调用时初始化,在第二次调用期间,捕获的变量不再存在,lambda 持有一个悬空引用,但只打印其地址。gcc没有明显问题,输出看起来还可以:

0x7ffc25301ddc
0x7ffc25301ddc

获取悬空引用的地址是未定义的行为,还是可以?

对于一个非常相似的例子 gcc ( -Wall -Werror -pedantic -O3) 会产生一个警告:

#include <iostream>

auto bar() {
    int i;
    return [&i]() {std::cout << &i << "\n"; };
}

int main() {
    bar()();
}

警告:

source>:5:14: error: address of stack memory associated with local variable 'i' returned [-Werror,-Wreturn-stack-address]
    return [&i]() {std::cout << &i << "\n"; };
             ^
<source>:5:14: note: captured by reference here
    return [&i]() {std::cout << &i << "\n"; };

当然,gcc 编译第一个示例并产生预期(?)输出,而第二个示例发出警告这一事实并不意味着什么。在标准中我可以在哪里找到使用悬空引用的地址是否可以?

PS:我想答案在[basic.life]中的某个地方,虽然我浏览了好几次,但我很难看到什么适用以及它试图告诉我什么。

标签: c++referencelanguage-lawyerundefined-behavior

解决方案


我相信这是没有明确规定的,但可能是实现定义的。

问题和其他答案假定这i是一个悬空引用。这假定它完全是一个参考。但这是不对的!

值得注意的是,引用捕获不是引用。该标准明确且有意地表示,引用捕获可能不会导致闭包类型的非静态数据成员。[expr.prim.lambda/12]

对于通过引用捕获的实体,是否在闭包类型中声明了其他未命名的非静态数据成员,这是未指定的。

这就是为什么实体名称的重写只发生在复制捕获上。[expr.prim.lambda/11]

lambda-expression复合语句中的每个id-expression是对由 copy 捕获的实体的 odr 使用,都被转换为对闭包类型的相应未命名数据成员的访问。

引用捕获并非如此。lambda 体内的id-expression指的是 原始实体。正如人们可能合理地假设的那样,它不是闭包类型的非静态成员,它充当.iint&

据我所知,这可以追溯到C++11 之前的 N2927 中的一些改写。在此之前,在标准化期间,引用捕获显然确实导致了闭包类型成员,并且确实触发了正文中的重写,就像复制捕获一样。这种改变是故意的。

所以...... lambda 主体命名了一个i类型的对象,该对象int在第二次调用时不仅在其生命周期之外,而且存储空间也已被释放。

考虑到这一点,让我们尝试推断是否可以。

该标准明确允许在生命周期之外但在存储重用之前使用名称。[basic.life/7]

在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,任何引用原始对象的glvalue都可以使用,但只能以有限的方式使用。对于正在构造或销毁的对象,请参阅 [class.cdtor]。否则,这样的glvalue指的是分配的存储([basic.stc.dynamic.allocation]),并且使用不依赖于其值的glvalue的属性是明确定义的。

这实际上并不适用,因为这里存储已释放。但是,当存储没有释放时,可以推断委员会一般打算不依赖于它的值的命名实体是可以的。在实践中,大多避免左值到右值的转换。

该标准还明确地使存储释放指针无效。[basic.stc.general/4]

当一个存储区域的持续时间结束时,表示该存储区域任何部分的地址的所有指针的值都变为无效指针值。通过无效指针值的间接传递以及将无效指针值传递给释放函数具有未定义的行为。无效指针值的任何其他使用都具有实现定义的行为。

我们没有指针。值得注意的是,引用不是“zapped”,但我们也没有引用。

那么,我们如何把这些放在一起呢?

i单独命名有问题吗?它被明确允许i在其生命周期之后但在存储发布之前命名。i在存储发布后,我找不到任何禁止命名的禁令。它必须引用相同的对象,该对象超出其生命周期。换句话说,规则说i是代表某个对象的左值,并且他们还说在对象生命周期后继续。他们并没有说它在存储释放时停止。

使用但访问i有问题吗?通过获取地址,我们不会触发左值到右值的转换,也不会“访问” i。我找不到禁令。地址运算符 ([expr.unary.op/3]) 表示它将返回指定对象的地址,即左值命名的对象。

结果是&i什么?关于指针切换的语言可以被解读为意味着结果,它是一个表示已释放存储地址的指针,必须是一个无效的指针值。

我们可以打印&i吗?无效指针值的语言很清楚,间接和释放是未定义的,但其他一切都是实现定义的。

所以......它可能是实现定义的。


推荐阅读