虚拟继承 - 跳过构造函数

Posted

技术标签:

【中文标题】虚拟继承 - 跳过构造函数【英文标题】:virtual inheritance - skipping constructors 【发布时间】:2013-06-05 16:43:33 【问题描述】:

我有以下课程:

class Socket

    Socket();
    Socket( SOCKET s );
;

class Connection : public virtual Socket

    Connection( IP ip );
;

这两个类包含一些纯虚函数和一些非虚函数以及它们自己的一些数据。它们的重点是我将派生一些套接字类型,实现不同的协议。

所以我专攻这两个课程:

class ProtocolSocket : public virtual Socket

    ProtocolSocket() 
    ProtocolSocket( SOCKET s ) : Socket( s )  ; 
;

class ProtocolConnection : public ProtocolSocket, public virtual Connection

    ProtocolConnection( SOCKET s, IP ip ) : ProtocolSocket( s ), Connection( ip ) ;
;

出了点问题 - 我相信你们中的许多人都可以看到。我尝试创建一个 ProtocolConnection:

new ProtocolConnection( s, ip );

施工过程如下:

start ctor ProtocolConnection
    start ctor Connection
       start ctor Socket
          Socket(); - default ctor via Connection's init list
       end ctor Socket
       Connection(); - default ctor ProtocolConnection's init list
    end ctor Connection
    start ctor ProtocolSocket
       start ctor Socket     
          // Socket( s ); - skipped!!! - would have been from init 
          //                list of ProtocolSocket, but ctor for this object 
          //                already called!
       end ctor Socket
       ProtocolSocket( s ); -from init list of ProtocolConnection()
    end ctor ProtocolSocket
    ProtocolConnection( s, ip );
end ctor ProtocolConnection

跳过第二个 Socket 构造函数是语言规范所说的应该发生的事情,并且有充分的理由。

如何让它调用带有 Socket(s) 的构造函数,而不是之前的那个?

我打算拥有多个派生类,例如还有 OtherProtocolSocket 和 OtherProcolConnection,与 ProtocoSocket 和 ProtocolConnection 对象处于同一级别。

我想要达到的效果是我想构造 ProtocolSocket 和 ProtocolConnection 对象,然后将它们作为 Socket 和 Connection 对象传递给我的系统。因此,在我制作了一个实现给定协议的套接字后,我只需对其进行读写,而不用担心底层协议的细节。

Connection 对象需要继承 Socket 对象的所有方法。

@更新:

DyP 建议在 ProtocolConnection 中为 Socket 添加初始化器。这解决了问题。我会给你一个接受...但这只是在评论中。

【问题讨论】:

是否可以通过让 Connection 类持有一个 Socket 对象而不是从 Socket 派生来避免使用虚拟继承?从长远来看,通过保持 Socket 和 Connection 的概念彼此独立,这可能会使事情变得更简单。 如果我通过遏制来做到这一点,那么我必须通过诸如“读取”和“写入”之类的方法从 Connection 到包含的套接字。另外,当它们已经连接时,我不能互换使用套接字和连接。所以连接和套接字都是读/写的东西。连接对象是我可以称之为“连接”的东西。 [class.base.init]/10 "首先,并且仅对于最派生类 (1.8) 的构造函数,虚拟基类按照它们出现在深度优先左侧的顺序进行初始化从右到右遍历基类的有向无环图,其中“从左到右”是派生类基类说明符列表中基类的出现顺序。”让我想知道为什么ConnectionSocket 之前初始化(通过ProtocolSocket)。 您是否尝试将Socket 的初始化程序放入ProtocolConnection 的mem-initializer-list 中? @DyP:有效。太棒了。 【参考方案1】:

要记住的关键是虚拟基类的构造函数是作为最派生类初始化的一部分完成的(并且在构造其他基类之前)。所以你的施工顺序幻灯片不正确。

事实上,当你构造 ProtocolConnection 时发生的事情是它首先构造了 Socket,然后是 Connection(因为你实际上继承了它),最后是 ProtcolSocket。

要调用你想要的socket的构造函数,你需要调用它的构造函数作为ProtocolSocket成员初始化列表的一部分,这样

class ProtocolConnection: public ProtocolSocket, public virtual Connection

    public:
    ProtocolConnection(int s, int ip) :
        Socket(s), Connection(ip), ProtocolSocket(s)  
        // Note, also reordered, since all virtual bases are initialized before the
        // non-virtual bases
    
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    
;

最后,作为说明,我建议您简化继承层次结构。特别是,虚拟继承和使用多个构造函数会使因素复杂化。

【讨论】:

感谢扩展的答案、解释等,类似于 DyP。我一般避免虚拟继承,也避免多重继承。我真的避免将它们结合起来。但是如果不这样做,我看不到另一种将协议隐藏在这些对象中的方法,除非我愿意编写一堆传递函数。【参考方案2】:

继承 DAG:

       ProtocolConnection
           /        \
     non-virtual  virtual
         /            \
ProtocolSocket     Connection
       |               |
    virtual         virtual
       |               |
    Socket           Socket

请注意,由于虚拟继承,ProtocolConnection 类型的对象中只有一个 Socket 子对象。

[class.base.init]/10

首先,并且仅对于最派生类 (1.8) 的构造函数,虚拟基类按照它们在基类的有向无环图的深度优先从左到右遍历中出现的顺序进行初始化,其中“从左到右”是派生类基说明符列表中基类的出现顺序。

虚拟基类的初始化是通过深度优先的从左到右遍历完成的。遍历顺序:

       (0) ProtocolConnection
             /             \
           nv               v
           /                 \
(1) ProtocolSocket    (3) Connection
         |                   |
         v                   nv
         |                   |
    (2) Socket         (4) Socket

导致以下初始化顺序:

(2); (3); (1); (0)Socket; Connection; ProtocolSocket(非虚拟基类); ProtocolConnection

派生最多的类ProtocolConnection 必须包含所有 虚拟基类的初始化器。如果虚拟基类没有出现在最派生类的 mem-initializer-list 中,则该虚拟基类的子对象将被默认构造。

【讨论】:

以上是关于虚拟继承 - 跳过构造函数的主要内容,如果未能解决你的问题,请参考以下文章

使用虚拟继承和委托构造函数在构造函数中崩溃

虚拟继承、默认构造函数和额外复制

使用虚拟继承时调用默认构造函数[重复]

在使用 C++ 进行虚拟继承期间调用构造函数

C++继承详解

C++继承详解