【问题标题】:Safety of static_cast to pointer-to-derived class from base destructor从基析构函数到派生类指针的 static_cast 的安全性
【发布时间】:2015-05-09 19:36:57
【问题描述】:

这是问题Downcasting using the Static_cast in C++Safety of invalid downcast using static_cast (or reinterpret_cast) for inheritance without added members 的变体

关于 ~B 中的行为,我不清楚标准中的短语“B 实际上是 D 类型对象的子对象,结果指针指向 D 类型的封闭对象”。如果你在 ~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 做这样的事情是个坏主意,但一位同事声称“代码有效且工作正常。演员阵容完全有效。评论明确指出不应取消引用”。

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

我可以告诉他演员是 UB 吗?

【问题讨论】:

  • 认为这是 UB,但我不确定。然而,代码 [至少在这个示例中] 绝对比我的袜子在炎热的夏天两周没有洗涤后更糟糕......这里的正确做法是在 D 中有一个析构函数,从 @ 删除对象987654325@ - 不应该在B 中完成。这当然也可以避免 UB 的任何问题。它可能确实有效并且定义明确的事实是无关紧要的。或者将ds 变成std::set&lt;B*&gt; bs...
  • 只要不存在从 B 和 D 派生的任何其他类没有额外的成员,这可能工作正常。但是整个东西都闻起来了。您如何确保每个 B 都是 D,如果可以确定,为什么您首先会有不同的课程?
  • @MatsPetersson:你为什么认为这是 UB? 5.2.9 的标准给出了一个几乎等同于 OP 的示例,尽管它使用了参考:struct B { }; struct D : public B { }; D d; B &amp;br = d; static_cast&lt;D&amp;&gt;(br); 当然,这只是一个理论问题。 OP 问题中的代码太可怕了。
  • @ChristianHackl 关键是 OP 中代码的转换发生在销毁期间,并且确实在D 的析构函数体执行完毕之后。
  • @T.C.:啊,我明白了。你是对的,这让一切变得更加复杂。 OP 那里有很好的高级开发人员... :)

标签: c++ casting language-lawyer static-cast


【解决方案1】:

[expr.static.cast]/p11:

“指向 cv1 B 的指针”类型的纯右值,其中 B 是类类型,可以 转换为“指向 cv2 D 的指针”类型的纯右值,其中 D 是 如果是有效的标准转换,则从 B 派生类(第 10 条) 从“指向D的指针”到“指向B的指针”存在(4.10),cv2是 与 cv1 相同的 cv 限定或更高的 cv 限定,并且 B 既不是 D 的虚拟基类,也不是 a 的基类 D 的虚拟基类。空指针值(4.10)被转换 到目标类型的空指针值。如果prvalue 键入“指向 cv1 B 的指针”指向实际上是子对象的 B D 类型的对象,结果指针指向 D 类型的封闭对象。否则,行为未定义。

那么,问题是,在static_cast 的时候,指针是否真的指向“一个B,它实际上是D 类型的对象的子对象”。如果是,则没有UB;如果不是,则行为未定义无论结果指针是否被取消引用或以其他方式使用

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

一旦为对象调用析构函数,对象就不再 存在

[basic.life]/p1 这么说

T 类型对象的生命周期在以下时间结束:

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

因此,D 对象的生命周期在它的析构函数被调用时就结束了,当然在B 的析构函数开始执行时——也就是在D 的析构函数主体执行完之后执行完毕。此时,没有“D 类型的对象”可以作为该B 的子对象——它“不再存在”。这样,你就有了 UB。

如果 B 被设为多态(给定一个虚函数),那么与 UBsan 的 Clang 将在此代码上 report an error,这支持这种读取。

【讨论】:

  • "如果存在从“指向 D”到“指向 B”的有效标准转换” - 请参考 12.7/3:它解释了只要“破坏这些类不应有已完成”,否则为 UB。由于D的破坏在这里完成并且B的一个已经开始,它绝对是UB!
  • @Christophe “指向 D 的指针”和“指向 B 的指针”是类型,这句话处理的是类型,而不是值。只要 B 是 D 的可访问且明确的基数,“就存在从“指向 D”的指针到“指向 B”的有效标准转换(参见 [conv.ptr]/p3)。
  • 我自己的理解:如果析构函数启动后对象不再存在,为什么允许我调用其中的任何清理函数?
  • @KitFisto 标准([class.cdtor]/p4)中有一段明确允许在构造或销毁期间调用成员函数。
  • 谢谢,T.C.我也是这么想的。我希望找到与此上下文相关的“子对象”的清晰且令人信服的定义,因为正如您所指出的那样,这似乎是关键术语。
【解决方案2】:

显然,您的同事认为只要您不取消引用无效指针,就可以了。

错了

仅仅评估这样的指针具有未定义的行为。这段代码明显坏了。

【讨论】:

  • 它是实现定义的,而不是未定义的,但是实现定义的行为附有一个脚注,上面写着“某些实现可能定义复制无效指针值会导致系统生成的运行时错误。”但是,直到释放函数运行之后,指针才会变为无效。指向未初始化存储的指针仍然是有效的指针,并且存储在释放函数运行之前保持可用。我认为 T.C. 的回答给出了一个更有说服力的论点,即代码仍然具有 UB。
【解决方案3】:

你应该明确告诉他这是 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 的操作数

【讨论】:

  • "静态转换指针不会触及对象本身。因此,仅此语句不是 UB,因为不会立即发生任何未定义的事情。"不,static_cast 本身可以是 UB。
  • 确实,static_cast 不是真正属于那种类型的东西是 UB。 reinterpret_cast 可以,只要它没有被取消引用。
  • @T.C.哎呀!你完全正确!我认为只有对 suc a cast poitner 的取消引用是 UB,但是根据您的评论,我发现了一个标准的引用,该引用明确地解决了对象生命周期结束后的 static_casting。我编辑了帖子:毫无疑问:那是 UB !感谢您的提示!
  • 我对 3.8/5 的引用表示怀疑,因为该段落明确排除了“正在构建或销毁的对象”。
  • @T.C.哎呀 ;) 好吧,没有为此定义语义,所以它是 UB。
猜你喜欢
  • 1970-01-01
  • 2021-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-08
  • 1970-01-01
相关资源
最近更新 更多