【问题标题】:Is converting a reinterpret_cast'd derived class pointer to base class pointer undefined behavior?是否将 reinterpret_cast 派生类指针转换为基类指针未定义行为?
【发布时间】:2019-04-05 08:43:28
【问题描述】:

看看简单的例子:

struct Base { /* some virtual functions here */ };
struct A: Base { /* members, overridden virtual functions */ };
struct B: Base { /* members, overridden virtual functions */ };

void fn() {
    A a;
    Base *base = &a;
    B *b = reinterpret_cast<B *>(base);
    Base *x = b;
    // use x here, call virtual functions on it
}

这个小 sn-p 有未定义的行为吗?

reinterpret_cast 定义明确,它返回不变的值base,只是类型为B *

但我不确定Base *x = b; 行。它使用b,它的类型为B *,但它实际上指向一个A 对象。而且我不确定,x 是否是“正确的”Base 指针,是否可以用它调用虚函数。

【问题讨论】:

  • 我不认为强制转换本身会导致 UB,但是尝试使用 b 调用虚函数或使用 B 只有成员肯定会导致 UB。认为x 是安全的。
  • @Someprogrammerdude:是的,问题是,“尽管认为x 是安全的”是真是假。我有一种感觉,虽然这乍一看似乎是无害的(这是一个无操作),但它是 UB。
  • reinterpret_cast 无法在基类和派生类指针/引用之间安全地转换。 b 不能保证是一个有效的指针。你可以用它做的唯一安全的事情是 reinterpret_cast 它回​​到原来的类型。
  • @n.m.:假设您将b 转换回reinterpret_cast。它应该给你一个正确的Base 指针。现在,reinterpret_cast 只不过是转换为void *,然后转换为Base *。我的示例代码做了类似的事情(它只是没有转换为void *,并且转换为Base * 是隐式的,而不是通过static_cast)。无论如何,我只是在这里扮演魔鬼的拥护者。我知道转换是 UB,但不能用标准备份。
  • 似乎关于隐式派生到基指针转换的行:The result of the conversion is a pointer to the base class subobject of the derived class object. (timsong-cpp.github.io/cppwp/conv.ptr#3),意味着我们确实取消了b 的引用,因此点击了 UB。

标签: c++ language-lawyer


【解决方案1】:

static_cast(或隐式派生到基指针的转换,其作用完全相同)与reinterpret_cast 大不相同。不能保证基本子对象与完整对象的起始地址相同。

大多数实现将第一个基础子对象与完整对象放在同一地址,但当然即使这样的实现也不能将两个不同的非空基础子对象放在同一个地址。 (具有虚函数的对象不为空)。当基子对象和完整对象不在同一个地址时,static_cast 不是空操作,它涉及指针调整。

有些实现甚至从不将第一个基本子对象放在与完整对象相同的地址。例如,允许将基础子对象放置在派生的所有成员之后。 IIRC Sun C++ 编译器曾经以这种方式布局类(不知道它是否还在这样做)。在这样的实现中,这段代码几乎肯定会失败。

具有多个基数的 B 的类似代码在许多实现中都会失败。 Example.

【讨论】:

  • 您刚刚提出了另一个问题:) 虽然您说的是真的,但如果Base *base = &amp;a; 不移动指针(通常是这种情况)怎么办?或者我将if 放入我的代码中,因此它会检查是否相等:if ((void*)base==(void*)&amp;a),并且只有当这是真的时,代码才会执行​​以下操作(reinterpret_cast + impl. conversion)。还会是UB吗?
  • 正如另一个答案指出的那样,b 不是安全派生的指针,x 也不是。它是实现定义的,是否可以取消引用不安全派生的指针。我刚刚澄清了它不被认为是安全派生的原因。
  • 标准中说“指针值是安全派生的指向动态对象的指针”,这很奇怪。我不知道为什么会有动态。但无论如何,检查它下面的list,似乎b 是一个安全派生的指针,因为它是“安全派生指针值的reinterpret_cast 的结果”(我认为我们可以考虑@987654332 @ 作为安全派生的指针)。
  • 确实是我的错误,显然安全派生的意思与我想象的不同。不知道这是否是你提供的附加条件的UB。
  • @geza 这个答案解释了为什么reinterpret_cast 不是在类层次结构中进行转换的标准方式。如果您想问是否可以在特定实现中利用对象布局的属性以便获得比标准允许的更多使用reinterpret_cast,您可能想问一个单独的问题(答案将是响亮的“不")。
【解决方案2】:

如果两个类布局兼容,reinterpret_cast 是有效的(结果可以取消引用);那是

  • 它们都有标准布局,
  • 它们都有相同的非静态数据成员

但是这些类没有标准布局,因为StandardLayoutType 的要求之一是该类没有虚拟函数或虚拟基类。

关于从转换派生的指针的有效性,标准在“安全派生的指针”一节中有这样的说法:

6.7.4.3安全派生指针

4。实现可能放宽了指针安全性,在这种情况下,指针值的有效性不取决于它是否是安全派生的指针值。或者,实现可能具有严格的指针安全性,在这种情况下,引用具有动态存储持续时间的对象的指针值不是安全派生的指针值是无效的指针值,除非引用的完整对象先前已被声明为可访问的。 [ 注意:使用无效指针值(包括将其传递给释放函数)的影响是未定义的,请参见 6.7.4.2。即使不安全派生的指针值可能与某个安全派生的指针值比较,也是如此。 —尾注 ] 实现是否具有宽松的或严格的指针安全是实现定义的。

【讨论】:

  • 但是这里的OP并没有直接访问内存,结果为reinterpret_cast。另一个演员 (static_cast) 发生在访问之前。
  • 但是对于无效的转换结果是否可以安全地使用另一个转换?
  • 案例的结果不是“无效”。该值仍然有效,但不能取消引用(否则 UB)。 (与nullptr相同,是有效值但不能取消引用)
  • 不过,问题仍然存在。可以将“不可引用”再次转换为“可引用”吗?
  • @BiagioFesta 是的,并转换回原始类型 (X*->Y*->X*)
【解决方案3】:

是的,它确实有未定义的行为。 A 和 B 中 Base 的子对象布局未定义。 x 可能不是真正的 Base 对象。

【讨论】:

    【解决方案4】:

    如果AB 是彼此的逐字副本(除了它们的名称)并且在相同的上下文中声明(相同的命名空间,相同的#defines,没有__LINE__ 用法),那么常见的C++ 编译器(gcc, clang) 将产生两个完全可互换的二进制表示。

    如果AB 使用相同的方法签名但对应方法的主体不同,则将A* 强制转换为B*不安全,因为编译器中通过了优化例如,可以在调用站点b-&gt;method() 部分内联void B::method() 的主体,而程序员的假设可能是b-&gt;method() 将调用A::method()。因此,只要程序员使用优化编译器,通过类型B* 访问A 的行为就变成未定义

    问题:所有编译器总是至少在某种程度上“优化”传递给它们的源代码,即使在-O0 也是如此。在 C++ 标准未强制要求的行为(即:未定义的行为)的情况下,编译器的隐式假设 - 当所有优化都关闭时 - 可能与程序员的假设不同。编译器的开发人员已经做出了隐含的假设。

    结论:如果程序员能够避免使用优化编译器,那么通过B* 访问A 是安全的。这样的程序员需要解决的唯一问题是不存在非优化编译器


    当通过reinterpret_castA* 强制转换为B*、访问b-&gt;field 或调用b-&gt;method() 时,托管C++ 实现可能会中止程序。其他一些托管 C++ 实现可能会更加努力地避免程序崩溃,因此当它看到程序通过 B* 访问 A 时,它会求助于临时鸭子类型。

    一些问题是:

    • 程序员能否猜测托管 C++ 实现在 C++ 标准未强制要求的行为情况下会做什么?
    • 如果程序员将代码发送给另一位程序员,后者将把它传递给不同的托管 C++ 实现?
    • 如果 C++ 标准未涵盖某个案例,是否意味着 C++ 实现可以选择做任何它认为合适的事情来处理该案例?

    【讨论】:

    • 我的问题标签为language-lawyer。这意味着编译器做什么并不重要。问题是,标准是怎么说的。
    • @geza 是的,尽管另一方面程序员总是通过特定的 C++ 实现访问 C++ 标准。从language-lawyer 的角度来看,唯一可以接受的正确答案只有一行:“标准不包括这种情况。” - 从language-lawyer 的观点来看,在此之上添加任何内容都是多余的,您不应该接受它作为您问题的最佳答案。
    猜你喜欢
    • 1970-01-01
    • 2013-09-23
    • 1970-01-01
    • 2016-10-10
    • 2014-03-09
    • 2016-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多