在C++中像这样“重新绑定”引用是合法的吗?

38
以下C++代码是否合法?
据我所知,Reference具有平凡析构函数,因此它应该是合法的。
但是我认为引用不能被合法地重新绑定...可以吗?
template<class T>
struct Reference
{
    T &r;
    Reference(T &r) : r(r) { }
};

int main()
{
    int x = 5, y = 6;
    Reference<int> r(x);
    new (&r) Reference<int>(y);
}

5
不是你的踩票者,但我猜测这是对做出这种事情的恐惧的本能反应。不过,这是一个有趣的问题。 - Fred Larson
3
这些情况绝对应由版主或社区管理员撤销下投票,因为这些下投票是没有正当理由的。 - user529758
@FredLarson:哈哈,可能吧。谢谢! :) - user541686
1
也许我根本不应该提到析构函数的琐碎问题 - 我刚刚意识到在放置 new 之前我完全可以做 r.~Reference<int>(),所以析构函数是否琐碎并不真正影响这个问题... - user541686
@Mehrdad,但它确实带来了一些有趣的讨论,我觉得非常好。 - chris
3个回答

18

你并没有重新绑定一个引用,而是使用了一种就地构造的方式在一个对象的内存上创建了一个新对象。由于旧对象的析构函数从未执行,因此我认为这将产生未定义的行为。


3
能否详细说明析构函数为什么没有被执行?如果这是微不足道的事情,那它不执行真的会导致未定义行为吗? - chris
5
如果在对象的存储空间被重用之前未运行析构函数,这并不会自动导致未定义行为(UB),仅当“任何取决于析构函数产生的副作用的程序都存在未定义行为”(ISO/IEC 14882:2011 3.8 [basic.life] / 4),这种情况下可能会有整个类别的程序省略对析构函数的调用,甚至是非平凡的析构函数,并且仍然没有 UB。 - CB Bailey
是的,我认为你没有理解问题的重点,@CharlesBailey说得很对。事实上,如果我们不依赖于它们的效果,非平凡析构函数可以随意省略,这一点我之前并不知道... - user541686
1
@CharlesBailey,感谢您提供的参考 - 引用的析构函数是否保证没有副作用?我想不出会有这种情况,但我的想象力以前曾经失败过。 - Mark Ransom
2
我认为引用甚至没有析构函数。 - CB Bailey
显示剩余6条评论

11

在您的示例中没有引用被重新绑定。第一个引用(使用名称r.r在第二行构造)在其整个生命周期内绑定到由x表示的int。当放置新表达式在第三行上重用其包含对象的存储时,此引用的生命周期结束。替换对象包含一个引用,该引用在其整个生命周期内绑定到y,并且其生命周期持续到其作用域的结束-即main的结尾。


1
我并不是那么在意术语(“重新绑定”),而更关注实际的代码……所以这段代码是否合法/定义良好? - user541686
代码没有问题,在main函数的结尾处没有未定义行为,因为Reference有一个平凡析构函数。即使没有,也不会有未定义行为,因为您确保在隐式析构函数调用发生的地方——main函数的结尾处——存在一个正确类型的对象来存储r - CB Bailey
1
优化编译器不能得出这样的结论吗?因为在该上下文中已知引用并且无法重新绑定,也没有被销毁,所以它可以直接使用底层源而完全跳过重建过程。 - Mark Ransom
2
@MarkRansom:但“尚未被销毁的部分”是一个错误的假设,因为重用对象的存储空间来创建另一个对象会销毁原始对象。这意味着这样的优化是不符合规范的。 - CB Bailey
“hasn't been destroyed” 指的是析构函数没有被调用。有许多不同的方法可以覆盖对象的存储,我怀疑其中大部分或全部都可能导致未定义行为。 - Mark Ransom
显示剩余5条评论

4
我认为我在“引用”的下面找到了答案,它讨论了微不足道的dtor / dtor副作用,即[basic.life] / 7:
如果在对象的生命周期结束之后,在重用或释放对象占用的存储器之前,在原始对象所占用的存储位置创建一个新对象,则指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用于操作新对象,如果:
- 新对象的存储器正好覆盖原始对象所占用的存储位置,并且 - 新对象与原始对象相同(忽略顶级cv限定符),并且 - 原始对象的类型未经const限定,如果是类类型,则不包含任何类型为const限定或引用类型的非静态数据成员,并且 - 原始对象是类型T的最派生对象,新对象是类型T的最派生对象(即它们不是基类子对象)。
通过重用存储,我们终止了原始对象的生命周期[basic.life] / 1
因此,我认为[basic.life] / 7涵盖了这种情况。
Reference<int> r(x);
new (&r) Reference<int>(y);

我们结束了由r表示的对象的生命周期,并在同一位置创建了一个新对象。

由于Reference<int>是具有引用数据成员的类类型,因此未满足[basic.life]/7的要求。也就是说,r甚至可能不指向新对象,我们不能使用它来“操作”这个新创建的对象(我将“操作”解释为只读访问)。


嗯,虽然我最初阅读的生命周期结束条件是“您调用dtor或重用存储”,但我现在认为它是一个严格的分离:“它要么有一个非平凡的dtor并且您调用它,要么您重用存储”。[basic.life]/7中的示例并不真正有帮助,因为dtor无论如何都是平凡的(@Mehrdad,你在这里吗?) - dyp
是的,第一段似乎在两者之间建立了严格的分离,我最初没有看到它,所以我认为只有在存在非平凡析构函数的情况下,生命周期才会结束(来自另一段)。答案很有道理,可能是正确的...让我再想一想,但我可能会接受它,谢谢!+1 - user541686
@dyp,那么,这是合法的吗? - alfC
@alfC 我有点不太了解,但据我所知,使用名称 r 访问对象是非法的,因为它涉及到引用成员。现在,人们可以使用 std::launder 来解决一些问题。 - dyp

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