C++11继承构造函数与委托构造函数详解

 
Category: C_C++

写在前面

看完了深入理解C++11的继承构造函数和委托构造函数部分, 更是对C++的新特性有了一些理解和认识, 下面来总结下.

测试环境: MacOS arm64 clang 15.0.6 llvm(with brew)

继承构造

继承构造函数用于解决基类中含有多个构造函数而派生类需要使用这些继承下来的构造函数的情况, 通过using关键字来实现简洁的函数继承.

例1

在新特性发布之前, 只能像下面这样完成构造函数的继承.

#include <iostream>
using namespace std;
struct A {
    A() { cout << "A::A()\n"; }
    A(int i) { cout << "A::A(int)\n"; }
};
struct B : A {
    B() { cout << "B::B()\n"; }
    /* B(int i) : d(i) { cout << "B::B(int)\n"; } */
    B(int i) : A(i), d(i) { cout << "B::B(int)\n"; } // 先初始化A再构造B
    int d;
};
void t1() {
    B b;
    /* A::A() */
    /* B::B() */
}
void t2() {
    B b(10);
    /* A::A(int) */
    /* B::B(int) */
}
int main(int argc, char *argv[]) {
    /* t1(); */
    t2();
    return 0;
}

上面的示例的使用方法是: 在初始化基类A的同时初始化成员d, 但是当基类构造函数不止一个的时候, 对应的派生类的构造函数也需要分别写出构造函数, 比较繁琐. 例如下面这样:

例2

#include <iostream>
using namespace std;
struct A {
    A(int i) { cout << "A(int)\n"; }
    A(double d, int i) { cout << "A(double,int)\n"; }
};

struct B : A {
    B(int i) : A(i) { cout << "B(int):A\n"; }
    B(double d, int i) : A(d, i) { cout << "B(double,int):A\n"; }
};

void t1() {
    B b(1);
    B bb(1.1, 1);
    /* A(int) */
    /* B(int):A */
    /* A(double,int) */
    /* B(double,int):A */
}
int main(int argc, char *argv[]) {
    t1();
    return 0;
}

可以看出构造顺序是先调用基类再调用派生类.

例3

此时使用using声明, 可以大大简化代码: (以成员函数为例)

#include <iostream>
using namespace std;

struct Base {
    void f(double i) { cout << "Base:" << i << endl; }
};

struct Derived : Base {
    using Base::f;
    void f(int i) { cout << "Derived:" << i << endl; }
};

int main() {
    Base b;
    b.f(4.5); // Base:4.5
    Derived d;
    d.f(4.5); // Base:4.5
    d.f(5);   // Derived:5
}

例4:构造函数继承使用using

#include <iostream>
using namespace std;

struct A {
    A() { cout << "A::A()\n"; }
    A(int i) { cout << "A::A(int)\n"; }
    A(double d, int i) { cout << "A::A(double, int)\n"; }
    A(float f, int i, const char* c) {}
    // ...
};
struct B : A {
    using A::A; // 继承构造函数
    // ...
    virtual void ExtraInterface() {}
};

void t1() {
    A a(1);
    B b(1.2, 1);
    /* A::A(int) */
    /* A::A(double, int) */
}
int main(int argc, char* argv[]) {
    t1();
    return 0;
}

不仅简化了代码, 同时还能发现, 代码中派生类的默认构造函数成为隐式声明了, 也就是说如果一个继承构造函数不被调用, 那么编译器不会为其产生相关的代码, 更节省了代码空间.

例5

#include <iostream>
using namespace std;
struct A {
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};
struct B : A {
    using A::A;
    int d{0};
};
int main() {
    B b(356);            // b.d被初始化为0
    cout << b.d << endl; // 0
}

成员变量类内初始化, 用来解决一些继承构造函数无法初始化派生类成员的问题.

例6

针对基类构造函数参数存在默认值的情况, 此时派生类中继承下来的构造函数中参数默认值不会被继承.

默认值导致基类产生多个构造函数版本, 这些函数版本都会被派生类继承.

下面的代码展示了有两个默认参数的基类出现的四种可能的继承构造函数的情况.

#include <iostream>
using namespace std;

struct A {
    A(int = 3, double = 2.4) { cout << "use A::A\n"; }
};
struct B : A {
    using A::A;
};

void t1() {
    A a1(3, 2.3);
    A a2(12);
    A a3(a1); // default copy ctor, do not use A::A
    A a4;     // default ctor
}

void t2() {
    B b1(3, 2.3);
    B b2(12);
    B b3(b1); // default copy ctor, not inherit
    B b4;     // default ctor
}

int main(int argc, char *argv[]) {
    t1();
    /* t2(); */
    return 0;
}

例7

下面这种情况导致了继承构造函数冲突, 这通常发生在派生类拥有多个基类的时候.

多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名/参数(即, 函数签名)都相同.

这种情况可以通过显式定义继承类中冲突的构造函数来阻止隐式生成相应的继承构造函数.

#include <iostream>
using namespace std;

struct A {
    A() { cout << "A::A()\n"; }
    A(int) { cout << "A::A(int)\n"; }
};
struct B {
    B() { cout << "B::B()\n"; }
    B(int) { cout << "B::B(int)\n"; }
};

struct C : A, B {
    using A::A;
    using B::B;
    C(int a) { cout << "C::C(int)\n"; }
};
void t1() {
    C c; // only for clang
    /* A::A() */
    /* B::B() */
}

void t2() {
    C c(1); //
    /* A::A() */
    /* B::B() */
    /* C::C(int) */
}
int main(int argc, char *argv[]) {
    /* t1(); */
    t2();
    return 0;
}

例8

一旦使用了继承构造函数, 编译器就不会再为派生类生成默认构造函数了, 所以下面代码中, B b;将不能通过编译.

#include <iostream>
using namespace std;


struct A {
    A(int) { cout << "use A::A(int)\n"; }
};

struct B : A {
    using A::A;
};


int main(int argc, char *argv[]) {
    /* B b1;    // error */
    B b2(1); // use A::A(int)

    return 0;
}

总结

使用using, 注意基类的多个构造函数的继承情况, 以及默认参数的情况.

委托构造

目的: 减少写构造函的时间, 通过委派其他构造函数使编写多构造函数的类更加方便.

例1: 冗余的实现

#include <iostream>
using namespace std;

class Info {
public:
    Info() : type(1), name('a') { InitRest(); }
    Info(int i) : type(i), name('a') { InitRest(); }
    Info(char e) : type(1), name(e) { InitRest(); }

private:
    void InitRest() { cout << "call InitRest()\n"; }
    int type;
    char name;
    // ...
};

void t1() { Info i1; }

int main(int argc, char *argv[]) {
    t1();
    return 0;
}

例2: 通过一致初始化实现

就地初始化.

#include <iostream>
using namespace std;
class Info {
public:
    Info() { InitRest(); }
    Info(int i) : type(i) { InitRest(); }
    Info(char e) : name(e) { InitRest(); }

private:
    void InitRest() { cout << "call InitRest()\n"; }
    int type{1};
    char name{'a'};
    // ...
};
// 编译选项:g++ -c 3-2-1.cpp
void t1() { Info i1; }

int main(int argc, char *argv[]) {
    t1();
    return 0;
}

有一种比较Hack的方法:

#include <iostream>
using namespace std;
class Info {
public:
    Info() { InitRest(); }
    Info(int i) {
        new (this) Info();
        type = i;
    }
    Info(char e) {
        new (this) Info();
        name = e;
    }
    /* Info(int i) : type(i) { InitRest(); } */
    /* Info(char e) : name(e) { InitRest(); } */

private:
    void InitRest() { cout << "call InitRest()\n"; }
    int type{1};
    char name{'a'};
    // ...
};
void t1() { Info i1; }

int main(int argc, char *argv[]) {
    t1();
    return 0;
}

通过placement new强制在本对象地址(this所指对象的地址)上调用默认构造来实现, 属于比较高级的用法.

例3: 使用委托构造函数

委派构造委派目标构造函数进行构造.

#include <iostream>
using namespace std;
/* 在初始化列表中调用“基准版本”的构造函数为委派构造函数(delegating
 * constructor),而被调用的“基准版本”则为目标构造函数(target constructor) */

class Info {
public:
    Info() { InitRest(); } // 目标ctor
    /* Info(int i) : Info(), type(i) {} */
    // error: an initializer for a delegating constructor must appear alone
    Info(int i) : Info() { type = i; }  // delegating ctor
    Info(char e) : Info() { name = e; } // delegating ctor
    void print() { cout << type << " " << name << endl; }

private:
    void InitRest() {
        type += 1;
        cout << "call InitRest()\n";
    }
    int type{1};
    char name{'a'};
    // ...
};
int main(int argc, char *argv[]) {
    Info i1;
    i1.print();
    /* call InitRest() */
    /* 1 a */
    Info i2(3); // 在InitRest()中变成4之后,在委派构造中又被赋值为3,
                // 这也能说明目标构造先于委派构造执行
    i2.print();
    /* call InitRest() */
    /* 3 a */
    return 0;
}

简洁明了.

但是要注意, 委派构造和变量赋值不能在初始化列表中同时出现, 所以如果有委派构造函数要给变量赋初值, 初始化的代码就要放在函数体中.

例4

这个例子可以改进上面提到的不能同时出现问题:

#include <iostream>
using namespace std;
class Info {
public:
    Info() : Info(1, 'a') {}
    Info(int i) : Info(i, 'a') {}
    Info(char e) : Info(1, e) {}
    void print() { cout << type << " " << name << endl; }

private:
    // 私有目标构造函数, 可以不需要InitRest()
    Info(int i, char e) : type(i), name(e) {
        /* type += 1; */
        cout << "target ctor..\n";
    }
    int type;
    char name;
    // ...
};
void t1() {
    Info i1;
    i1.print();
    Info i2(2);
    i2.print();
    Info i3('x');
    i3.print();
    /* target ctor.. */
    /* 1 a */
    /* target ctor.. */
    /* 2 a */
    /* target ctor.. */
    /* 1 x */
    // 在C++11中,目标构造函数的执行总是先于委派构造函数
    //  if add `type+=1`:
    /* target ctor.. */
    /* 2 a */
    /* target ctor.. */
    /* 3 a */
    /* target ctor.. */
    /* 2 x */
}

int main(int argc, char *argv[]) {
    t1();
    return 0;
}

例5

链状委托构造:

#include <iostream>
using namespace std;
class Info {
public:
    Info() : Info(1) {}           // 委派构造函数
    Info(int i) : Info(i, 'a') {} // 既是目标构造函数,也是委派构造函数
    Info(char e) : Info(1, e) {} // 委派构造函数
    void print() { cout << type << " " << name << endl; }

private:
    Info(int i, char e) : type(i), name(e) { type += 1; } // 目标构造函数
    int type;
    char name;
    // ...
};
void t1() {
    Info i1;
    i1.print();
    Info i2(2);
    i2.print();
    Info i3('x');
    i3.print();
    /* 1 a */
    /* 2 a */
    /* 1 x */
}

int main(int argc, char *argv[]) {
    t1();
    return 0;
}

但是要注意不能形成委托环(无限递归), 例如:

struct Rule2 {
    int i, c;
    Rule2() : Rule2(2) {}
    Rule2(int i) : Rule2('c') {}
    Rule2(char c) : Rule2(2) {}
    /* error: constructor for 'Rule2' creates a delegation cycle [-Wdelegating-ctor-cycles] */
};

例6:应用1

委派构造的实际应用: 使用构造模板函数产生目标构造函数.

#include <iostream>
#include <list>
#include <vector>
#include <deque>
using namespace std;

class TDConstructed {
private:
    // 构造模板函数, 在下面的两个委派构造函数委托时, 该模板函数被实例化
    // 比罗列多种类型的构造函数方便, 委托构造也使构造函数泛型编程成为可能
    template <class T>
    TDConstructed(T first, T last) : l(first, last) {
        cout << "use template ctor\n";
    }
    list<int> l;

public:
    TDConstructed(vector<short> &v) : TDConstructed(v.begin(), v.end()) {}
    TDConstructed(deque<int> &d) : TDConstructed(d.begin(), d.end()) {}
};

void t1() {
    vector<short> v1;
    TDConstructed t1(v1);

    deque<int> d1;
    TDConstructed t2(d1);
    /* use template ctor */
    /* use template ctor */
}

int main(int argc, char *argv[]) {
    t1();
    return 0;
}

例7: 应用2

异常处理.

#include <iostream>
using namespace std;
class DCExcept {
public:
    // 委派构造函数中使用try, 可以捕获到目标构造中抛出的异常
    DCExcept(double d) try : DCExcept(1, d) {
        // 此处并没有执行
        cout << "Run the body." << endl;
        // 其他初始化
    } catch (...) {
        cout << "caught exception." << endl;
    }

private:
    // 目标构造函数中throw
    DCExcept(int i, double d) {
        cout << "going to throw!" << endl;
        throw 0;
    }
    int type;
    double data;
};
int main() { DCExcept a(1.2); }
/* going to throw! */
/* caught exception. */
/* libc++abi: terminating with uncaught exception of type int */

总结

委托构造能够简化代码, 同时可以支持泛型初始化, 具有积极意义.