【问题标题】:Multiple inheritance issue with templates and interfaces模板和接口的多重继承问题
【发布时间】:2015-02-06 00:10:13
【问题描述】:

我正在开发一个使用 tcl 解释器的项目,我们将 c++ 对象和方法绑定到 tcl 命令。绑定是具有 ClientData 指针参数(几乎是 void*)的 c++ 函数。然后,我们将其转换为我们期望的对象,并与该对象进行交互。

我正在整合一组模板化的类。当我将这些类绑定到 tcl 命令时,我不知道模板参数,所以我使用抽象的“接口”基类与它们交互并将模板参数放在实现类中,所以我不需要模板参数使用它们。

最重要的是,我有一个处理所有常见操作的基类,以及处理所有其他事情的派生类,它们都有接口类。

我创建了一个简单的示例来说明我想要完成的工作。但是我的代码显然更复杂。

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 得到奇怪的行为。 看起来我的 vtables 发生了一些奇怪的事情。这可能与模板参数有关吗? 如果我使用(Derived&lt;short&gt; *) 进行投射,它工作正常。 我想了解我做错了什么,以及如何正确地使用模板进行 MI。

据我所知,这些操作没有任何歧义。 所以我的问题是双重的。
1) MI 是这里的正确答案吗?有很多类需要派生,另一个继承链将使用相同的 Base,所以我想尽可能多地重用代码。但如果有更好的方法,我宁愿避免 MI。 2) 我对 MI 做错了什么?

【问题讨论】:

  • stackoverflow.com/help/mcve,此代码中有太多语法错误,无法从表面上理解(template&lt;T&gt;virtual DerivedAction()=0;Derived&lt;short&gt; a = new Derived&lt;short&gt;(); 等等等等)。
  • 这太冗长了,不是最低限度的工作示例。您应该制作一个最小的工作示例并减少文本以说明您遇到的问题以及您遇到的问题。我不认为你会因此受到惩罚;但我确实认为人们会避免它,因为尝试解析它需要做很多工作。我知道我读完第一段就停止了阅读。
  • 已编辑。抱歉,我复制粘贴了错误的 h 文件。我将它放入一个文件并删除了 tcl 代码。这种行为也发生在我的简单示例中。
  • 你到底为什么要投给BaseInterFace?它不是继承链的一部分,这只是未定义的行为。

标签: c++ templates inheritance multiple-inheritance


【解决方案1】:

底线是,如果您知道 void* 将被转换为 to,那么在转换为 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);

第一个代码错误而第二个代码正确的原因在于,第二个代码在 void* 的两个转换中使用了相同的指针类型。

让我们看一下对象布局,以了解为什么会出现这种情况,以及为什么您的示例最终会调用错误的函数。 (注意:这个讨论在概念上是正确的,但实现并不完全匹配。它足以向您展示如何正确编码。)

首先,让我们看一下 BaseInterface 对象的布局:

如您所知,由于纯虚方法,您实际上无法实例化 BaseInterface 对象,但是当使用 BaseInterface 指针时,这是编译器所期望的布局。有一个编译器生成的成员变量 vtable,它指向一个函数指针数组,每个虚函数一个。对base_interface_instance-&gt;GetNameOfClass() 的调用实际上是对base_interface_instance-&gt;vtable[1]() 的调用,base_interface_instance-&gt;BaseAction() 实际上是对base_interface_instance-&gt;vtable[2]() 的调用。

Base 对象的布局看起来就像 BaseInterface 的布局。当您创建这种类型的对象时,vtable 包含指向每个虚函数的基类实现的指针。对 base_instance-&gt;GetNameOfClass() 的调用看起来就像对 BaseInterface 对象 base_instance-&gt;vtable[1] 所做的那样。

如果 Base 有任何超出 BaseInterface 中定义的附加虚函数,它们将被放置在 vtable 的末尾。这就是为什么使用单继承,您可以在继承链上下进行简单的转换并安全地使用结果指针(只要对象确实是您要转换的类型或从它派生的类型)。它们都使用相同的 vtable 指针来调用虚函数。更多的派生类碰巧知道 vtable 会向下扩展,从而可以访问在继承链中稍后定义的虚函数。

DerivedInterface 布局如下所示:

多重继承有点不同。您的 Derived 类的实例如下所示:

现在应该清楚为什么调用了错误的函数。从 void* 转换为 DerivedInterface* 后,编译器认为它正在使用 DerivedInterface 对象的内存布局,但它实际上指向的是反映 BaseInterface 布局的对象部分。 c-&gt;DerivedAction() 正在调用 c-&gt;vtable[1],但 vtable 指向 BaseInterface 的虚拟方法(Derived 类布局中的 vtable1),而不是 DerivedInterface 的方法(Derived 类布局中的 vtable2)。 vtable[1]Base::GetNameOfClass(),而不是 Derived::DerivedAction()

所以,重复底线,如果您知道 void* 将被转换为,然后在转换为 void* 时将相同的指针类型转换为 from。在多继承方案中,其他任何事情都有可能失败。

【讨论】:

  • 在您的示例中,您向DerivedInterface&lt;short&gt; 添加了一个模板参数,这是有意的吗?我不知道模板参数。你的解释就是我想要的。我知道 vtables 出了点问题,但我想要更多细节,谢谢。另外,如果我让 DerivedInterface 继承自 BaseInterface 会有什么影响?
  • 即钻石继承。
  • 不,那是个错误。模板内容确实不会影响您遇到的问题。
  • 不用担心。我就是这么想的。如果我进行菱形继承(DerivedInterface 现在继承自 BaseInterface),这将如何影响 vtable?
  • 无论有无虚拟继承,钻石继承都可能面临同样的问题。看看phpcompiler.org/articles/virtualinheritance.html。请记住,尽管那篇文章将事物显示为“左”和“右”,但这些对象实际上是按顺序排列的(“左”可能首先布局,“右”在其后。)。
【解决方案2】:

您发布的代码示例似乎不可编译:

DerivedInterface *c = (DerivedInterface *)clientData;
c->DerivedAction(.1);

但是您发布的DerivedInterface 类的定义是:

class DerivedInterface
{
public:
    DerivedInterface(){};
    virtual ~DerivedInterface(){};
    virtual DerivedAction()=0;
};

您的DerivedInterface 类定义的唯一DerivedAction() 方法是不带参数的方法,而您的示例代码传递floatdouble 参数。

这甚至无法编译。如果您想帮助找出您没有从代码中获得预期结果的原因:

  1. 发布一个最低限度的完整示例。将构建和编译的最小代码片段,但会演示您的问题。

  2. 说明您期望的结果,以及使用示例代码获得的结果。

发布随机的 sn-ps 代码,甚至无法编译,但提出假设代码编译和运行的问题,就像这里一样,不太可能得到有用的答案。

【讨论】:

    【解决方案3】:

    您将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&lt;short&gt;。请考虑避免使用 c 风格的强制类型转换,如果您使用 c++ 类型,编译器能够拾取某些类型的无效类型转换。

    【讨论】:

    • 我需要从 void* 投射。我尝试先转换为 BaseInterFace,然后使用动态转换为 DerivedInterface。 BaseInterFace *b2 = (BaseInterFace *)cd; printf("InputClass: %s\n",b2-&gt;GetNameOfClass()); DerivedInterface *c = dynamic_cast&lt;DerivedInterface*&gt;(b2); 这似乎有效。我要测试一下。也感谢您更新我的问题的提示。
    • @jmerkow 不能保证它会起作用,它仍然是未定义的行为,如果BaseInterFace 碰巧有任何成员变量,它将完全中断。
    • 我现在正在使用它。真正的BaseInterface 有多个成员变量。它似乎工作正常。有没有更好的方法来与模板对象交互而不知道你可以指出我的类型?
    • 我打算尝试切换到装饰器模式。我的直觉告诉我,MI 将导致比其价值更多的问题。
    • @jmerkow 我的错,只要BaseInterface 的地址恰好与派生对象相同,它就可以工作,但你不能依赖它,因为它是一个实现细节。无论哪种方式,也许你应该看看你的设计,多继承通常可以用其他构造代替。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多