从基析构函数static_cast指向派生类的指针的安全性

Safety of static_cast to pointer-to-derived class from base destructor

本文关键字:派生 指针 安全性 析构函数 static cast      更新时间:2023-10-16

这是问题"使用C++中的Static_cast向下转换"和"使用static_cast(或reinterpret_cast)进行继承而不添加成员的无效向下转换的安全性"的变体

我不清楚标准中的短语"B 实际上是 D 类型的对象的子对象,生成的指针指向 D 类型的封闭对象"关于 ~B 中的行为。如果你在 ~B 中投射到 D,那么它仍然是一个子对象吗?下面的简单示例显示了问题:

void f(B* b);
class B {
public:
  B() {}
  ~B() { f(this); }
};
class D : public B { public: D() {} };
std::set<D*> ds;
void f(B* b) {
  D* d = static_cast<D*>(b);  // UB or subobject of type D?
  ds.erase(d);
}

我知道演员表是一扇通往灾难的大门,从 dtor 做这样的事情都是一个坏主意,但一位同事声称"代码有效且工作正常。这个演员表是完全有效的。该评论明确指出,不应取消提及"。

我指出,强制转换是不必要的,我们应该更喜欢类型系统提供的保护而不是注释。可悲的是,他是高级/首席开发人员之一,也是所谓的 c++ "专家"。

我可以告诉他演员是UB?

[expr.static.cast]/p11:

类型为"指向 cv1 的指针B"的 prvalue,其中 B 是类类型,可以 转换为类型为"指向 CV2 D 的指针"的 prvalue,其中 D 是 类派生自B(条款 10),如果有效的标准转换 从"指向D的指针"到"指向B的指针"存在(4.10),cv2是 与CV1相同或更高的CV资格,以及 B既不是 D 的虚拟基类,也不是 D的虚拟基类 .空指针值 (4.10) 被转换 到目标类型的空指针值。如果 prvalue 的 类型"指向 CV1 B的指针"指向实际上是子对象的B 对于类型为 D 的对象,生成的指针指向 封闭类型 D 的对象。否则,行为是未定义的。

那么,问题是,在static_cast时,指针是否实际上指向"实际上是类型D对象的子对象的子对象的B"。如果是这样,则没有 UB;如果不是,则无论是否取消引用或以其他方式使用生成的指针,行为都是未定义的。

[class.dtor]/p15 说(强调我的)

为对象调用析构函数后,该对象将不再 存在

和 [basic.life]/p1 说

类型 T 对象的生存期在以下情况下结束:

  • 如果 T 是具有非平凡析构函数 (12.4) 的类类型,则析构函数调用将启动,或者
  • [...]

因此,一旦调用D的析构函数,对象的生存期就结束了,当然,当B的析构函数开始执行时 - 也就是在D的析构函数主体完成执行之后。此时,没有"类型D的对象"可以成为该B的子对象 - 它"不再存在"。因此,你有 UB。

如果 UBsan 的 Clang 是多态的(给定一个虚函数),则会报告此代码的错误B该代码支持此读取。

显然,你的同事的印象是,只要你不取消引用无效的指针,你就没问题。

错了

仅评估此类指针具有未定义的行为。这段代码显然被破坏了。

你应该明确地告诉他这是 UB ! !

为什么?

12.4/7:基和成员以与其构造函数完成的相反顺序销毁 对象在 他们构造的相反顺序。

12.6.2/10:首先初始化虚拟基类 (...),然后初始化直接基类

因此,在析构 D 时,首先析构 D 成员,然后析构 D 子对象,然后才析构 B。

此代码确保在销毁 B 对象时调用 f()

 ~B() { f(this); } 

因此,当 D 对象被销毁时,首先销毁 D 子弹,然后执行 ~B(),调用 f()

f()中,您将指向 B 的指针转换为指向 D 的指针。这是 UB:

3.8/5:(...) 在对象的生存期结束后,在对象占用的存储被重用或释放之前,任何 指向对象将所在的存储位置的指针 或位于 可以使用,但只能以有限的方式使用。 (...)如果指针用于访问非静态数据成员或调用 对象,或 (...指针用作static_cast的操作数