private继承

实际开发中应用private继承的场景并不多,STL的MSVC实现、GCC的libstdc++、Clang的libC++中有少数使用了private继承,而我的实际开发中只有一个应用场景,例如libstdc++中的forward_list:

template<typename _Tp, typename _Alloc = allocator<_Tp>>
class forward_list : private _Fwd_list_base<_Tp, _Alloc>
{

例如libstdc++中的bitset:

template<size_t _Nb>
class bitset : private _Base_bitset<_GLIBCXX_BITSET_WORDS(_Nb)>
{

而实际开发中遇到的一个NonCopyable应用场景:

class NonCopyable
{
protected:
	NonCopyable()
	{
	}

	NonCopyable(NonCopyable&&) = delete;
	NonCopyable& operator=(NonCopyable&&) = delete;

	NonCopyable(const NonCopyable&) = delete;
	NonCopyable& operator=(const NonCopyable&) = delete;
};

class Test : private NonCopyable

...

先回顾一下private继承,在对上面的应用进一步解释

回顾private继承


private 继承的规则可以总结如下:

  • 如果class之间的继承关系是private,那么继承而来的所有成员在derived class中都为private属性,即使它们在base class中是public属性或者是protected属性

  • 如果class之间的继承关系是private,那么编译器不会自动将一个derived class对象转换为base class对象

    下面是public继承例子

      class Derived : public Base
      {
          ...
      }
    	
      void TestPrivateInherit(const Base& nc)
      {
          ...
      }
    	
      {
          Derived d;
          TestPrivateInherit(d);
      }
    

    这便是通常使用的上行转换,基类指针或者引用指向子类

    下面是使用private继承的例子

      class Derived : private Base
      {
          ...
      }
    	
      void TestPrivateInherit(const Base& nc)
      {
          ...
      }
    	
      {
          Derived d;
          TestPrivateInherit(d);
      }
    

    在编译期编译器报出类似如下错误:

      不允许对不可访问的基类 "Base" 进行转换	
    

从上面的规则延展出如下特性:

  • private继承并不能模塑出像public继承一样的is-a关系

    public继承能够模塑出is-a的关系,例如老鹰是鸟类,即老鹰public继承鸟类

  • private继承意味着derived class采用base class内已经存在的某些特性,derived class和base class之间不存在任何观念上的关系

    进一步说明是一种包含关系,例如游戏中的怪物有AI行为,并不能说怪物是AI

  • private继承意味只有实现部分被继承,接口被略去

    这里要知道什么是实现继承和接口继承

误区


从上面的规则看出,private继承可以表达一种包含关系,继而有人可能使用这种方式来表达包含关系,既然这样为什么不选择将继承的类作为子类的private成员变量呢, 实际开发中很少会使用private继承,通常的选择是将其作为private成员变量,大多数的时候是这样

如果设计的类选择private继承,而设计的类有可能又被继承,那么子类有可能重写基类的虚函数,即使选择的是private继承,例如下面代码:

class Base
{
public:
	virtual void Test()
	{
		...
	}
}

class Derived : private Base
{
public:
	void Invoked()
	{
		Test();
	}
}

class FinalDerived : public Derived
{
public:
	void Test() override
	{
		...
	}
}

而通常选择private继承并不希望其子类可以重写虚函数,例如上面的例子Invoked函数调用基类的Test()函数,而Test()函数被其他设计者重写了,对于类Derived的设计者并不希望如此,所以 Derived的设计者应该选择将Base作为其自身的private成员,而不是选择private继承

private继承应用场景


上面提到大多数的时候不会使用private继承,但是还有少数情况,当两个类不能是is-a关系,并且基类没有成员变量,没有virtual函数,也没有virtual base class可以使用private继承

这样的类是个空类,没有任何对象数据需要存储,例如上面的class NonCopyable,本意上要限定继承者的copy操作,NonCopyable没有任何成员变量,也没有virtual函数

如果使用public继承,也就意味着确定了is-a关系,也就意味着继承者是NonCopyable, 这显然在观念上是错误的,对于使用者这样的代码是成立的

class NonCopyable
{
protected:
	NonCopyable()
	{
	}

	NonCopyable(NonCopyable&&) = delete;
	NonCopyable& operator=(NonCopyable&&) = delete;

	NonCopyable(const NonCopyable&) = delete;
	NonCopyable& operator=(const NonCopyable&) = delete;
};

class SceneManager : public NonCopyable
class WidgetManager : public NonCopyable

void Test(NonCopyable* base);

{
	SceneManager a;
	WidgetManager b;
	Test(&a);
	Test(&b);
}

其实示例代码中的SceneManager和WidgetManager没有任何关系,对于使用者而言这样的使用方式可以说的过去,但是使用public继承那么基类的析构函数应该是virtual, 这里的基类显然没有定义virtual析构函数,这里使用private继承更为合理

如果说可以让这样的类作为另一个类的private成员变量,那么以NonCopyable为例,如果让NonCopyable作为SceneManager的private成员变量,并不能达成限定copy的效果

下面从空间的角度进一步佐证不能让这样的类作为另一个类的private成员变量

上面提到这样的类其实是个空类,C++规定独立(非附属)对象都必须有非零的大小,C++官方要求对于一个空对象要安插一个char,保证非空, 这就导致以包含关系作为另一个类的private成员变量会导致另一个类的大小不止增加1字节, 因为还有对齐规则,例如下面代码:

class Empty{}

class Widget
{
private:
	int x_;
	Empty e_;
}

会发现sizeof(Widget) > sizeof(int),所以不止增加1字节,其余空间是由对齐规则而增加的

从规定来看,如果以private继承的方式,这样的类作为基类,那么这样的类就是非独立的,例如下面代码:

class Empty{}

class widget : private Empty
{
private:
	int x_;
}

以private继承的方式计算其大小,会发现sizeof(Widget) == sizeof(int),这也就是EBO空基类优化EBO一般只在单一继承(而非多重继承)下才可行,即EBO技术不能实施于拥有多个base的 derived class身上

另外可以在空基类内使用typedef、enum、static成员变量、non-virtual函数

尽量使用复合避免使用private继承


实际开发中很少遇到空类,像上面的NonCopyable非常贴合private继承的应用场景,当没有足够的理由使用private继承时尽量使用复合的形式,即使private继承也能表达出复合的语义,复合的形式更易于理解

从空间的角度来看,以服务器开发为例,相信以现代的硬件配置不至于让开发者去节省几个字节,但是如果从事其他开发并且非常担忧空间浪费,那么EBO技术可能很有帮助