C++多重继承下强制类型转换的坑

617 阅读2分钟

先看一个代码示例:

#include <iostream>
#include <string>

class Nlp {
public:
  virtual void DoNlp() {
    std::cout << "DoNlp\n";
  }
  virtual void DoTermNlp() {
    std::cout << "DoTermNlp\n";
  }
};

class BaseNlp {
public:
  virtual void DoNlpTask() = 0;
};

class DerivedNlp: public Nlp, public BaseNlp {
public:
  void DoNlpTask() override {
    std::cout << "DoNlpTask\n";
  }
};

int main() {
  void* ptr = new DerivedNlp();
  BaseNlp* base_ptr = (BaseNlp*)(ptr);
  base_ptr->DoNlpTask();
  return 0;
}

DeriveNlp先继承Nlp,然后继承BaseNlp。我们在main函数中,先用void*存储DerivedNlp的地址,然后强制类型转换到BaseNlp类型。从逻辑上看,父类指针可以兼容子类指针,并且也是可以正常执行的,但是该代码却是如下输出:

DoNlp

这和我们的认知不符合。

但是,如果把main函数改成如下的形式:

int main() {
  DerivedNlp* ptr = new DerivedNlp();
  BaseNlp* base_ptr = (BaseNlp*)(ptr);
  base_ptr->DoNlpTask();
  return 0;
}

会正常输出

DoNlpTask

为了解释这个现象,我们需要理解C++继承的虚表机制,具体可以参看这篇文章:zhuanlan.zhihu.com/p/75172640

总结如下: C++的每个class会把virtual函数放到一个虚表中,继承父class的子类,会把父类的虚表按照继承顺序排列。对于上文提到的正常输出的情况,C++在编译期间知道具体的类型转换情况,因此会正常处理虚表的偏移位置等,所以输出也是正常的。而void*的情况下,编译器只能强制使用BaseNlp的方式解释虚表,这会直接映射到Nlp的虚表上,导致输出差异。

因此,多重继承的情况,坚决避免使用void*强制转换,这会带来不确定性,而且可能会导致coredump。

上面第二种正确的情况也不建议这么使用,正确的方式应该是利用dynamic_cast进行类型安全的强制转换。

但是,作者最近却遇到了必须使用void*的情况,这种情况下,我们能做的是把多继承改成单继承,这么做虽然逻辑理解上不太合理,但是能解决上述的问题。

更改后的代码:

#include <iostream>
#include <string>

class Nlp {
public:
  void DoNlp() {}
  void DoTermNlp() {}
};

class BaseNlp: private Nlp {
public:
  virtual void DoNlpTask() = 0;
};

class DerivedNlp: public BaseNlp {
public:
  void DoNlpTask() override {
    std::cout << "DoNlpTask\n";
  }
};

int main() {
  void* ptr = new DerivedNlp();
  BaseNlp* base_ptr = (BaseNlp*)(ptr);
  base_ptr->DoNlpTask();
  return 0;
}