【问题标题】:Is replacing `this` with a different type allowed?是否允许用不同的类型替换“this”?
【发布时间】:2018-10-18 09:06:05
【问题描述】:

在 cmets 和这个问题的答案中: Virtual function compiler optimization c++ 有人认为循环中的虚函数调用不能被去虚拟化,因为虚函数可能会使用placement new 将this 替换为另一个对象,例如:

void A::foo() { // virtual 
   static_assert(sizeof(A) == sizeof(Derived)); 
   new(this) Derived; 
}

示例来自LLVM blog article about devirtualization

现在我的问题是:标准是否允许这样做?

我可以在 cppreference 上找到有关存储重用的信息:(强调我的)

如果对象是可简单破坏的或程序不依赖于析构函数的副作用,则程序不需要调用对象的析构函数来结束其生命周期。然而,如果一个程序结束了一个重要对象的生命周期,它必须确保在调用析构函数之前就地构造相同类型的新对象(例如通过placement new)隐含的

如果新对象必须具有相同的类型,则它必须具有相同的虚函数。所以不可能有不同的虚函数,因此去虚化是可以接受的。

还是我误解了什么?

【问题讨论】:

  • 无论其合法性如何,我都不担心会在运行时秘密更改对象类型的不正当代码。 C++ 标准包含一个使用placement new 替换现有对象的示例是一个大错误,因为它鼓励人们认为这样做实际上是有意义的。
  • 如果他们是 covariant 似乎很好

标签: c++ language-lawyer


【解决方案1】:

你提供的报价说:

如果一个程序结束了一个重要对象的生命周期,它必须确保在隐式调用析构函数之前就地构造一个相同类型的新对象(例如通过放置 new)

此声明的意图与您正在做的事情有些不同。该语句的意思是说,当您在不破坏其名称的情况下销毁对象时,某些东西仍然引用具有原始类型的存储,您需要在那里构造一个新对象,以便在发生隐式销毁时,有一个有效的对象摧毁。例如,如果您有一个自动(“堆栈”)变量,并且您调用它的析构函数 - 您需要在该变量超出范围时调用析构函数之前在那里构造一个新实例。

整个语句,尤其是它的“相同类型”子句,与您正在讨论的主题无关,即是否允许您构造具有相同存储要求的不同多态类型一个旧的地方。我不知道为什么你不应该被允许这样做。

现在,话虽如此,您链接的问题正在做一些不同的事情:它在循环中使用隐式 this 调用函数,问题是编译器是否可以假设 this 的 vptr 将在那个循环中不会改变。我相信编译器可以(并且clang -fstrict-vtable-pointers 确实)假设这一点,因为this 只有在放置new 之后的类型相同时才有效。

因此,虽然您提供的标准中的引用与此问题无关,但最终结果是优化器似乎可以在假设 *this 的类型的情况下对循环中进行的函数调用进行去虚拟化(或其 vptr)不能改变。存储在地址中的对象类型(及其 vptr)可以更改,但如果更改,旧的 this 将不再有效。

【讨论】:

  • 我知道这一段处理对象的破坏,但它明确指出“同一类型”。稍后,它会读取指针/引用/等。仅当(除其他外)“新对象与原始对象的类型相同(忽略顶级 cv 限定符)”时才有效。对我来说,这意味着我只能更改 CV 限定词吗?但我同意,这里对“同类型”没有明确的定义
  • 您的“新对象与原始对象的类型相同”引用描述了对使用名称、指针和对原始存储的引用的限制。这意味着this 不能用于引用新构造的对象(它的类型错误),但这并不意味着新构造的对象无效。例如,原始对象可能已经在char 的数组中使用放置new 构造了,并且该数组必然仍然存在,因此将reinterpret_cast 放置在new 之后的新构造类型是合法的调用。
【解决方案2】:

您似乎打算使用重新创建之前存在的句柄(指针、引用或原始变量名)来使用新对象。仅当实例类型未更改以及除const 对象和子对象之外的一些其他条件时才允许这样做:

来自[basic.life]

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:

  • 新对象的存储恰好覆盖了原始对象占用的存储位置,

  • 新对象与原始对象的类型相同(忽略顶级cv-qualifiers),并且

    李>
  • 原始对象的类型不是 const 限定的,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且

  • 原始对象是T 类型的最衍生对象,而新对象是T 类型的最衍生对象(也就是说,它们不是基类子对象)。

您对标准的引用仅仅是这个的结果。

您提出的“去虚拟化反例”不符合这些要求,因此在对象被替换后访问对象的所有尝试都会导致未定义的行为。

博客文章甚至在您查看的示例代码之后的下一句中指出了这一点。

【讨论】:

  • 嗯,应该进一步阅读......我停在“这段代码是否合法?C++ 标准说是的。”......
猜你喜欢
  • 1970-01-01
  • 2023-03-19
  • 2016-07-03
  • 1970-01-01
  • 1970-01-01
  • 2018-03-03
  • 2019-03-29
  • 2014-09-18
相关资源
最近更新 更多