C++ 杂谈(5)

再看 public/protected/private 继承

通常我们常用 public 继承表达 is 关系,private 继承表达 implemented with 关系:

  • public 继承,父类所有 public 方法均在子类中成为 public,因此从接口上来看,子类的对象拥有父类的接口,因此子类的对象也“是”父类的对象;
  • private 继承表示子类是使用父类提供的接口实现的,此时父类的接口默认在子类里面可以用,但并不能被使用子类的人使用

值得注意的是与 access control 的 public/protected/private 这是一个平行的概念,access control 表示了不论哪种继承,哪些接口、成员是子类可以访问的:

  • public/protected 是子类可以访问
  • private 是子类无法访问的

而作为继承,它表示继承关系本身的可见:

  • public 继承表示任何知道父类和子类的都知道两者的继承关系
  • protected 继承表示仅有子类及其它的子类知道两者的继承关系
  • private 继承表示除了子类自己,其他都不知道这层继承关系

已知这层继承关系意味着对应指针/引用可以进行转换。我们拿一个 CRTP 的设计问题来看这三个继承的使用。在 lua binding 的设计中我们引入了一个 wrapper 类,它包裹了 lua C API 的所有功能,提供了 OO 抽象的基本 layer,它依赖子类提供 state() 方法的实现,一般来说子类的 state 方法并不应该是 public 的,那么在 CRTP 里面进行 static_cast<Child*>(this) 时不能访问 state() 方法:具体的一个例子是比如 table 类继承了 wrapper<table>,因为我们 table 的接口明显不需要那些 low-level 的 API,所以 table 是 implemented by 关系,选择 private 继承,可是一般因为在 wrapper 里面调用 table 的 state 方法并不是子类调父类的方法而是反过来的,因此 table 必须让 wrapper<table> 成为自己的 friend。

那么什么时候应该使用 protected 继承呢?我们考虑 vm 的设计,一开始我们也使用了 wrapper<vm> 作为 private 的父类,后来我们引入 thread 发现 thread 和 vm 的区别仅仅是 vm 需要 own 其中的 lua_State*,而 thread 必然是被其他的 lua_State 生成出来,所以它不需要在最后 lua_close 自己的 state,产生它的部分的 gc 会处理。因此 thread 和 vm 共享了很多方法,我们将这些使用 wrapper 提供的 API 实现的方法剥离出来放在 thread_ops 这个类里面,这样实际 thread、vm 可以共享这个部分。这时 thread_ops 自身是一个 template class,其实现依赖于 wrapper,但是却不仅仅是 implemented by 的关系:thread_ops 并不提供 wrapper 需要的 state 方法,因此 thread_ops<T> 继承 wrapper<T> 而不是 wrapper<thread_ops<T> >,它是 wrapper<T> 的扩展。

  • 这时如果我们使用 public 继承那么 thread_ops 将拥有所有 wrapper 的 API,但其实我们只希望将 thread_ops 的 API 贡献到 thread/vm 类,也就是说 thread/vm 应该 public 继承 thread_ops,这样一来我们也就将 wrapper 的 API 引入到 thread/vm 中了。
  • 那么是否能使用 private 继承呢?需要注意如果 thread/vm 不知道其父类存在 wrapper<thread> 或 wrapper<vm>,那么在 wrapper 中使用 static_cast<Derived*>(this) 就会出现问题,这个转换之所以能成功是因为父类指针 this 本身其实就是指向子类 Derived 的对象(Derived 继承了 CRTP<Derived>),private 继承一来,thread/vm 都不认为 wrapper<thread> 和 wrapper<vm> 是其父类,因此 wrapper 里面的转换就不被编译器允许了

使用 protected 继承在这个情况下是唯一的选择,这时我们仍然能够进行转换且将父类的 API 隐藏了起来。更多讨论参看这里

成员函数指针

我们可以通过 RetType (T::*)(Args…) 匹配这类指针,可以使用 is_member_function_pointer 这个 trait 来判断,通常通过它可以 invoke 对应的方法,如果有 T* t,则可以 (t->*func_ptr)(Args…) 完成调用,语法看起来有点诡异。

奇怪的模板方法调用

有某些时候你得把调用写成 t->template foo<T>(),这个出现在一个模板类的实现里面,它需要调用另一个模板方法的实例传递自己的模板参数到 t 的 foo 方法需要在前面加上 template。否则编译器的错误可难理解了。参看这里。至于原因是为了简化编译器的实现, t->foo<T> 中 < 可以理解成为 t->foo 比 T 小,这跟 typename T::SubType * ptr 需要 typename 类似,因为可以理解成为 T::SubType 是一个类 T 的 static member,这时就可以当做两个变量相乘了,不过总觉得 C++ 为了简化编译器实现而把这种 exception burden 施加给 developer 太不厚道了。

模板方法的又一奇葩功能

前面说了模板方法与模板类的一个区别,模板方法不能“部分特化”(并不是不能特化),但是模板函数可以与重载函数共存,匹配的时候看谁更具体,重载的函数支持 implicit conversion,而模板函数不支持,这在两者共存的时候提供了判断到底调用哪个版本的依据。一个有意思的事情是我们这里设计的 wrapper 类其中提供了对 is*、to* 和 push* 的封装,push 完全可以通过重载来做,但是前两者因为没有参数,只能通过模板,但是因为特化不能用,就得在外部实现一个 struct 专门用来在模板函数里用。

——————
And he said, Let us take our journey, and let us go, and I will go before thee

Advertisements
C++ 杂谈(5)

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s