我正在开发一个使用 tcl 解释器的项目,我们将 c++ 对象和方法绑定(bind)到 tcl 命令。绑定(bind)是具有 ClientData 指针参数(几乎是 void*)的 C++ 函数。然后我们将其转换为我们期望的对象,并与该对象交互。
我正在努力合并一组模板化的类。当我将这些类绑定(bind)到 tcl 命令时,我不知道模板参数,因此我使用抽象“接口(interface)”基类与它们交互并将模板参数放入实现类中,因此我不需要模板参数使用它们。
最重要的是,我有一个处理所有常见操作的基类,以及执行其他所有操作的派生类,两者都有接口(interface)类。
我创建了一个简单的示例来说明我想要完成的任务。但我的代码显然更复杂。
StackTest.h
#include <string>
#include <iostream>
#define STRLEN 1000
class BaseInterFace
{
public:
BaseInterFace(){};
virtual ~BaseInterFace(){};
virtual const char *GetNameOfClass() const=0;
virtual void BaseAction()=0;
};
template<typename T>
class Base: public BaseInterFace
{
public:
Base(){};
virtual ~Base(){};
virtual const char *GetNameOfClass() const
{
std::cout << "This is " << this << std::endl;
return "Base";
}
virtual void BaseAction(){
printf("Base Action Done.\n");
}
};
class DerivedInterface
{
public:
DerivedInterface(){};
virtual ~DerivedInterface(){};
virtual void DerivedAction(double value)=0;
};
template<typename T>
class Derived :
public Base<T>,public DerivedInterface
{
public:
char tclName_[STRLEN];
Derived(){};
virtual ~Derived(){};
virtual const char *GetNameOfClass() const
{ std::cout << "This is " << this << std::endl;
return "Derived";
}
virtual void DerivedAction(double value){
printf("Derived Action Done.\n");
}
};
简单示例 这个例子没有意义,因为我知道类型。但它会在我不这样做时进行模拟,这就是我使用它的方式。
Derived <short> *a = new Derived <short>();
void *cd = (void *)a;
printf("Base_BaseActionMtd\n");
BaseInterFace *b = (BaseInterFace *)cd;
printf("InputClass: %s\n",b->GetNameOfClass());
b->BaseAction();
printf("Done!\n");
BaseInterFace *b2 = (BaseInterFace *)cd;
printf("InputClass: %s\n",b2->GetNameOfClass());
DerivedInterface *c = (DerivedInterface *)cd;
c->DerivedAction(.01);
printf("Done!\n");
输出:
Base_BaseActionMtd
This is 0x1089fdc00
InputClass: Derived
Base Action Done.
Done!
This is 0x1089fdc00
InputClass: Derived
This is 0x1089fdc00
Done!
派生操作未发生,且其调用 GetNameOfClass?我不明白。即使我将 printf 放入派生接口(interface)中,它也不会显示。
另外,如果我切换导出中的继承顺序,我会从 BaseAction 中得到奇怪的行为。
看起来 vtable 发生了一些奇怪的事情。这可能与模板参数有关吗?
如果我使用 (Derived<short> *)
进行转换效果很好。
我想了解我做错了什么,以及如何使用模板正确地进行 MI。
据我所知,操作没有任何歧义。
所以我的问题有两个。
1) MI 是正确的答案吗?有很多很多类需要派生,另一个继承链将使用相同的 Base,所以我想重用尽可能多的代码。但如果有更好的方法,我宁愿避免 MI。
2)我的 MI 做错了什么?
最佳答案
底线是,如果您知道 void* 将转换为,那么在转换为 void* 时从转换相同的指针类型。在多重继承场景中,任何其他操作都有可能失败。根据您的情况,替换此:
Derived<short> *d = new Derived<short>();
void *cd = (void*)d;
DerivedInterface *di = (DerivedInterface*)cd;
di->DerivedAction(.01);
这样:
Derived<short> *d = new Derived<short>();
DerivedInterface *di = (DerivedInterface*)d;
void *cd = (void*)di;
DerivedInterface *di2 = (DerivedInterface*)cd;
di2->DerivedAction(.01);
第一个代码错误而第二个代码正确的原因是第二个代码在与 void* 之间的转换中使用相同的指针类型。
让我们看一下对象布局,以了解为什么会出现这种情况,以及为什么您的示例最终会调用错误的函数。 (注意:这个讨论在概念上是正确的,但实现与此不完全匹配。它足够接近,足以向您展示如何正确编码。)
首先,让我们看一下 BaseInterface 对象的布局:
如您所知,由于纯虚方法,您实际上无法实例化 BaseInterface 对象,但在使用 BaseInterface 指针时,这是编译器期望的布局。有一个编译器生成的成员变量 vtable,它指向一组函数指针,每个虚拟函数一个。对 base_interface_instance->GetNameOfClass()
的调用实际上是对 base_interface_instance->vtable[1]()
和 base_interface_instance->BaseAction()
的调用code> 实际上是对 base_interface_instance->vtable[2]()
的调用。
Base 对象的布局看起来就像 BaseInterface 的布局。当您创建这种类型的对象时,vtable 保存指向基类的每个虚函数的实现的指针。对 base_instance->GetNameOfClass()
的调用看起来就像对 BaseInterface 对象 base_instance->vtable[1]
所做的那样。
如果 Base 有任何超出 BaseInterface 中定义的附加虚拟函数,它们就会被放置在 vtable 的末尾。这就是为什么,通过单一继承,您可以在继承链上进行简单的向上和向下转换,并安全地使用生成的指针(只要该对象确实是您要转换到的类型或从中派生的类型)。它们都使用相同的vtable指针来调用虚函数。更多的派生类恰好知道 vtable 进一步向下扩展,从而可以访问稍后在继承链中定义的虚拟函数。
DerivedInterface 布局如下所示:
多重继承有点不同。派生类的实例如下所示:
现在应该清楚为什么调用了错误的函数。从 void* 转换为 DerivedInterface* 后,编译器认为它正在使用 DerivedInterface 对象的内存布局,但它实际上指向反射(reflect) BaseInterface 布局的对象部分。 c->DerivedAction()
正在调用 c->vtable[1]
,但 vtable 指向 BaseInterface 的虚拟方法(Derived 类布局中的 vtable1),而不是 DerivedInterface 的方法(派生类布局中的 vtable2)。 vtable[1]
是 Base::GetNameOfClass()
,而不是 Derived::DerivedAction()
。
所以,重复一下底线,如果您知道 void* 将被转换为什么,那么在转换为 void* 时,从相同的指针类型进行转换。在多重继承场景中,任何其他操作都有可能失败。
关于c++ - 模板和接口(interface)的多重继承问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28356435/