解决C++多继承中override虚函数签名冲突

创作于:zhihu

C++本身并没有提供标准的接口(Interface)类型

所以我们可以有各种方式来实现接口机制

考虑面向对象的范式,C++提供了纯虚函数,所以我们可以自然地使用抽象类作为接口类型

纯虚函数是以= 0声明的虚函数(什么怪语法..)

如果一个类内部有纯虚函数,或者继承链上有没被实现的纯虚函数,那么这个类成为抽象类

抽象类对象无法构造,我们只能利用它的引用或指针类型多态,而实际派生出来的类通过继承抽象类并override纯虚函数来实现接口

一个简单的接口例子

struct IPrintable {
  virtual void print() = 0;
};

struct StrangePrinter : IPrintable {
  void print() override {
    cout << "StrangePrinter::print() hola, amigo!" << endl;
  }
};

struct NormalPrinter : IPrintable {
  void print() override {
    cout << "NormalPrinter::print() hello, world!" << endl;
  }
};

int main() {
  using PrinterPtr = IPrintable*;
  StrangePrinter s;
  NormalPrinter n;
  PrinterPtr ps = &s, pn = &n;
  ps->print();
  pn->print();
}
//output:
//StrangePrinter::print() hola, amigo!
//NormalPrinter::print() hello, world!

当然实际中我们不可能每个类只实现一个接口吧,如果是,是不是该考虑解耦了?

C++支持多重继承,所以我们直接让实际类继承多个抽象类实现多个接口

但是如果多个接口内有相同签名的纯虚函数该如何应对

比如我有一个IHasMinimum<T>接口,value()返回T的最小值,和一个IHasMaximum<T>接口,value()返回T的最大值

然后又有一个IHasBound<T>,其min(), max()分别返回T的最小(大)值,所以让IHasBound<T>接口去实现IHasMinimum<T>IHasMaximum<T>接口很合理吧

template <typename T> struct IHasMinimum {
  virtual T value() = 0;
};

template <typename T> struct IHasMaximum {
  virtual T value() = 0;
};
template <typename T>
struct IHasBound : public IHasMinimum<T>, public IHasMaximum<T> {
  virtual T min() = 0;
  virtual T max() = 0;
    // override IHasMinimum::value and IHasMaximum::value ???
}

但是这俩接口的value签名都是value(),如果IHasBoundoverridevalueIHasMinimumIHasMaximumvalue()都会被override,一个要返回最大值,一个要最小值,不能返回相同的值啊..

template <typename T>
struct IHasBound : public IHasMinimum<T>, public IHasMaximum<T> {
  T value() override { /*...*/ }
    // override both IHasMinimum::value and IHasMaximum::value 
}

也许改个函数名就解决问题了,比如两个value改名成min_valuemax_value

但也许 1. 我们改不了父类代码 2. 取不出更好的名字 3. 承担不了重命名的痛

如果可以这样分别为IHasMinimumIHasMaximum override两个版本的value()就好了

template <typename T>
struct IHasBound : public IHasMinimum<T>, public IHasMaximum<T> {
  T IHasMinimum<T>::value() override { /*...*/ }  
  T IHasMaximum<T>::value() override { /*...*/ }  
}

C++好像不支持这样的机制,C#是支持的

所以可以这样曲线救国,我们在IHasBoundIHasMinimum/IHasMaximum间分别再加一个新类,新类实现value()override的方法中向下转型获取IHasBoundthis指针

为了减少垃圾命名,先定义一个模板类

template <typename Interface, typename Struct> struct Impl;

然后这样

template <typename T> struct IHasMinimum {
  virtual T value() = 0;
};
template <typename T> struct IHasMaximum {
  virtual T value() = 0;
};

template <typename T> struct IHasBound;

template <typename T>
struct Impl<IHasMaximum<T>, IHasBound<T>> : public IHasMaximum<T> {
  T value() override { return static_cast<IHasBound<T> *>(this)->max(); }
};
template <typename T>
struct Impl<IHasMinimum<T>, IHasBound<T>> : public IHasMinimum<T> {
  T value() override { return static_cast<IHasBound<T> *>(this)->min(); }
};

template <typename T>
struct IHasBound : public Impl<IHasMaximum<T>, IHasBound<T>>,
                   public Impl<IHasMinimum<T>, IHasBound<T>> {
  virtual T min() = 0;
  virtual T max() = 0;
};

就好了,一个使用栗子,创建Range<T>类实现IHasBound,就可以同时也实现IHasMinimum/IHasMaximum

template <typename T> struct Range : public IHasBound<T> {
  T _mini, _maxi;
  T min() override { return _mini; }
  T max() override { return _maxi; }
  Range(T mini, T maxi) : _mini{mini}, _maxi{maxi} {}
};

int main() { 
  Range d(114.514, 1919.810);
  IHasMinimum<double> &i1 = d;
  IHasMaximum<double> &i2 = d;
  IHasBound<double> &i3 = d;
  IHasBound<double> *i4 = &d;
  std::cout << std::fixed << std::setprecision(3);
  std::cout << "IHasMinimum::value(): " << i1.value() << std::endl;
  std::cout << "IHasMaximum::value(): " << i2.value() << std::endl;
  std::cout << "IHasBound::min(): " << i3.min() << std::endl;
  std::cout << "IHasBound::max(): " << i3.max() << std::endl;
  std::cout << "IHasBound->min(): " << i4->min() << std::endl;
  std::cout << "IHasBound->max(): " << i4->max() << std::endl;
}
//IHasMinimum::value(): 114.514
//IHasMaximum::value(): 1919.810
//IHasBound::min(): 114.514
//IHasBound::max(): 1919.810
//IHasBound->min(): 114.514
//IHasBound->max(): 1919.810

btw,如果实现类不是模板类,以上写法可能会有incomplete type错误

这时可以使用CRTP,大概这么写

#include <iostream>
using namespace std;

template <typename Interface, typename Struct> struct Impl;
struct Base1 {
  virtual void fun() = 0;
};

struct Base2 {
  virtual void fun() = 0;
};

struct child;
template <std::same_as<child> T> struct Impl<Base1, T> : public Base1 {
  void fun() override {
    cout << static_cast<T *>(this)->name << ", hello from override for Base1" << endl;
  }
};
template <std::same_as<child> T> struct Impl<Base2, T> : public Base2 {
  void fun() override { cout << static_cast<T *>(this)->name << ", hello from override for Base2" << endl; }
};

struct child : public Impl<Base1, child>, public Impl<Base2, child> {
    string name{"iam child"};
};
int main() {
  child c;
  Base1 &b1 = c;
  Base2 &b2 = c;
  b1.fun();
  // hello from override for Base1
  b2.fun();
  // hello from override for Base2
  //c.fun();
  // ERROR: member found by ambiguous name lookup
}