模板和接口的多重继承问题
Multiple inheritance issue with templates and interfaces
我正在开发一个使用 tcl 解释器的项目,我们将 c++ 对象和方法绑定到 tcl 命令。绑定是具有 ClientData 指针参数(几乎是 void*)的 c++ 函数。然后我们将其转换为我们期望的对象,并与该对象进行交互。
我正在努力整合一组 class 模板化的元素。当我将这些 classes 绑定到 tcl 命令时,我不知道模板参数,所以我使用抽象 'interface' base classes 与它们交互并将模板参数放在一个实现 class,所以我不需要模板参数来使用它们。
最重要的是,我有一个基础 class 来处理所有常见操作,派生 classes 来做其他所有事情,两者都有接口 classes。
我创建了一个简单示例来说明我要完成的任务。但是我的代码显然更复杂。
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,它也不会显示出来。
此外,如果我在派生中切换继承顺序,我会从 BaseAction 得到奇怪的行为。
看起来 vtable 发生了一些奇怪的事情。这可能与模板参数有关吗?
如果我使用 (Derived<short> *)
投射,它工作正常。
我想了解我做错了什么,以及如何正确地进行 MI,以及如何使用模板。
据我所知,这些操作没有任何歧义。
所以我的问题有两个。
1) MI 是这里的正确答案吗?有很多很多 class 需要派生,还有另一个继承链将使用相同的 Base,所以我想尽可能多地重用代码。但如果有更好的方法,我宁愿避免使用 MI。
2) 我对 MI 做错了什么?
您发布的代码示例似乎不可编译:
DerivedInterface *c = (DerivedInterface *)clientData;
c->DerivedAction(.1);
但是您发布的 DerivedInterface
class 的定义是:
class DerivedInterface
{
public:
DerivedInterface(){};
virtual ~DerivedInterface(){};
virtual DerivedAction()=0;
};
您的 DerivedInterface
class 定义的唯一 DerivedAction
() 方法是不带参数的方法,而您的示例代码传递 float
或 double
参数.
这甚至无法编译。如果你想帮助弄清楚为什么你没有从你的代码中得到预期的结果:
Post 一个最小的完整示例。将构建和编译的最小代码片段,但会演示您的问题。
解释您期望的结果,以及您使用示例代码获得的结果。
Post使用随机代码片段,甚至无法编译,但是提出假设代码编译和运行的问题,就像这里一样,不太可能得到有用的答案。
您正在将 Derived*
转换为 void*
,然后尝试将 void*
转换为 DerivedInterface*
,您无法通过多重继承(或可靠地具有任何继承),DerivedInterface
与 Derived
不在同一地址。您对 void*
唯一可以做的就是将其转换回原始对象。
以下任一方法都有效:
Derived<short>* a = new Derived<short>;
DerivedInterface *c = a;
c->DerivedAction(.01);
Derived<short>* a = new Derived<short>;
BaseInterFace *cd = a;
DerivedInterface *c = dynamic_cast<DerivedInterface*>(cd);
c->DerivedAction(.01);
我必须使用 void*
,您必须先将其转换回 Derived<short>
。请考虑避免使用 C 风格的强制转换,如果您使用 C++ 类型,编译器能够识别出某些类型的无效强制转换。
底线是,如果您知道 void* 将被转换为 到 ,那么在转换时将相同的指针类型转换为 from作废*。在多重继承场景中,其他任何东西都有可能失败。在您的情况下,替换为:
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);
第一个代码错误而第二个代码正确的原因是第二个代码在两个转换中使用相同的指针类型 to/from void*.
让我们看一下对象布局,了解为什么会这样,以及为什么您的示例最终调用了错误的函数。 (注意:此讨论在概念上是正确的,但实现与此不完全匹配。它足以向您展示如何正确编码。)
首先,让我们看一下 BaseInterface 对象的布局:
如您所知,由于纯虚方法,您无法实际实例化 BaseInterface 对象,但在使用 BaseInterface 指针时,这是编译器期望的布局。有一个编译器生成的成员变量 vtable,它指向一组函数指针,每个虚函数一个。对base_interface_instance->GetNameOfClass()
的调用实际上是对base_interface_instance->vtable[1]()
的调用,而base_interface_instance->BaseAction()
实际上是对base_interface_instance->vtable[2]()
.
的调用
Base 对象的布局看起来就像 BaseInterface 的布局。当您创建这种类型的对象时,vtable 会保存指向每个虚函数的 Base class 实现的指针。对 base_instance->GetNameOfClass()
的调用看起来就像对 BaseInterface 对象所做的一样,base_instance->vtable[1]
.
如果 Base 在 BaseInterface 中定义的之外还有任何额外的虚函数,它们就会被放置在 vtable 的末尾。这就是为什么通过单继承,您可以在继承链上进行简单的向上和向下转换并安全地使用结果指针(只要该对象确实属于您要转换为或从中派生的类型)。它们都使用相同的虚表指针来调用虚函数。更派生的 classes 恰好知道 vtable 进一步向下扩展,从而可以访问在继承链中稍后定义的虚函数。
DerivedInterface 布局如下所示:
多重继承有点不同。 Derived class 的实例如下所示:
现在应该清楚为什么调用了错误的函数。从 void* 转换为 DerivedInterface* 后,编译器认为它正在处理 DerivedInterface 对象的内存布局,但它实际上指向反映 BaseInterface 布局的对象部分。 c->DerivedAction()
正在调用 c->vtable[1]
,但 vtable 指向 BaseInterface 的虚拟方法(Derived class 布局中的 vtable1),而不是 DerivedInterface 的方法(Derived class 布局中的 vtable2)。 vtable[1]
是 Base::GetNameOfClass()
,而不是 Derived::DerivedAction()
。
所以,重复底线,如果你知道 void* 将被转换为 到 ,然后转换 from转换为 void* 时的指针类型。在多重继承场景中,其他任何东西都有可能失败。
我正在开发一个使用 tcl 解释器的项目,我们将 c++ 对象和方法绑定到 tcl 命令。绑定是具有 ClientData 指针参数(几乎是 void*)的 c++ 函数。然后我们将其转换为我们期望的对象,并与该对象进行交互。
我正在努力整合一组 class 模板化的元素。当我将这些 classes 绑定到 tcl 命令时,我不知道模板参数,所以我使用抽象 'interface' base classes 与它们交互并将模板参数放在一个实现 class,所以我不需要模板参数来使用它们。
最重要的是,我有一个基础 class 来处理所有常见操作,派生 classes 来做其他所有事情,两者都有接口 classes。
我创建了一个简单示例来说明我要完成的任务。但是我的代码显然更复杂。
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,它也不会显示出来。
此外,如果我在派生中切换继承顺序,我会从 BaseAction 得到奇怪的行为。
看起来 vtable 发生了一些奇怪的事情。这可能与模板参数有关吗?
如果我使用 (Derived<short> *)
投射,它工作正常。
我想了解我做错了什么,以及如何正确地进行 MI,以及如何使用模板。
据我所知,这些操作没有任何歧义。
所以我的问题有两个。
1) MI 是这里的正确答案吗?有很多很多 class 需要派生,还有另一个继承链将使用相同的 Base,所以我想尽可能多地重用代码。但如果有更好的方法,我宁愿避免使用 MI。
2) 我对 MI 做错了什么?
您发布的代码示例似乎不可编译:
DerivedInterface *c = (DerivedInterface *)clientData;
c->DerivedAction(.1);
但是您发布的 DerivedInterface
class 的定义是:
class DerivedInterface
{
public:
DerivedInterface(){};
virtual ~DerivedInterface(){};
virtual DerivedAction()=0;
};
您的 DerivedInterface
class 定义的唯一 DerivedAction
() 方法是不带参数的方法,而您的示例代码传递 float
或 double
参数.
这甚至无法编译。如果你想帮助弄清楚为什么你没有从你的代码中得到预期的结果:
Post 一个最小的完整示例。将构建和编译的最小代码片段,但会演示您的问题。
解释您期望的结果,以及您使用示例代码获得的结果。
Post使用随机代码片段,甚至无法编译,但是提出假设代码编译和运行的问题,就像这里一样,不太可能得到有用的答案。
您正在将 Derived*
转换为 void*
,然后尝试将 void*
转换为 DerivedInterface*
,您无法通过多重继承(或可靠地具有任何继承),DerivedInterface
与 Derived
不在同一地址。您对 void*
唯一可以做的就是将其转换回原始对象。
以下任一方法都有效:
Derived<short>* a = new Derived<short>;
DerivedInterface *c = a;
c->DerivedAction(.01);
Derived<short>* a = new Derived<short>;
BaseInterFace *cd = a;
DerivedInterface *c = dynamic_cast<DerivedInterface*>(cd);
c->DerivedAction(.01);
我必须使用 void*
,您必须先将其转换回 Derived<short>
。请考虑避免使用 C 风格的强制转换,如果您使用 C++ 类型,编译器能够识别出某些类型的无效强制转换。
底线是,如果您知道 void* 将被转换为 到 ,那么在转换时将相同的指针类型转换为 from作废*。在多重继承场景中,其他任何东西都有可能失败。在您的情况下,替换为:
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);
第一个代码错误而第二个代码正确的原因是第二个代码在两个转换中使用相同的指针类型 to/from void*.
让我们看一下对象布局,了解为什么会这样,以及为什么您的示例最终调用了错误的函数。 (注意:此讨论在概念上是正确的,但实现与此不完全匹配。它足以向您展示如何正确编码。)
首先,让我们看一下 BaseInterface 对象的布局:
如您所知,由于纯虚方法,您无法实际实例化 BaseInterface 对象,但在使用 BaseInterface 指针时,这是编译器期望的布局。有一个编译器生成的成员变量 vtable,它指向一组函数指针,每个虚函数一个。对base_interface_instance->GetNameOfClass()
的调用实际上是对base_interface_instance->vtable[1]()
的调用,而base_interface_instance->BaseAction()
实际上是对base_interface_instance->vtable[2]()
.
Base 对象的布局看起来就像 BaseInterface 的布局。当您创建这种类型的对象时,vtable 会保存指向每个虚函数的 Base class 实现的指针。对 base_instance->GetNameOfClass()
的调用看起来就像对 BaseInterface 对象所做的一样,base_instance->vtable[1]
.
如果 Base 在 BaseInterface 中定义的之外还有任何额外的虚函数,它们就会被放置在 vtable 的末尾。这就是为什么通过单继承,您可以在继承链上进行简单的向上和向下转换并安全地使用结果指针(只要该对象确实属于您要转换为或从中派生的类型)。它们都使用相同的虚表指针来调用虚函数。更派生的 classes 恰好知道 vtable 进一步向下扩展,从而可以访问在继承链中稍后定义的虚函数。
DerivedInterface 布局如下所示:
多重继承有点不同。 Derived class 的实例如下所示:
现在应该清楚为什么调用了错误的函数。从 void* 转换为 DerivedInterface* 后,编译器认为它正在处理 DerivedInterface 对象的内存布局,但它实际上指向反映 BaseInterface 布局的对象部分。 c->DerivedAction()
正在调用 c->vtable[1]
,但 vtable 指向 BaseInterface 的虚拟方法(Derived class 布局中的 vtable1),而不是 DerivedInterface 的方法(Derived class 布局中的 vtable2)。 vtable[1]
是 Base::GetNameOfClass()
,而不是 Derived::DerivedAction()
。
所以,重复底线,如果你知道 void* 将被转换为 到 ,然后转换 from转换为 void* 时的指针类型。在多重继承场景中,其他任何东西都有可能失败。