如何设计可扩展的多重继承?

How to design extensible multiple inheritance?

我还有另一个关于多重继承设计的问题,它有一个答案,即 here (but focused on footprint) or here (too vague), but most answers I stumbled upon is emphasizing the performance drawbacks. However (as Bjarne Stroustrup claims here) 它是一种语言特性,应该优先于解决方法。这是一个更长的例子来说明例子后面的问题:

例子

在捷克共和国,出生号码(相当于 SSN)的分配格式如下: YYMMDDXXX,所以让我们用 class 来获取标准 D.M.YYYY:

中的出生日期
class Human {
protected:
  char output[11];
  char input[10];

public:
  Human (const char* number) {
    strncpy(input, number, 10);
    if(!number[10]) throw E_INVALID_NUMBER;
  }

  static int twoCharsToNum(const char* str) {
    if(!isdigit(str[0]) || !isdigit(str[1])) throw E_INVALID_NUMBER;
    return (str[0]-'0')*10 +str[1]-'0';
  }

  const char* getDate() {
    sprintf(output, "%d.%d.%d", getDay(), getMonth(), getYear());
    return output;
  }

  // range check omitted here to make code short
  virtual int getDay() { return twoCharsToNum(input+4); }
  virtual int getMonth() { return twoCharsToNum(input+2); }
  virtual int getYear() { return twoCharsToNum(input)+1900; }
};

三种方法是虚拟的,因为女性的月数+50。因此,让我们继承 Man 和 Woman classes 以正确获取日期:

class Man : public Human {
public:
  using Human::Human;
};

class Woman : public Human {
public:
  using Human::Human;
  int getMonth() {
    int result = twoCharsToNum(input+2)-50;
    if(result<0) throw E_INVALID_GENDER;
    if(result==0 || result>12) throw E_INVALID_RANGE;
    return result;
  }
};

自 1954 年以来,该号码的附录是 4 位数字,而不是 3 位数字(本问题末尾提到的这背后有一个悲伤的故事)。如果图书馆是在 1944 年编写的,十年后有人可以编写一个 Facade 来为未来的千禧一代正确获取出生日期:

class Human2 : public Human {
public:
  using Human::Human;
  virtual int getYear() {
    int year = twoCharsToNum(input);
    if(year<54 && strlen(number)==10) year+= 2000;
    else year+= 1900;
    return year;
  }
};

class Man2 : public Human2 {
public:
  using Human2::Human2;
};

class Woman2中我们需要Woman::getMonth方法,所以需要解决菱形问题:

class Human2 : virtual public Human { ... };
class Woman  : virtual public Human { ... }; // here is the real issue
class Woman2 : public Human2, public Woman {
  using Human2::Human2;
  using Woman::Woman;
};

钻石题图:

    Woman2
    ^    ^
    |    |
Woman    Human2
    ^    ^
    |    |
    Human 

问题

问题是 HumanManWoman 可能采用二进制库的形式,其中客户端代码无法将继承重写为虚拟。那么如何正确设计可扩展库来实现多重继承呢?我应该将库范围内的所有继承都虚拟化(因为我事先不知道如何扩展它),还是有更优雅的通用设计?

关于性能:这不是低级编程和编译器优化的领域吗,难道设计观点不应该在高级编程中占上风吗?为什么编译器不像在 RVO 或 inline 调用决策中那样自动虚拟化继承?

榜样背后的心酸故事

在 1954 年,一些受技术启发的官僚决定以某种方式添加第十位密码,这样数字就可以被 11 整除。后来这位天才发现有些数字是无法修改的方法。所以他发布了一个例外,在这些情况下,最后一个数字将为零。那年晚些时候发布了一项内部指令,不允许出现此类例外情况。但与此同时,发布了 1000 多个不能被 11 整除的出生号码,但仍然合法。撇开这乱七八糟的事情不谈,根据数字长度可以推算出世纪,直到2054年,我们才会经历Y2K复兴。唉,1964年之前出生的移民也有一个普遍的做法,就是分配一个10位数的出生号码。

如果不能编辑原来的lib,可以尝试用一个"mixin"来解决,即新门面class由自己的baseclass参数化ManWoman

例如:

#include <iostream>
#include <system_error>
#include <cstring>
#include <memory>
#include <type_traits>

class Human {
protected:
    char output[11];
    char input[10];

public:
    Human (const char* number) {
        memcpy(input, number, 10);
        if(!number[10])
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
    }

    static int twoCharsToNum(const char* str) {
        if(!isdigit(str[0]) || !isdigit(str[1]))
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
        return (str[0]-'0')*10 +str[1]-'0';
    }

    const char* getDate() {
        sprintf(output, "%d.%d.%d", getDay(), getMonth(), getYear());
        return output;
    }

    // range check omitted here to make code short
    virtual int getDay() {
        return twoCharsToNum(input+4);
    }
    virtual int getMonth() {
        return twoCharsToNum(input+2);
    }
    virtual int getYear() {
        return twoCharsToNum(input)+1900;
    }
};

class Man:public Human {
public:
    Man(const char* number):
        Human(number)
    {}
};

class Woman : public Human {
public:
    Woman(const char* number):
        Human(number)
    {}
    virtual int getMonth() override {
        int result = Human::twoCharsToNum(input+2)-50;
        if(result<0)
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
        if(result==0 || result>12)
            throw std::system_error( std::make_error_code( std::errc::invalid_argument ) );
        return result;
    }
};

template<class GenderType>
class Human_Century21:public GenderType {
public:

    explicit Human_Century21(const char* number):
        GenderType(number)
    {
        // or use std::enabled_if etc
        static_assert( std::is_base_of<Human,GenderType>::value, "Gender type must inherit Human" );
    }

    virtual int getYear() override {
        int year = Human::twoCharsToNum(this->input);
        if(year<54 && std::strlen(this->input) == 10 )
            year += 2000;
        else
            year += 1900;
        return year;
    }
};


int main ()
{
    auto man = std::make_shared< Human_Century21<Man> >(  "530101123"  );
    std::cout << "Man: [ year: " << man->getYear() << ", month:" << man->getMonth() << " ]" << std::endl;
    auto woman = std::make_shared< Human_Century21<Woman> >( "54510112345" );
    std::cout << "Woman: [ year: " << woman->getYear() << ", month:" << woman->getMonth() << " ]" << std::endl;
    return 0;
}

输出:

Man: [ year: 1953, month:1 ]
Woman: [ year: 1954, month:1 ]

无论如何,你最好重新设计所有那些 classes,恕我直言,最好的选择 - 将日期存储为整数或 std::chrono 类型和性别作为枚举字段。提供额外的工厂方法来解析日期格式字符串,并将依赖关系注入到人类 class.