文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载
1.继承关系中构造函数的困局
相信读者在编程经历中一定遇到过下面的问题,假设现在有一个类Base提供了很多不同的构造函数。某一天,你发现Base无法满足未来业务需求,需要把Base作为基类派生出一个新类Derived并且对某些函数进行改造以满足未来新的业务需求,比如下面的代码:
class Base {
public:
Base() : x_(0), y_(0.) {};
Base(int x, double y) : x_(x), y_(y) {}
Base(int x) : x_(x), y_(0.) {}
Base(double y) : x_(0), y_(y) {}
void SomeFunc() {}
private:
int x_;
double y_;
};
class Derived : public Base {
public:
Derived() {};
Derived(int x, double y) : Base(x, y) {}
Derived(int x) : Base(x) {}
Derived(double y) : Base(y) {}
void SomeFunc() {}
};
基类Base的SomeFunc无法满足当前的业务需求,于是在其派生类Derived中重写了这个函数,但令人头痛的是,面对Base中大量的构造函数,我们不得不在Derived中定义同样多的构造函数,目的仅仅是转发构造参数,因为派生类本身并没有需要初始化的数据成员。单纯地转发构造函数不仅会导致代码的冗余,而且大量重复的代码也会让程序更容易出错。实际上,这个工作完全可以让编译器自动完成,因为它实在太简单了,让编译器代劳不仅消除了代码冗余而且意图上也更加明确。
2. 使用继承构造函
我们都知道C++中可以使用using关键字将基类的函数引入派生类,比如:
class Base {
public:
void foo(int) {}
};
class Derived : public Base {
public:
using Base::foo;
void foo(char*) {}
};
int main(int argc, char** argv)
{
Derived d;
d.foo(5);
}
C++11的继承构造函数正是利用了这一点,将using关键字的能力进行了扩展,使其能够引入基类的构造函数:
class Base {
public:
Base() : x_(0), y_(0.) {};
Base(int x, double y) : x_(x), y_(y) {}
Base(int x) : x_(x), y_(0.) {}
Base(double y) : x_(0), y_(y) {}
private:
int x_;
double y_;
};
class Derived : public Base {
public:
using Base::Base;
};
在上面的代码中,派生类Derived使用using Base::Base让编译器为自己生成转发到基类的构造函数,从结果上看这种实现方式和前面人工编写代码转发构造函数没有什么区别,但是在过程上代码变得更加简洁易于维护了。
使用继承构造函数虽然很方便,但是还有6条规则需要注意。
1.派生类是隐式继承基类的构造函数,所以只有在程序中使用了这些构造函数,编译器才会为派生类生成继承构造函数的代码。
2.派生类不会继承基类的默认构造函数和复制构造函数。这一点乍看有些奇怪,但仔细想想也是顺理成章的。因为在C++语法规则中,执行派生类默认构造函数之前一定会先执行基类的构造函数。同样的,在执行复制构造函数之前也一定会先执行基类的复制构造函数。所以继承基类的默认构造函数和默认复制构造函数的做法是多余的,这里不会这么做。
3.继承构造函数不会影响派生类默认构造函数的隐式声明,也就是说对于继承基类构造函数的派生类,编译器依然会为其自动生成默认构造函数的代码。 4.在派生类中声明签名相同的构造函数会禁止继承相应的构造函数。这一条规则不太好理解,让我们结合代码来看一看:
class Base {
public:
Base() : x_(0), y_(0.) {};
Base(int x, double y) : x_(x), y_(y) {}
Base(int x) : x_(x), y_(0.)
{
cout << "Base(int x)" << endl;
}
Base(double y) : x_(0), y_(y)
{
cout << "Base(double y)" << endl;
}
private:
int x_;
double y_;
};
class Derived : public Base {
public:
using Base::Base;
Derived(int x)
{
cout << "Derived(int x)" << endl;
}
};
int main(int argc, char** argv)
{
Derived d(5);
Derived d1(5.5);
}
在上面的代码中,派生类Derived使用using Base::Base继承了基类的构造函数,但是由于Derived定义了构造函数Derived(int x),该函数的签名与基类的构造函数Base(int x) 相同,因此这个构造函数的继承被禁止了,Derived d(5)会调用派生类的构造函数并且输出"Derived(int x)"。另外,这个禁止动作并不会影响到其他签名的构造函数,Derived d1(5.5)依然可以成功地使用基类的构造函数进行构造初始化。
5.派生类继承多个签名相同的构造函数会导致编译失败:
class Base1 {
public:
Base1(int)
{
cout << "Base1(int x)" << endl;
};
};
class Base2 {
public:
Base2(int)
{
cout << "Base2(int x)" << endl;
};
};
class Derived : public Base1, Base2 {
public:
using Base1::Base1;
using Base2::Base2;
};
int main(int argc, char** argv)
{
Derived d(5);
}
在上面的代码中,Derived继承了两个类Base1和Base2,并且继承了它们的构造函数。但是由于这两个类的构造函数Base1(int) 和Base2(int)拥有相同的签名,导致编译器在构造对象的时候不知道应该使用哪一个基类的构造函数,因此在编译时给出一个二义性错误。
6.继承构造函数的基类构造函数不能为私有:
class Base {
Base(int) {}
public:
Base(double) {}
};
class Derived : public Base {
public:
using Base::Base;
};
int main(int argc, char** argv)
{
Derived d(5.5);
Derived d1(5);
}
在上面的代码中,Derived d1(5)无法通过编译,因为它对应的基类构造函数Base(int)是一个私有函数,Derived d(5.5)则没有这个问题。
最后再介绍一个有趣的问题,在早期的C++11编译器中,继承构造函数会把基类构造函数注入派生类,于是导致了这样一个问题:
#include
struct Base {
Base() = default;
template Base(T, typename T::type = 0)
{
cout << "Base(T, typename T::type)" << endl;
}
Base(int)
{
cout << "Base(int)" << endl;
}
};
struct Derived : Base {
using Base::Base;
Derived(int)
{
cout << "Derived(int)" << endl;
}
};
int main(int argc, char** argv)
{
Derived d(42L);
}
上面这段代码用早期的编译器(比如GCC 6.4)编译运行的输出结果是Base(int),而用新的GCC编译运行的输出结果是
Derived(int)。在老的版本中,template
template Derived(T);
template Derived(T, typename T::type);
这是因为继承基类构造函数时,不会继承默认参数,而是在派生类中注入带有各种参数数量的构造函数的重载集合。于是,编译器理所当然地选择推导Derived(T)为Derived(long)作为构造函数。在构造基类时,由于Base(long, typename long::type = 0) 显然是一个非法的声明,因此编译器选择使用Base(int)作为基类的构造函数。最终结果就是我们看到的输出了Base(int)。而在新版本中继承构造函数不会注入派生类,所以不存在这个问题,编译器会直接使用派生类的Derived(int)构造函数构造对象。