捉妖记之——私有继承

C++的继承和大部分的语言不同的地方在于它的继承也有访问权限。通常来说用得比较广泛的两种是,公有继承和私有继承。虽然他们都是继承,但是实际上有非常本质的区别。《Effctive C++》中有提到,私有继承实际上是has-a的关系,而公有继承才是真正传统意义上的继承,也就是is-a的关系。

我们常说C++的动多态使用继承来实现,但是实际上更准确一点的说法应该是,C++中的动多态使用公有继承来实现,因为私有继承本质上来说是组合关系而不是继承关系,我们没有办法用父类的引用指向一个私有继承的子类对象。也就是说下面这段代码实际上无法通过编译。

1
2
3
4
5
6
7
8
9
10
class Base {
};

class Drived : Base {
};

int main(int argc, char* argv[]) {
Drived drived;
Base& base = drived; // 编译错误
}

这个错误其实并不难,因为编译时期就会报错,不会有太大的问题。我最近碰到一个编译可以正常通过,但是运行的时候会抛异常的BUG,花了一些时间定位,记录在这。

std::enable_shared_from_this

在C++11中,我们常常使用智能指针来避免资源泄露或者野指针问题,其中基于引用计数实现的智能指针std::shared_ptr有一个非常有趣的功能是:它可以实现获取this的智能指针。

我们知道,每个成员函数,都默认会有一个this参数指向当前对象的地址。但是这个指针是一个裸指针,使用不当会有野指针的风险,比如下面这段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
std::vector<std::function<void(void)>> actions;

class Widget {
public:
void RegisterAction() {
actions.push_back(std::bind(&Widget::Action, this));
}

void Action() {
// do something
}
};

void RegisterAction() {
Widget widget;
widget.RegisterAction()
}

int main(int argc, char* argv[]) {
RegisterAction();

for (const auto& action : actions) {
action();
}
}

Widget::RegisterAction中,我们把this指针传递给了std::bind,生成了一个std::function放在全局的actions中(我并不推荐使用全局变量,这里只是为了方便演示而已,实际代码中尽量不用全局变量),这个std::function对象的生命周期比widget本身要长,所以在main函数中,会产生野指针崩溃。

为了避免野指针,我们第一个想到的就是智能指针,搭配std::enable_shared_from_this,它可以获取到this的指针指针。所以上面这一段代码可以这样更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Widget : std::enable_shared_from_this<Widget> {                      // 1
public:
void RegisterAction() {
actions.push_back(std::bind(&Widget::Action, shared_from_this())); // 2
}

void Action() {
// do something
}
};

void RegisterAction() {
auto widget = std::make_shared<Widget>(); // 3
widget->RegisterAction()
}

注意上面的注释处的修改,首先如注释// 1所示,我们要继承自std::enable_shared_from_this,然后如注释// 2所示,使用shared_from_this获取到this的智能指针。需要特别注意的是,我们必须保证Widget的对象是通过智能指针构造出来的,如注释//3标注所示。

但是

上面这段代码可以通过编译,但是在运行的时候会抛std::bad_weak_ptr异常。上面这段代码逻辑上没有什么问题,真正的错误在于继承上,我们使用std::enable_shared_from_this的时候,必须公有继承自它,而class的默认的继承是私有继承。下面这段描述来自cppreference

Publicly inheriting from std::enable_shared_from_this provides the type T with a member function shared_from_this

注意最前面的Public inheriting

为什么

实际上,enable_shared_from_this类的内部,保存了一个weakptr,这个weakptr会在构造shared_ptr的时候被设置,下面这段代码来自boost

1
2
3
4
5
6
7
8
9
10
11
template< class X, class Y, class T > inline void sp_enable_shared_from_this( boost::shared_ptr<X> const * ppx, Y const * py, boost::enable_shared_from_this< T > const * pe )
{
if( pe != 0 )
{
pe->_internal_accept_owner( ppx, const_cast< Y* >( py ) );
}
}

inline void sp_enable_shared_from_this( ... )
{
}

如果我们使用的是公有继承,我们可以使用多态,把自己绑定到第一个模板函数的最后一个参数pe上面,进而设置pe内部的weakptr。否则我们会fallback到后面那个函数,导致enable_shared_from_this内部的weakptr未初始化成正确的值,最终我们调用shared_from_this的时候会尝试从这个未初始化成正确的值的weakptr构造一个shared_ptr,导致std::bad_weak_ptr异常的产生。