继承构造函数

继承构造函数是C++11 引入的一个语法特性 - 解决了在类继承结构中 派生类重复定义基类构造函数 的繁琐问题

为什么引入?

  • 减少重复代码, 避免手动转发
  • 提高代码的表达能力

一、基础用法和场景

复用基类的构造函数

继承构造函数这个特性引入之前, 即使基类和派生的构造函数形式没有任何区别, 也需要重新定义, 这不仅造成了一定程度的代码重复, 而且也不够简洁。例如, 下面的MyObject就对每个Base中的构造函数做了重新实现

class ObjectBase {
    //...
public:
    ObjectBase(int) {}
    ObjectBase(double) {}
};

class MyObject : public ObjectBase {
public:
    MyObject(int x) : ObjectBase(x) {}
    MyObject(double y) : ObjectBase(y) {}
    //...
};

而用这个特性, 可以通过using ObjectBase::ObjectBase;直接继承基类中的构造函数, 避免这个手动转发的过程

class MyObject : public ObjectBase {
public:
    using ObjectBase::ObjectBase;
    //...
};

这里需要注意的是, 构造函数继承 的编译期隐式代码生成, 不仅仅是对构造函数的"单纯"复制, 而且在派生类中还有类似"自动重命名的效果 ObjectBase -> MyObject "。即:

class MyObject : public ObjectBase {
public:
    // 可能的生成代码
    MyObject(int x) : ObjectBase(x) {}
    MyObject(double y) : ObjectBase(y) {}
};

临时扩展功能用于测试

对一些类型做测试或调试时, 我们常常期望可以使用像to_string()之类的一些接口。如果在不方便直接修改源代码的情况下, 就可以使用 继承构造函数 的性质创建一个"具有一样接口"的新类型, 并追加一些方便调试的接口函数, 从而在有更方便的调试函数下实现间接测试。例如下面有个Student类:

class Student {
protected:
    //...
    double score;
public:
    string id;
    string name;
    uint age;

    Student(string id, string name);
    Student(string id, string name, uint age);
    Student(string id, ...);
};

在对其测试时, 通过实现StudentTest并增加一些辅助测试的函数, 这样更方便测试代码的编写。

class StudentTest : public Student {
public:
    using Student::Student;

    std::string to_string() const {
        return "{ id: " + id + ", name: " + name
            + ", age: " + std::to_string(age) + " }";
    }

    void dump() const { /* 一些成绩细节 ... */ }
    void assert_valid() const {
        assert(score >= 0 && score <= 100);
        // ...
    }
};

其中需要注意的是, 在继承Student的同时, 也继承了构造函数。所以, 他们具有相同的内部布局、构造方式及接口, 从而实现了:

  • 保证了, 使用上的一致性和间接测试的有效性
  • 不修改源码, 做了测试相关功能的扩展, 更方便代码的测试
  • 相对外部为其编写测试函数, 能访问到被保护的数据成员(一般建议以只读方式访问)

其实, 对于很多 "不改变类数据结构的前提下, 来扩展只读行为或工具函数的很多场景", 继承构造函数都用发挥其作用

泛型装饰器和行为约束

继承构造函数不仅可以用于普通的继承中, 他还可以用于模板类型。例如, 下面定义的NoCopy中, 使用了using T::T对泛型T中的构造函数做继承。他的作用是在不改变目标对象的内存布局和使用接口下, 做一定的行为约束

template <typename T>
class NoCopy : public T {
public:
    using T::T;

    NoCopy(const NoCopy&) = delete;
    NoCopy& operator=(const NoCopy&) = delete;
    // ...
};

在一些模块或场景中, 我们期望再对象创想创建后, 不能再复制的方式创建其他对象时, 就可以在定义时使用这个NoCopy装饰器/包装器, 通过包装器中的delete显示告诉编译器删除了拷贝构造拷贝赋值, 也意味着对象不在拥有拷贝语义。例如:

class Point {
    double mX, mY;
public:
    Point() : mX { 0 }, mY { 0 } { }
    Point(double x, double y) : mX { x }, mY { y } { }

    string to_string() const {
        return "{ " + std::to_string(mX)
            + ", " + std::to_string(mY) + " }";
    }
};

Point p1(1, 2);
NoCopy<Point> p2(2, 3);

这个时候p1p2在接口的使用上都是一样的, 但是p2相对p1就少了可拷贝的属性

p1.to_string(); // ok
p2.to_string(); // ok

auto p3 = p1; // ok (拷贝构造)
auto p4 = p2; // error (不能拷贝)

二、注意事项

优先考虑继承还是组合

由于本章是介绍继承构造函数的特性和使用方式, 它是和继承性质绑定的。所以, 从实现上是倾向用继承的方式来实现的。 但是从于目标功能上考虑, 往往使用继承和组合都是可以实现的, 他们更偏向是手段而不是目的, 所以选择需要结合具体的应用场景。

例如, 对于一些测试环境, 或仅功能函数扩展, 无数据结构变动的场景下, 使用继承配合继承构造函数是比较方便的, 还可以避免大量的函数转发。但是, 对于一些 要对少量特定接口做"拦截"或较复杂的场景, 现在(2025)主流是更倾向用组合代替继承的

  • 复杂场景或要加一个中间层做特殊处理 -> 一般组合优于继承
  • 简单功能扩展, 且需保留接口使用的一致 -> 一般继承优于组合

三、练习代码

练习代码主题

练习代码自动检测命令

d2x checker inherited-constructors

四、其他