Как спроектировать расширяемое множественное наследование?

У меня есть еще один вопрос о множественном наследовании, у которого есть ответ, т.е. Вот (но сосредоточены на след) или Вот (слишком расплывчато), но большинство ответов, на которые я наткнулся, подчеркивают недостатки производительности. Однако (как утверждает Бьярне Страуструп Вот) это языковая особенность, которая должна быть предпочтительнее, чем обходные пути. Вот более длинный пример, чтобы проиллюстрировать вопрос, который следует за примером:

Пример

В Чешской Республике номер рождения (эквивалент SSN) присваивается в следующем формате:
YYMMDDXXX, так что давайте класс, чтобы получить дату рождения в стандартном 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, чтобы получить дату правильно:

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 году, через десять лет кто-нибудь может написать Фасад, чтобы правильно определить дату рождения для будущих тысячелетий:

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

Вопрос

Проблема в том, что Human, Man а также Woman может быть в виде двоичной библиотеки, где клиентский код не может переписать наследование в виртуальное. Итак, как правильно спроектировать расширяемую библиотеку, чтобы включить множественное наследование? Должен ли я сделать каждое наследование в области видимости библиотеки виртуальным (поскольку я заранее не знаю, как его можно расширить), или есть какой-нибудь более элегантный универсальный дизайн?

Относительно производительности: разве это не область низкоуровневого программирования и оптимизации компилятора, не должна ли перспектива проектирования преобладать на высокоуровневом программировании? Почему компиляторы не виртуализируют наследование автоматически, как в RVO или inline называет решения?

Грустная история за примером

В 1954 году технически вдохновленный бюрократ решил, что десятый шифр будет добавлен таким образом, что число будет делиться на 11. Позже гений выяснил, что существуют числа, которые нельзя изменить таким способом. Поэтому он выдал исключение, что в этих случаях последний номер будет нулевым. Позже в том же году была издана внутренняя директива, запрещающая подобные исключения. Но в то же время было выпущено более 1000 номеров рождений, которые не делятся на 11, но все же являются законными. Независимо от этого беспорядка, по числу чисел можно определить век года до 2054 года, когда мы испытаем возрождение 2000 года. Увы, также распространена практика, что иммигрантам, родившимся до 1964 года, присваивается 10-значный номер рождения.

0

Решение

Если вы не можете редактировать исходную библиотеку, вы можете попытаться решить ее с помощью «mixin», то есть новый класс фасадов параметризован их собственным базовым классом. Man или же Woman,

Например:

#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 ]

В любом случае, вам лучше переопределить все эти классы, ИМХО лучший вариант — сохранить дату как целое число или тип (ы) std :: chrono, а пол — как поле перечисления. Предоставьте дополнительные фабричные методы для разбора строки формата даты и вставьте зависимости только в человеческий класс.

1

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]