现代C++新特性 委托构造函数

现代C++新特性 委托构造函数,第1张

   文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载 

1.冗余的构造函

一个类有多个不同的构造函数在C++中是很常见的,例如:

class X {
public:
    X() : a_(0), b_(0.)
    {
        CommonInit(); 
    }
    X(int a) : a_(a), b_(0.) 
    { 
        CommonInit(); 
    }
    X(double b) : a_(0), b_(b)
    { 
        CommonInit();
    }
    X(int a, double b) : a_(a), b_(b) 
    {
        CommonInit(); 
    }
private:
    void CommonInit() {}
    int a_;
    double b_;
};

虽然这段代码在语法上没有任何问题,但是构造函数包含了太多重复代码,这使代码的维护变得困难。首先,类X需要在每个构造函数的初始化列表中初始化构造所有的成员变量,这段代码只有两个数据成员,而在现实代码编写中常常会有更多的数据成员或者更多的构造函数,那么在初始化列表中会有更多的重复内容,非常不利于代码的维护。其次,在构造函数主体中也有相同的情况,一旦类的构造过程需要依赖某个函数,那么所有构造函数的主体就需要调用这个函数,在例子中这个函数就是CommonInit。也许有读者会提出将数据成员的初始化放到CommonInit函数里,从而减轻初始化列表代码冗余的问题,例如:

class X1 {
public:
    X1() 
    { 
        CommonInit(0, 0.); 
    }
    X1(int a) 
    {
        CommonInit(a, 0.); 
    }
    X1(double b) 
    {
        CommonInit(0, b); 
    }
    X1(int a, double b) 
    {
        CommonInit(a, b); 
    }
private:
    void CommonInit(int a, double b)
    {
        a_ = a;
        b_ = b;
    }
    int a_;
    double b_;
};

以上代码在编译和运行上都没有问题,因为类X1的成员变量都是基本类型,所以在构造函数主体进行赋值也不会有什么问题。但是,如果成员函数中包含复杂的对象,那么就可能引发不确定问题,最好的情况是只影响类的构造效率,例如:

class X2 {
public:
    X2()
    {
        CommonInit(0, 0.);
    }

    X2(int a)
    {
        CommonInit(a, 0.);
    }

    X2(double b)
    {
        CommonInit(0, b);
    }

    X2(int a, double b)
    {
        CommonInit(a, b);
    }

private:
    void CommonInit(int a, double b)
    {
        a_ = a;
        b_ = b;
        c_ = "hello world";
    }   
    
    int a_;
    double b_;
    string c_;
};

在上面的代码中,string类型的对象c_看似是在CommonInit函数中初始化为hello world,但是实际上它并不是一个初始化过程,而是一个赋值过程。因为对象的初始化过程早在构造函数主体执行之前,也就是初始化列表阶段就已经执行了。所以这里的c_对象进行了两次 *** 作,一次为初始化,另一次才是赋值为hello world,很明显这样对程序造成了不必要的性能损失。另外,有些情况是不能使用函数主体对成员对象进行赋值的,比如禁用了赋值运算符的数据成员。

当然读者还可能会提出通过为构造函数提供默认参数的方法来解决代码冗余的问题,例如:

class X3 {
public:
    X3(double b) : a_(0), b_(b)
    {
        CommonInit();
    }

    X3(int a = 0, double b = 0.) : a_(a), b_(b)
    {
        CommonInit();
    }
private:
    void CommonInit() {}
    int a_;
    double b_;
};

这种做法的作用非常有限,可以看到上面这段代码,虽然通过默认参数的方式优化了两个构造函数,但是对于X3(double b)这个构造函数依然需要在初始化列表中重复初始化成员变量。另外,使用默认参数稍不注意就会引发二义性的问题,例如:

class X4 {
public:
    X4(int c) : a_(0), b_(0.), c_(c)
    {
        CommonInit();
    }

    X4(double b) : a_(0), b_(b), c_(0)
    {
        CommonInit();
    }
    X4(int a = 0, double b = 0., int c = 0) : a_(a), b_(b), c_(c)
    {
        CommonInit();
    }
private:
    void CommonInit() {}
    int a_;
    double b_;
    int c_;
};


int main(int argc, char** argv)
{
    X4 x4(1);
    return 0;
}

以上代码无法通过编译,因为当main函数对x4进行构造时,编译器不知道应该调用X4(int c)还是X4(int a = 0, double b = 0., int c = 0)。所以让构造函数使用默认参数也不是一个好的解决方案。

现在读者可以看出其中的问题了,过去C++没有提供一种复用同类型构造函数的方法,也就是说无法让一个构造函数将初始化的一部分工作委托给同类型的另外一个构造函数。这种功能的缺失就造成了程序员不得不编写重复烦琐代码的困境,更进一步来说它也造成了代码维护性下降。比如,如果想在类X中增加一个数据成员d_,那么就必须在4个构造函数的初始化列表中初始化成员变量d_,修改和删除也一样。

 2.委托构造函数

为了合理复用构造函数来减少代码冗余,C++11标准支持了委托构造函数:某个类型的一个构造函数可以委托同类型的另一个构造函数对对象进行初始化。为了描述方便我们称前者为委托构造函数,后者为代理构造函数(英文直译为目标构造函数)。委托构造函数会将控制权交给代理构造函数,在代理构造函数执行完之后,再执行委托构造函数的主体。委托构造函数的语法非常简单,只需要在委托构造函数的初始化列表中调用代理构造函数即可,例如:

class X {
public:
    X() : X(0, 0.) {}
    X(int a) : X(a, 0.) {}
    X(double b) : X(0, b) {}
    X(int a, double b) : a_(a), b_(b)
    {
        CommonInit();
    }
private:
    void CommonInit() {}
    int a_;
    double b_;
};

可以看到X()、X(int a)、X(double b)分别作为委托构造函数将控制权交给了代理构造函数X(int a, double b)。它们的执行顺序是先执行代理构造函数的初始化列表,接着执行代理构造函数的主体(也就是CommonInit函数),最后执行委托构造函数的主体,在这个例子中委托构造函数的主体都为空。

委托构造函数的语法很简单,不过想合理使用它还需注意以下5点。 1.每个构造函数都可以委托另一个构造函数为代理。也就是说,可能存在一个构造函数,它既是委托构造函数也是代理构造函数,例如:

class X {
public:
    X() : X(0) {}
    X(int a) : X(a, 0.) {}
    X(double b) : X(0, b) {}
    X(int a, double b) : a_(a), b_(b)
    {
        CommonInit();
    }
private:
    void CommonInit() {}
    int a_;
    double b_;
};

在上面的代码中构造函数X(int a),它既是一个委托构造函数,也是X()的代理构造函数。另外,除了自定义构造函数以外,我们还能让特殊构造函数也成为委托构造函数,例如:

class X {
public:
    X() : X(0) {}
    X(int a) : X(a, 0.) {}
    X(double b) : X(0, b) {}
    X(int a, double b) : a_(a), b_(b)
    {
        CommonInit();
    }

    X(const X& other) : X(other.a_, other.b_) {}  // 委托复制构造函数 
private:
    void CommonInit() {}
    int a_;
    double b_;
};

以上代码增加了一个复制构造函数X(const X &other),并且把复制构造函数的控制权委托给了X(int a, double b),而其自身主体不需要执行。

2.不要递归循环委托!这一点非常重要,因为循环委托不会被编译器报错,随之而来的是程序运行时发生未定义行为,最常见的结果是程序因栈内存用尽而崩溃:

class X {
public:
    X() : X(0) {}
    X(int a) : X(a, 0.) {}
    X(double b) : X(0, b) {}
    X(int a, double b) : X()
    {
        CommonInit();
    }
private:
    void CommonInit() {}
    int a_;
    double b_;
};

上面代码中的3个构造函数形成了一个循环递归委托,X()委托到X(int a),X(int a)委托到X(int a, double b),最后X(int a, double b)又委托到X()。请读者务必注意不要编写出这样的循环递归委托代码,因为我目前实验的编译器,默认情况下除了CLang会给出错误提示,MSVC和GCC都不会发出任何警告。这里也建议读者在使用委托构造函数时,通常只指定一个代理构造函数即可,其他的构造函数都委托到这个代理构造函数,尽量不要形成链式委托,避免出现循环递归委托。

3.如果一个构造函数为委托构造函数,那么其初始化列表里就不能对数据成员和基类进行初始化:

class X {
public:
    X() : a_(0), b_(0)
    {
        CommonInit();
    }
    X(int a) : X(), a_(a) {}   // 编译错误,委托构造函数不能在初始化列表初始化成员变量 
    X(double b) : X(), b_(b) {}// 编译错误,委托构造函数不能在初始化列表初始化成员变量 
private:
    void CommonInit() {}
    int a_;
    double b_;
};

在上面的代码中X(int a)和X(double b)都委托了X()作为代理构造函数,但是它们又打算初始化自己所需的成员变量,这样就导致了编译错误。其实这个错误很容易理解,因为根据C++标准规定,一旦类型有一个构造函数完成执行,那么就会认为其构造的对象已经构造完成。将这个规则放在这里来看,委托构造函数将控制权交给代理构造函数,代理构造函数执行完成以后,编译器认为对象已经构造成功,再次执行初始化列表必然会导致不可预知的问题,所以C++标准禁止了这样的语法。

4.委托构造函数的执行顺序是先执行代理构造函数的初始化列表,然后执行代理构造函数的主体,最后执行委托构造函数的主体,例如:

#include  

class X {
public:
    X() : X(0)
    {
        InitStep3();
    }

    X(int a) : X(a, 0.)
    {
        InitStep2();
    }

    X(double b) : X(0, b) {}

    X(int a, double b) : a_(a), b_(b)
    {
        InitStep1();
    }
private:
    void InitStep1()
    {
        cout << "InitStep1()" << endl;
    }

    void InitStep2()
    {
        cout << "InitStep2()" << endl;
    }

    void InitStep3()
    {
        cout << "InitStep3()" << endl;
    }

    int a_;
    double b_;
};

int main(int argc, char** argv)
{
    X x;
    return 0;
}

编译执行以上代码,输出结果如下:

InitStep1() 
InitStep2()
InitStep3()

5.如果在代理构造函数执行完成后,委托构造函数主体抛出了异常,则自动调用该类型的析构函数。这一条规则看起来有些奇怪,因为通常在没有完成构造函数的情况下,也就是说构造函数发生异常,对象类型的析构函数是不会被调用的。而这里的情况正好是一种中间状态,是否应该调用析构函数看似存在争议,其实不然,因为C++标准规定(规则3也提到过),一旦类型有一个构造函数完成执行,那么就会认为其构造的对象已经构造完成,所以发生异常后需要调用析构函数,来看一看具体的例子:

#include  

class X {
public:
    X() : X(0, 0.)
    {
        throw 1;
    }

    X(int a) : X(a, 0.) {}
    X(double b) : X(0, b) {}

    X(int a, double b) : a_(a), b_(b)
    {
        CommonInit();
    }
    ~X()
    {
        cout << "~X()" << endl;
    }
private:
    void CommonInit() {}
    int a_;
    double b_;
};

int main(int argc, char** argv)
{
    try {
        X x;
    }
    catch (…) {
    }
    return 0;
}

上面的代码中,构造函数X()委托构造函数X(int a, double b)对对象进行初始化,在代理构造函数初始化完成后,在X()主体内抛出了一个异常。这个异常会被main函数的try cache捕获,并且调用X的析构函数析构对象。读者不妨自己编译运行代码,并观察运行结果。

​​​​​​​3.委托模板构造函数

委托模板构造函数是指一个构造函数将控制权委托到同类型的一个模板构造函数,简单地说,就是代理构造函数是一个函数模板。这样做的意义在于泛化了构造函数,减少冗余的代码的产生。将代理构造函数编写成函数模板往往会获得很好的效果,让我们看一看例子:

#include 
#include 
#include 

class X {
    template X(T first, T last) : l_(first, last) { }
    list l_;
public:
    X(vector&);
    X(deque&);
};

X::X(vector& v) : X(v.begin(), v.end()) { }
X::X(deque& v) : X(v.begin(), v.end()) { }

int main(int argc, char** argv)
{
    vector a{ 1,2,3,4,5 };
    deque b{ 1,2,3,4,5 };
    X x1(a);
    X x2(b);
    return 0;
}

在上面的代码中template X(T first, T last)是一个代理模板构造函数,X(vector&)和X(deque&)将控制权委托给了它。这样一来,我们就无须编写vector和deque 版本的代理构造函数。后续增加委托构造函数也不需要修改代理构造函数,只需要保证参数类型支持迭代器就行了。

4.​​​​​​​捕获委托构造函数的异常

当使用Function-try-block去捕获委托构造函数异常时,其过程和捕获初始化列表异常如出一辙。如果一个异常在代理构造函数的初始化列表或者主体中被抛出,那么委托构造函数的主体将不再被执行,与之相对的,控制权会交到异常捕获的catch代码块中:

#include 

class X {
public:
    X() try : X(0) {}
    catch (int e) {
        cout << "catch: " << e << endl;
        throw 3;
    }
    X(int a) try : X(a, 0.) {}
    catch (int e) {
        cout << "catch: " << e << endl;
        throw 2;
    }
    X(double b) : X(0, b) {}

    X(int a, double b) : a_(a), b_(b) 
    { 
        throw 1; 
    }
private:
    int a_;
    double b_;
};


int main(int argc, char** argv)
{
    try {
        X x;
    }
    catch (int e) {
        cout << "catch: " << e << endl;
    }
    return 0;
}

编译运行以上代码,输出结果如下:

catch : 1
catch : 2 
catch : 3

由于这段代码是一个链式委托构造,X()委托到X(int a),X(int a)委托到X(int a, double b)。因此在X(int a, double b)发生异常的时候,会以相反的顺序抛出异常。

​​​​​​​5.委托参数较少的构造函数

看了以上各种示例代码,读者是否发现一个特点:将参数较少的构造函数委托给参数较多的构造函数。通常情况下我们建议这么做,因为这样做的自由度更高。但是,并不是完全否定从参数较多的构造函数委托参数较少的构造函数的意义。这种情况通常发生在构造函数的参数必须在函数体中使用的场景。以fstream作为例子:

basic_fstream();
explicit basic_fstream(const char* s, ios_base::openmode mode);

basic_fstream的这两个构造函数,由于basic_fstream(const char * s, ios_base::openmode mode)需要在构造函数体内执行具体打开文件的 *** 作,所以它完全可以委托basic_fstream()来完成一些最基础的初始化工作,最后执行到自己的主体时再打开文件:

basic_fstream::basic_fstream(const char* s, ios_base::openmode mode) : basic_fstream()
{
    if (open(s, mode) == 0) {
        setstate(failbit);
    }  
}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/707589.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-24
下一篇 2022-04-24

发表评论

登录后才能评论

评论列表(0条)

保存