如何设计可扩展的多重继承?
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
问题
问题是 Human
、Man
和 Woman
可能采用二进制库的形式,其中客户端代码无法将继承重写为虚拟。那么如何正确设计可扩展库来实现多重继承呢?我应该将库范围内的所有继承都虚拟化(因为我事先不知道如何扩展它),还是有更优雅的通用设计?
关于性能:这不是低级编程和编译器优化的领域吗,难道设计观点不应该在高级编程中占上风吗?为什么编译器不像在 RVO 或 inline
调用决策中那样自动虚拟化继承?
榜样背后的心酸故事
在 1954 年,一些受技术启发的官僚决定以某种方式添加第十位密码,这样数字就可以被 11 整除。后来这位天才发现有些数字是无法修改的方法。所以他发布了一个例外,在这些情况下,最后一个数字将为零。那年晚些时候发布了一项内部指令,不允许出现此类例外情况。但与此同时,发布了 1000 多个不能被 11 整除的出生号码,但仍然合法。撇开这乱七八糟的事情不谈,根据数字长度可以推算出世纪,直到2054年,我们才会经历Y2K复兴。唉,1964年之前出生的移民也有一个普遍的做法,就是分配一个10位数的出生号码。
如果不能编辑原来的lib,可以尝试用一个"mixin"来解决,即新门面class由自己的baseclass参数化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 ]
无论如何,你最好重新设计所有那些 classes,恕我直言,最好的选择 - 将日期存储为整数或 std::chrono 类型和性别作为枚举字段。提供额外的工厂方法来解析日期格式字符串,并将依赖关系注入到人类 class.
我还有另一个关于多重继承设计的问题,它有一个答案,即 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
问题
问题是 Human
、Man
和 Woman
可能采用二进制库的形式,其中客户端代码无法将继承重写为虚拟。那么如何正确设计可扩展库来实现多重继承呢?我应该将库范围内的所有继承都虚拟化(因为我事先不知道如何扩展它),还是有更优雅的通用设计?
关于性能:这不是低级编程和编译器优化的领域吗,难道设计观点不应该在高级编程中占上风吗?为什么编译器不像在 RVO 或 inline
调用决策中那样自动虚拟化继承?
榜样背后的心酸故事
在 1954 年,一些受技术启发的官僚决定以某种方式添加第十位密码,这样数字就可以被 11 整除。后来这位天才发现有些数字是无法修改的方法。所以他发布了一个例外,在这些情况下,最后一个数字将为零。那年晚些时候发布了一项内部指令,不允许出现此类例外情况。但与此同时,发布了 1000 多个不能被 11 整除的出生号码,但仍然合法。撇开这乱七八糟的事情不谈,根据数字长度可以推算出世纪,直到2054年,我们才会经历Y2K复兴。唉,1964年之前出生的移民也有一个普遍的做法,就是分配一个10位数的出生号码。
如果不能编辑原来的lib,可以尝试用一个"mixin"来解决,即新门面class由自己的baseclass参数化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 ]
无论如何,你最好重新设计所有那些 classes,恕我直言,最好的选择 - 将日期存储为整数或 std::chrono 类型和性别作为枚举字段。提供额外的工厂方法来解析日期格式字符串,并将依赖关系注入到人类 class.