Blood is inherited and virtue is acquired.
-- Venezuelan Proverb
引子
在刚刚结束的《权力的游戏》第六季里,最让人热血沸腾的是第九集《The War of Bastards
》;而在第十集,Jon Snow
,这个具有公正,勇敢,富有荣誉感等多种美德的bastard
,正式获得继承权,成为临冬城主,北境之王!
这让我想起了C++
语言中的一个有趣特性:私有继承。
私有继承或许是C++
语言特有的一种特性。所以你在各种面向对象教材中,很难看到针对这种用法的讨论。在现实项目中,也很难看到它的踪影。
它究竟有何用途?
匿名访问
我们知道继承意味着子类和父类之间存在IS-A
的关系。所以,子类可自动被当作父类来使用(当然要符合里氏替换原则)。
下面的代码,可以非常顺利的编译通过。
struct Base {};
struct Derived : public Base {};
void f(Base* base);
// ...
Derived derived;
f(&derived); // 编译正常
但如果Derived
和Base
之间是私有继承的关系,在Derived
的外部客户看来, 它和Base
之间并不存在IS-A
的关系。事实上,在客户眼里,它们什么关系也没有。Base
作为 Derived
的一种内部实现细节,从逻辑上被彻底隐藏了。比如:
struct Derived : private Base {}; // 私有继承
// ...
Derived derived;
f(&derived); // 不能编译!!
而在类的设计者看来,Derived
像Base
的任何外部客户一样,可以自由访问Base
的所有public
成员,但对其非public
成员却毫无权限。也就是说,Base
相当于Derived
的一个成员变量。所以它们之间的关系相当于委托。
但它比委托要更方便。因为在委托方式下,Derived
必须定义一个 Base
类型的成员变量,并通过变量访问Base
的成员。而私有继承则无须这么做。其差别如下所示:
struct Base
{
void f();
};
// 委托
struct Object
{
void f1()
{
base.f(); // 需通过成员变量访问
}
private:
Base base;
};
// 私有继承
struct Derived : private Base
{
void f1()
{
f(); // 可直接访问
}
};
避免"中间人"
另外,编程实践中最让人厌烦的活动之一,是编写中间人(Middle Man
)式的转调代码。(关于中间人,参见Martin Fowler
的《重构》)
比如,我们原来有一个类Foo
,存在着多个public
成员函数接口。现在,系统中另一处需要一个类Bar
,Bar
只需要提供一个接口int f()
,其功能需求和Foo
提供的同名接口完全一致。所以,我们想通过委托关系复用Foo
的实现。
经典委托的实现方式,是直接转调其接口。比如:
struct Foo
{
int f();
int g();
int h();
// ...
};
struct Bar
{
int f()
{
return foo.f();
}
private:
Foo foo;
};
如果使用私有继承,则可以通过部分暴露的方式来简化你的工作:
struct Bar : private Foo // 私有继承
{
// 部分暴露 Foo 的接口。
using Foo::f;
};
我们知道,实现继承本质上是一种扩展关系。而这种通过私有继承进行部分暴露接口的用法是一种反扩展。
避免额外的派生类
像公有继承一样,私有继承的子类可以实现父类的虚函数。
所以,如果简单的以委托的方式来实现组合,程序员们则不得不先通过派生类给出实现,然后再组合派生类。如下图所示:
但这样的实现方式,不仅需要定义一个新的类,更重要的是,派生类的实现方式很可能需要使用客户类的内容,而客户类并不想将这些内容公开。两个类该如何配合,就变成了设计者一个非常棘手的问题。
但通过私有继承,则可以完美的解决这中问题。如下图所示:
对遗留系统结构体的封装
在遗留系统中,会存在一些只有数据没有行为的结构体。而这样的结构体经常作为参数,在模块之间到处传递。不同模块在获取数据之后,会根据这些数据进行一系列的计算。
比如, 我们又一个名为Rectangle
的数据结构:
struct Rectangle
{
int width;
int height;
};
对于这样的数据结构,我们当然想进行封装,以享用封装所带来好处。而如果这一切都发生在可控的单个子系统内部,毫无疑问,你应该这么做。
但如果Rectangle
跨多个子系统,或者一个子系统过大,你可能就会面临下列问题:
- 每个子系统对于
Rectangle
的行为定义都是不一样的,也就是说,它们唯一共享的就是数据; - 不同子系统由不同的团队维护,你无权修改它们的代码,也无权修改共享的头文件;
- 直接进行封装,会造成大面积的代码的修改;
- 其它子系统的开发语言仍然是
C
。 - ...
总而言之,你不能修改原有的结构体Rectangle
。 这种情况下,你依然想在正在重构的代码中对Rectangle
进行封装,那该怎么
办?
私有继承,是解决这类问题的不错选择。
struct MyRectangle : private Rectangle
{
int getArea() const
{
return width * height;
}
int getPerimeter() const
{
return 2 * (width + height);
}
};
为什么是私有继承?因为我们想进行封装,让子系统内部的代码没有人可以自由的访问数据,以享用封装带来的好处。
为什么不使用委托?因为如果没有之前所说的那些约束,这些数据和行为本来就应该属于一个类。而委托关系,会造成我们访问每个数据成员时,都必须通过成员变量进行间接访问,这毫无疑问给我们带来了不必要的负担。
但私有继承也意味着,在客户代码那里IS-A
关系的丧失。好在我们还有强制转换的武器,我们只需要在子系统边界对其进行类型强换,在子系统内部均使用MyRectangle
即可。
尽管看起来让人有些不安,由于这种继承完全没有修改任何内存布局,所以这种强转是绝对安全的。
// 本子系统边界函数
void s1_boundary(Rectangle* rect)
{
// ...
// 强制转换
s1_internal1((MyRectangle&)*rect);
// ...
}
// 子系统内部函数,使用 MyRectangle
void s1_internal1(MyRectangle& rect)
{
int perimeter = rect.getPerimeter();
// ...
s1_internal2(rect);
// ...
}
// 子系统内部函数,使用 MyRectangle
void s1_internal2(MyRectangle& rect)
{
int area = rect.getArea();
// ...
s2_boundary((Rectangle*)&rect);
}
// 其它子系统的边界函数,仍然使用 Rectangle
void s2_boundary(Rectangle* rect);
这种方法,在一些以消息作为进程间通信手段的嵌入式系统中,是一种非常有效的封装手段。
私有继承也是继承
Scott Meyers
在其著作《Effective C++》中,将私有继承定义为和组合一样的关系(is-implemented-in-terms- of
)。
尽管他也提到私有继承的一个优势是可以实现父类的虚函数,但他没有明确的指出,私有继承也是一种继承。
只是这种继承关系通过private
关键字对外界隐藏了真相;但是,在类的内部, 子类和基类IS-A
关系依然成立。我将这种关系称之为:私有继承的子类是父类的私生子。
这就意味着,当我们想利用继承关系来完成一些特定的设计,但又不想让这种关系被外部利用时,私有继承就是绝佳的选择。
比如:在一颗二叉树上,正常情况下,每个结点都有存在一个父结点和两个子节点。但存在一些例外情况:根节点没有父节点,而叶子节点则没有子节点。
所以,我们用这样的数据结构来表现一个节点:
struct Node
{
// ...
private:
Node* parent;
Node* leftChild, rightChild;
};
在这个树上,有时候一个节点的状态变化必须通知给其所有父子节点,而其父子也会进一步将此事件向上下传播。如下:
void Node::notifyEvent()
{
notifyParent();
notifyChildren();
}
void Node::notifyParent()
{
if(parent != 0) parent->onChildStateChange();
}
void Node::notifyChildren()
{
if(leftChild != 0)
{
leftChild->onParentStateChange();
}
if(rightChild != 0)
{
rightChild->onParentStateChange();
}
}
可以看到,代码中存在一些空指针判断,如果这些空指针只有少数的几个,也没有什么大问题。但如果这些的事件很多,每个事件的处理手段也不一样,可能就会早就很多的空指针判断语句,这让我们的代码很不干净。
当然解决空指针问题的常用手段是空对象模式(Null Object
)。所以,我们将设计修改为:
struct NodeEventListener
{
virtual void onParentStateChange() = 0;
virtual void onChildStateChange() = 0;
// ...
virtual ~NodeEventListener(){}
};
struct Node : NodeEventListener
{
Node(Node* parent, Node* leftChild, Node* rightChild);
void onParentStateChange();
void onChildStateChange();
// ...
private:
NodeEventListener* parent;
NodeEventListener* leftChild;
NodeEventListener* rightChild;
};
namespace
{
struct NullNode : NodeEventListener
{
void onParentStateChange() {}
void onChildStateChange() {}
// ...
static NullNode* getInstance()
{
static NullNode instance;
return &instance;
}
};
NodeEventListener* getListener(NodeEventListener* node)
{
return node == 0 ? NullNode::getInstance() : node;
}
}
Node::Node
( Node* parent
, Node* leftChild
, Node* rightChild)
: parent(getListener(parent))
, leftChild(getListener(leftChild))
, rightChild(getListener(rightChild))
{ }
void Node::notifyParent()
{
parent->onChildStateChange();
}
void Node::notifyChildren()
{
leftChild->onParentStateChange();
rightChild->onParentStateChange();
}
void Node::onParentStateChange()
{
// 真正的事件处理代码
}
void Node::onChildStateChange()
{
// 真正的事件处理代码
}
作为一个C++
的标准空对象模式的实现,上述设计解决了空指针判断的问题。并且Node
类和NodeEventListener
之间从概念层面的 IS-A
也是成立的。所以,我们理应为这样的结果感到欣慰。
但如果你再仔细审视一下,就会产生这样的疑问:这个IS-A
的关系,需要被Node
外部的用户知道吗?你希望他们可以通过NodeEventListener
类型来调用Node
的 onParentStateChange()
和onChildStateChange()
等方法吗?
答案是否定的,因为这是一个内部设计。
所以我们需要将这些接口隐藏起来。方法很简单:将继承关系改为 private
, 并将继承自NodeEventListener
的函数也设为 private
即可:
// ...
struct Node : private NodeEventListener
{
Node(Node* parent, Node* leftChild, Node* rightChild);
// ...
// 来自于 NodeEventListener 的函数被设为私有
private:
void onParentStateChange();
void onChildStateChange();
// ...
private:
NodeEventListener* parent;
NodeEventListener* leftChild;
NodeEventListener* rightChild;
};
尽管我们将继承关系改为了私有,但请注意,parent
等成员变量仍然是NodeEventListener
类型,而构造函数的参数类型却是Node
类型(想想为什么?)。 从Node
到NodeEventListener
的自动类型转换在构造函数的实现代码里完成,这说明Node
和 NodeEventListener
之间的IS-A
关系,在Node
看来,依然是成立的。
越狱
我们现在知道,私有继承会对外屏蔽子类和父类的继承关系。子类的外部客户不能将其看作父类类型,更不可能调用子类继承自父类的函数。
因此,在下面的代码中,Derived
似乎永远也无法被当作 Foo::f(Base*)
的参数,以利用Foo
所提供的服务。
struct Base
{
virtual void f() = 0;
virtual ~Base() {}
};
struct Foo
{
void f(Base* base)
{
base->f();
}
};
struct Derived : private Base
{
// ...
private:
void f() {}
};
Really
?你不妨尝试编译一下下面的代码,然后就会惊讶的发现,竟然是通过的。
// ...
struct Derived : private Base
{
// ...
void doSth(Foo* foo)
{
foo->f(this);
}
private:
void f() {}
};
其实不必惊讶,这是因为:这种继承关系只是对外界进行了隐藏,但其并没有消失,只是作为Derived
的一个私人秘密不为外人所知罢了。
在需要的时候,Derived
就可以将这种关系摆出来,让别人看在其父类的面子上为其提供服务。只要这种基于IS-A
关系的类型转换得以成功,别人才不管你是不是私生子。
这样的发现,可以在很多场合给我们的设计带来便利。比如,很多嵌入式设备都提供了Timer
服务。而Timer
模块要求:用户必须通过注册一个接口,以便于当指定的时间到期后,可以进行回调。比如:
struct TimerEventHandler
{
virtual void onTimeout() = 0;
virtual ~TimerEventHandler() {}
};
struct Timer
{
static void registerHandler
( unsigned int timeout
, TimerEventHandler* handler);
};
然后,一个类NetworkClient
需要Timer
服务。但当Timer
过期之后,其需要进行的操作都是自己的私有成员。如果创建一个专门的类以实现TimerEventHandler
,就要么需要使用友元关系,要么就必须把相关的成员改为public
。无论怎样,都会破坏NetworkClient
的封装性。
这种情况下,让 NetworkClient
直接继承(实现)TimerEventHandler
,就是一个最恰当的选择。同样,由于我们不希望这层关系为外界所知,从而产生不必要的依赖,我们应选择私有继承。
struct NetworkClient : private TimerEventHandler
{
void connect()
{
server.connect();
Timer::registerHandler(30, this);
// ...
}
private:
// 对 TimerEventHandler 的实现
void onTimeout()
{
state = STATE_DISCONNECTED;
sendAlarm(CONNECT_TIMEOUT);
}
void sendAlarm(AlarmType alarmType);
private:
State state;
// ...
};
结论
C++
的私有继承是一种非常有趣的关系。我们在之前几个大型C++
项目里,对于上述场景下,均经常性的使用私有继承。(估计我们是C++
社区里使用这个特性最多的团队:D)。
善用它,可以帮助我们在设计的便利性,信息隐藏,组合方面带来诸多好处。私有继承的美德可以让它成为软件设计一方之王。