【问题标题】:Using C++, how do I correctly inherit from the same base class twice?使用 C++,我如何正确地从同一个基类继承两次?
【发布时间】:2010-01-01 23:34:13
【问题描述】:

这是我们理想的继承层次结构:

class Foobar;

class FoobarClient : Foobar;

class FoobarServer : Foobar;

class WindowsFoobar : Foobar;

class UnixFoobar : Foobar;

class WindowsFoobarClient : WindowsFoobar, FoobarClient;

class WindowsFoobarServer : WindowsFoobar, FoobarServer;

class UnixFoobarClient : UnixFoobar, FoobarClient;

class UnixFoobarServer : UnixFoobar, FoobarServer;

这是因为我们的继承层次结构会尝试从Foobar 继承两次,因此编译器会抱怨Foobar 的任何成员的引用不明确。

请允许我解释一下为什么我想要这样一个复杂的模型。这是因为我们希望从WindowsFoobarUnixFoobarFoobarClientFoobarServer 访问相同的变量。这不是问题,只是我想使用以上任意组合的多重继承,这样我就可以在任何平台上使用服务器/客户端功能,也可以在客户端或服务器上使用平台功能。

我不禁觉得这是多继承的一个常见问题......我是从完全错误的角度解决这个问题吗?

更新 1:

另外,考虑到我们可以使用#ifdef 来解决这个问题,但是,这往往会产生非常丑陋的代码,如下所示:

CFoobar::CFoobar()
#if SYSAPI_WIN32
: m_someData(1234)
#endif
{
}

...糟糕!

更新 2:

对于那些想进一步了解本期背景的人,我真的建议浏览一下适当的mailing list thread。事情开始在第 3 篇文章中变得有趣起来。还有一个related code commit,你可以在这里看到真实的代码。

【问题讨论】:

  • 我强烈建议您认真考虑多重继承是否是正确的计划。每次我用过多重继承,我都后悔了。我想说你应该仔细研究组合来解决这个问题,或者看看让“最派生”类成为唯一从 Foobar 继承的类。当您尝试实际调试代码时,您生成的继承树将很难理解并且真的很难遵循操作。
  • 这就是我害怕迈克尔的原因,但这是我能想到的唯一解决方案。您(或其他任何人)能否想出一个不那么做作且符合我简单要求的相同目标的模型。我正在尝试创建一个可以最大限度地减少代码重复的模型。
  • 查看我的后续问题:stackoverflow.com/questions/1990135/…

标签: c++ multiple-inheritance


【解决方案1】:

工作,虽然你会得到基本Foobar 类的两个副本。要获得单个副本,您需要使用虚拟继承。阅读多重继承here

class Foobar;

class FoobarClient : virtual public Foobar;

class FoobarServer : virtual public Foobar;

class WindowsFoobar : virtual public Foobar;

class UnixFoobar : virtual public Foobar;

但是,与多重继承相关的问题很多。如果你真的想展示模型,为什么不让FoobarClientFoobarServer 在构造时引用Foobar,然后让Foobar& FoobarClient/Server::getFoobar 呢?

组合通常是摆脱多重继承的一种方式。现在举个例子:

class WindowsFoobarClient : public WindowsFoobar 
{
    FoobarClient client;
public:
    WindowsFoobarClient() : client( this ) {}
    FoobarClient& getClient() { return client }
}

但是 care must be taken 在构造函数中使用 this。

【讨论】:

  • 感谢您扩展您的答案以提供替代方案;我没有考虑过这个。
  • Kornel,我最终这样做了——尽管我是反过来的;我将WindowsFoobar 重命名为WindowsFoobarUtil 并为Foobar 创建了一个m_util 成员。 WindowsFoobarUtil 实例在每个平台客户端/服务器类的构造函数中创建(例如WindowsFoobarClient)。
【解决方案2】:

你所追求的是C++的虚拟继承特性。你在这里的目的是一场维护噩梦。这可能并不令人惊讶,因为像 H. Sutter 这样的知名作者已经反对这种继承的使用已经有一段时间了。但这来自对此类代码的直接经验。避免深度继承链。非常害怕protected 关键字——它的用途非常有限。这种设计很快就失控了——从低级类的继承链上的某个地方追踪对受保护变量的访问模式变得很困难,代码部分的职责变得模糊等等,而且每年查看你代码的人从现在开始会恨你:)

【讨论】:

  • +1 "一年后看你代码的人会恨你"
【解决方案3】:

你在 C++ 中,你应该对模板友好。使用 template-argument-is-a-base-class 模式,您将不需要任何多重继承或冗余实现。它看起来像这样:

class Foobar {};

template <typename Base> class UnixFoobarAspect : public Base {};
template <typename Base> class WindowsFoobarAspect : public Base {};
template <typename Base> class FoobarClientAspect : public Base {};
template <typename Base> class FoobarServerAspect : public Base {};

typedef UnixFoobarAspect<FoobarClientAspect<Foobar>/*this whitespace not needed in C++0x*/> UnixFoobarClient;
typedef WindowsFoobarAspect<FoobarClientAspect<Foobar> > WindowsFoobarClient;
typedef UnixFoobarAspect<FoobarServerAspect<Foobar> > UnixFoobarServer;
typedef WindowsFoobarAspect<FoobarServerAspect<Foobar> > WindowsFoobarServer;

您还可以考虑使用奇怪重复的模板模式,而不是声明抽象函数,以避免在基类需要调用以特定变体之一实现的函数时调用虚函数。

【讨论】:

  • 嗯,你不觉得这样一个小例子有点过火了吗?还考虑到它会强制内联所有这些类?
  • 为什么你认为这是一个小例子? OP 没有为任何类提供主体,在层次结构的每个级别上都可能并且可能已经实现了数十个成员函数。它可以简化,层次结构的底层可以直接继承而不使用模板。是的,所有类都将在实例化期间内联,与​​虚拟调用相比,这通常是一个巨大的性能提升。您仍然可以使用显式实例化,因此编译器只需处理一次函数体,而不是使用它们的每个编译单元。
  • 这个例子是进行“基于策略的设计”的方法之一(其他人不使用继承),查找有关 PBD 的信息可能会为您提供有用的见解和示例来解决这个问题有点问题。
  • 您已经让 WindowsAspect 从 ServerAspect 继承,这似乎是任意的。为什么打破一个方向而不是另一个方向的对称性?为什么在设计中引入任意关系比使用多重继承更好?
【解决方案4】:

使用虚继承,在FoobarClientFoobarServerWindowsFoobarUnixFoobar的声明中,在Foobar基类名前加上virtual字样。

这将确保始终有一个 Foobar 实例,无论它在您的基类层次结构中出现多少次。

【讨论】:

    【解决方案5】:

    看看这个search。钻石继承是一个有争议的问题,适当的解决方案取决于个人情况。

    我想评论一下 Unix/Windows 方面的事情。通常会#ifndef 一些不适合特定平台的东西。因此,您最终只会使用预处理器指令为 Windows 或 Unix 编译 Foobar,而不是 UnixFoobarWindowsFoobar。在探索虚拟继承之前,看看您可以使用该范式多远。

    【讨论】:

    • 我们以前使用过#ifdefs,但由于代码量大,这在过去 10 年中开始变得非常混乱。我将其用作#ifdef 范式的实验替代品……但我非常担心这会使代码更加复杂,从而违背我认为的目的。
    • 对,听起来有点痛苦。我对交叉编译不是很熟悉,看看其他人使用什么模式来解决这个问题可能会有所收获。
    【解决方案6】:

    试试这个组合和继承的例子:

    class Client_Base;
    class Server_Base;
    
    class Foobar
    {
      Client_Base * p_client;
      Server_Base * p_server;
    };
    
    class Windows_Client : public Client_Base;
    class Windows_Server : public Server_Base;
    
    class Win32 : Foobar
    {
      Win32()
      {
        p_client = new Windows_Client;
        p_server = new Windows_Server;
      }
    };
    
    class Unix_Client : public Client_Base;
    class Unix_Server : public Server_Base;
    
    class Unix : Foobar
    {
      Unix()
      {
        p_client = new Unix_Client;
        p_server = new Unix_Server;
      }
    };
    

    许多专家表示,问题可以通过另一个级别的间接来解决。

    【讨论】:

    • 根据过去的经验,我仍然认为 #ifdef 非常邪恶。
    【解决方案7】:

    两次拥有相同的基类并没有什么“非法”的。最后一个子类将只是(字面上)具有基类的多个副本作为它的一部分(包括基类中的每个变量等)。但是,它可能会导致对该基类函数的一些模棱两可的调用,您可能必须手动解决。这听起来不像你想要的。

    考虑组合而不是继承。

    此外,虚拟继承是一种将出现两次的同一个基类折叠在一起的方法。不过,如果它真的只是关于数据共享,那么组合可能更有意义。

    【讨论】:

      【解决方案8】:

      您可以使用限定的类名访问变量,但我忘记了确切的语法。

      但是,这是使用多重继承的糟糕案例之一,可能会给您带来很多困难。您可能不希望这样。

      您更可能希望 foobar 私有继承,让每个子类拥有一个 foobar,让 foobar 成为纯虚拟类,或者让派生类拥有它当前定义的东西,甚至自己定义 foobar。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-11-21
        • 2014-04-22
        • 2022-11-28
        • 1970-01-01
        相关资源
        最近更新 更多