虚函数和强制类型转换为void以及再次转回原类型

8

我目前正在处理一个遗留的C++代码库。在这个代码库中,对象的指针被转换为void指针,然后存储在一个C库中。 考虑以下代码:

class interface {
public:
  virtual void foo() {
    std::cout << "Interface" << std::endl;}
  virtual ~interface(){};
};

class debug_interface: public interface {
public:
  virtual void foo() {
   std::cout << "Debug Interface" << std::endl;}
};

对象interfacedebug_interface在堆上分配,地址存储在一个无类型指针中。在某些时候,这些指针被检索并转换回基类interface,然后调用虚函数。请参见。

int main(int argc, char *argv[]){

    void *handle = reinterpret_cast<void*>(new interface());
    void *debug_handle = reinterpret_cast<void*>(new debug_interface());

   //void *handle = new interface();
   //void *debug_handle = new debug_interface();

   interface *foo1 = reinterpret_cast<interface*>(handle);
   interface *foo2 = reinterpret_cast<interface*>(debug_handle);

   //interface *foo1 = static_cast<interface*>(handle);
   //interface *foo2 = static_cast<interface*>(debug_handle);

   foo1->foo();
   foo2->foo();

   return 0;
}

首先,我不理解为什么要使用reinterpret_cast。据我所知,对象指针可以隐式转换为void*指针。此外,为了使类型转换明确,使用static_cast就足够了,是这样吗?
更重要的问题是:将指针debug_handle强制转换为interface*(而不是debug_interface*)并调用虚函数真的安全吗?根据C++标准(5.2.10),这是未定义行为。
“指向T1”的值v可以显式转换为指向不同对象类型的值“指向cv T2”的值。如果T1和T2都是标准布局类型(3.9),并且T2的对齐要求不比T1严格,当类型为“指向T1”的prvalue v转换为类型“指向T2”时,结果为static_cast(static_cast(v))。将类型为“指向T1”的prvalue转换为类型“指向T2”(其中T1和T2都是对象类型,并且T2的对齐要求不比T1严格),然后再转回其原始类型,将得到原始指针值。任何其他此类指针转换的结果是未指定的。
从handle转换为foo1应该没问题,但我是否可以再次使用static_cast呢?
编辑:我的示例源代码是错误的。debug_interface是interface的派生类。

reinterpret_cast只是更明确地表示您要将其转换为指针的方式。您可以使用C样式的强制类型转换,但可能会根据编译器而产生警告。如果您很明确,就告诉编译器您打算将该值解释为指针,而不是错误地进行解释。 - Robinson
你可能会对这个答案感兴趣:https://dev59.com/N3RB5IYBdhLWcg3wn4UL#573345。它基本上说是安全的。 - davidhigh
谢谢,我已经读了。但是对于我的第二个问题,我仍然不确定。我只是过于小心,还是这真的很危险? - IcePic
1
“安全”这个词并不是非常精确。它能在你的编译器上运行吗?很可能可以。它符合标准吗?绝对不符合。 - n. m.
4个回答

4

免责声明:本文第一部分是在这两个接口之间没有继承关系时编写的。

未定义行为实际上发生在这里:

   foo2->foo();

在这里,您正在使用指向未实现此API的对象的指针上的interface API。事实是,即使interface和debug_interface都将foo()成员实现为它们的第一个方法,也不会改变任何内容:这些类没有继承关系,因此它们不兼容。
您引用的摘录涉及允许转换的情况。在您的情况下,我的理解是您实际上可以将指向debug_interface的指针转换为指向interface的指针:但是,您现在可以对接口指针执行的唯一安全操作是将其转换回debug_interface指针:使用它来访问interface成员是不安全的。
编辑:如果debug_interface公共派生自interface,则这是一个不同的问题。
在这种情况下,从debug_interface*到interface*的转换是完全安全的:编译器甚至可以隐式应用派生到基础的转换。然而,要安全,必须直接进行此转换,通过以下方式之一:
静态转换
dynamic_cast(将引入运行时检查,过度使用向上转换)。
通过两个reinterpret_cast进行操作是未定义的行为:它可能适用于单继承(在某些编译器上),但标准绝对不保证它。通过两个static_cast进行操作也是未定义的行为。标准保证(我强调):
将转换为“指向cv void”的对象指针类型的值转换回原始指针类型,其原始值将具有其原始值。
在您的示例中,您不是将其转换回原始指针,而是将其转换为另一种指针类型:标准不会为您提供任何关于将获得的值的保证。
可能的解决方案
知道:
直接从debug_interface转换到interface是安全的
将对象类型的指针转换为void *,然后再转换回相同对象类型的指针是安全的。
您可以组装它以获得标准保证的解决方案:
// Safe, see point #1.
// The new expression returns a debug_interface* and static_cast applies a derived-to-base conversion.
interface *debug_handle_interim = static_cast<interface*>(new debug_interface());

// Convert a interface* to void* then back to interface*, see #2
void *type_erased = static_cast<void*>(debug_handle_interim);
interface *debug_handle_back = static_cast<interface*>(type_erased);

是的,你说得对。但是在我的示例源代码中有一个错误。debug_interface是interface的派生类。 - IcePic
@IcePic 感谢您的评论,现在这改变了很多事情;) - Ad N
@IcePic 编辑你的问题以准确反映继承关系。 - n. m.
@Ad N:感谢您的编辑。如果将debug_interface转换为void,然后再转换回interface*,会怎么样呢? - IcePic
@IcePic 这是未定义行为(参见“*虽然使用两个reinterpret_cast来执行它...*”)。我会稍微澄清一下。 - Ad N
谢谢您的澄清,我现在明白了。您的解决方法中有一个小错误。您能否删除新的static_cast<void*>(new debug_handle_interim)?我无法编辑您的帖子。 - IcePic

2

您是正确的,它是未定义的。当您将void*转换回指针时,应始终使用原始指针类型。并且您应该使用static_cast而不是dynamic_cast

因此,您的代码可以安全地编写为:

int main(int argc, char *argv[]){

    void *handle = new interface();
    void *debug_handle = static_cast<interface*>(new debug_interface());

    //Beware! This would be wrong:
    //void *debug_handle = new debug_interface();

   interface *foo1 = static_cast<interface*>(handle);
   interface *foo2 = static_cast<interface*>(debug_handle);

   foo1->foo();
   foo2->foo();

   return 0;
}

如果您恰好有一个这样的类:

class weird_interface : something, public interface
{ /* */};

然后写:

weird_interface *a = new weird_interface();
interface *b = static_cast<interface*>(static_cast<void*>(a));
interface *c = a;
static_assert(b == c, "");

你会看到为什么UB很重要。

1
这不符合标准。正确的存储指针的方式应该是:
interface* tmp = new debug_interface;
void* handle = reinterpret_cast<void*>(tmp);

当然你可以在一行中完成它:

void* handle = reinterpret_cast<void*>(
    static_cast<interface*>(new debug_interface));

但这种方式过于笨重且容易出错,不符合我的口味。
标准允许你将指针转换为void*,然后再转换回来,但你必须将其转换回与开始时完全相同的指针类型。指向基类的指针并不能替代。
如果你让debug_interface使用多继承或虚继承,你的代码很可能会崩溃,即使只使用单一继承也无法符合规范。

0

你说得对,这段代码通常会在类的第二个接口处出现问题。


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