【问题标题】:Is it allowed to print the address of a dangling reference?是否允许打印悬空引用的地址?
【发布时间】:2026-01-19 23:30:01
【问题描述】:

考虑这段代码,它是从here 稍微修改的:

#include <iostream>

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

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

lambda f 仅在第一次调用时初始化,在第二次调用期间,捕获的变量不再存在,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] 中的某个地方,虽然我浏览了好几次,但我很难看到什么适用以及它试图告诉我什么。

【问题讨论】:

  • FWIW,在我看来,像您在此处所做的那样,在静态 lambda 中捕获本地变量至少不会产生警告,这是一种疏忽。
  • timsong-cpp.github.io/cppwp/n4868/basic.stc.general#4 对引用会发生什么保持沉默,所以我会说它未指定
  • @500-InternalServerError 是的,我也认为这是一个疏忽。实际上,我试图让优化器产生一些意想不到的输出,但那个警告是我能得到的“最好的”。

标签: c++ reference language-lawyer undefined-behavior


【解决方案1】:

我认为这是不明确的,但可能是实现定义的。

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

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

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

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

lambda-expressioncompound-statement 中的每个 id-expression 都是对副本捕获的实体的 odr 使用被转换为对闭包类型的相应未命名数据成员的访问。

引用捕获并非如此。 lambda 正文中的 id-expression i 指的是 original 实体。正如人们可能合理假设的那样,它不是充当int&amp; 的闭包类型的非静态成员。

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

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

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

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

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

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

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

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

我们没有指针。值得注意的是,引用并没有被“删除”,但我们也没有引用。

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

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

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

&amp;i 的结果是什么?关于指针切换的语言可以理解为结果,即表示已释放存储地址的指针,必须是无效的指针值。

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

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

【讨论】:

  • 关于指针切换的语言可以被解读为结果,它是一个表示已释放存储地址的指针,必须是一个无效的指针值。 你意思是&amp;i首先产生指向对象的指针,表示释放的存储地址,然后这个值立即被一个无效的指针值替换?
  • &i 具有指向 int 的类型指针并“指向指定对象”,因此它是表示该存储区域中地址的指针。 “表示该存储区域任何部分的地址的所有指针的值都成为无效的指针值”......所以你可以读到,因为值需要是 zap 之后的无效指针值,即使指针本身确实如此在 zap 时不存在。想象一下,如果您在 zap 时将指针值存储在 uintptr_t 中。从它重新创建的指针是否应该具有无效的指针值?我不确定。
  • 我读到«达到存储区域的持续时间结束»(与«After»相反),因为«zap» 仅适用于存储释放时存在的指针。但这可能是对单词选择的过多解读。一种可能的解决方法是,当存储被释放时,与存储中的对象关联的所有实体(变量和 NSDM)都不再与它们关联。所以i 将不再是表示对象的左值,在表达式中使用i 将只是UB。
【解决方案2】:

是的,这是 未定义的行为,因为将 &amp; 地址操作符应用于引用会检索被引用对象的地址,在您的示例中不再存在,因为它已经超出范围并被销毁。您不能获取不存在的对象的地址。

【讨论】:

  • 您能否展示使对象“不存在”的措辞? timsong-cpp.github.io/cppwp/n4868/intro.object#1.sentence-2 描述了对象是如何创建的,有文字说它们的生命周期是如何结束的,但是当它结束时它们仍然存在。对象何时停止存在?
  • "它们的生命如何结束,但它们在结束时仍然存在" - 不,它们不存在。一旦对象超出范围并被销毁,它就不再存在。它占用的内存可能还存在,但对象本身已经没有了。
  • 不,他们没有。一旦对象超出范围并被销毁,它就不再存在。我在您的评论中没有看到对标准的引用。 (而且,顺便说一句,对象没有范围。)
  • 在对象被销毁后使用对该对象初始化的引用并不总是被禁止的 (eel.is/c++draft/basic.life#8),所以这个答案是不完整的。