本文首先介绍了在委派构造函数提出之前类成员构造所面临的问题,再结合实例介绍了委派构造函数的用法,并说明了使用委派构造函数特性时需要注意的语言点。在本文最后还介绍了 IBM XL C/C++编译器用来控制该特性的编译选项。
特性背景
在 C++98 中,如果一个类有多个构造函数且要实现类成员构造,这些构造函数通常要包含基本相同的类成员构造代码。在最坏的情况下,相同的类成员构造语句被拷贝粘贴在每一个构造函数中,请参考下面的例子:
清单 1.程序源代码 (ctorini.cpp)
1 2 3 4 5 6 7 8 9 10 11 | class A{ public: // 构造函数 A(), A(int i)和 A(int i, int j)有相同的函数体 A(): num1(0), num2(0) {average=(num1+num2)/2;} A(int i): num1(i), num2(0) {average=(num1+num2)/2;} A(int i, int j): num1(i), num2(j) {average=(num1+num2)/2;} private: int num1; int num2; int average; }; |
上例中的三个构造函数有完全相同的函数体。重复的代码会增大维护的工作量和难度。如果程序员想增加更多的类成员构造语句或者修改已有类成员的类型,他需要在三个构造函数中做三次完全相同的改动。为了避免代码重复,一些程序员将公有的类成员构造代码移到一个类成员函数里,意图让构造函数通过调用该成员函数完成类成员构造。清单 1 的程序根据这个思路,可以修改如下:
清单 2.程序源代码 (memfuncini.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 | class A{ public: // 构造函数 A(), A(int i)和 A(int i, int j)意图通过调用成员函数 int()来完成类成员构造 A(): num1(0), num2(0) {init();} A(int i): num1(i), num2(0) {init();} A(int i, int j): num1(i), num2(j) {init();} private: int num1; int num2; int average; void init(){ average=(num1+num2)/2;}; }; |
清单 2 的程序消除了代码重复的问题,但又引入了以下的新问题:
- 其它的成员函数可能会不小心调用到 init()函数,这样会造成不可预期的结果。
- 当编译器处理成员函数调用的时候,全部的类成员其实都已经了完成构造。也就是说,想在类成员函数里实现类成员构造是行不通的。
委派构造函数的提出和基本用法
基于以上类成员构造所面临的问题,C++11 标准提出了委派构造函数新特性。利用这个特性,程序员可以将公有的类成员构造代码集中在某一个构造函数里,这个函数被称为目标构造函数。其他构造函数通过调用目标构造函数来实现类成员构造,这些构造函数被称为委派构造函数。在该新特性提出之前,构造函数是不能显式被调用的,委派构造函数打破了这一限制。让我们用下面例子来说明该新特性是如何使用的:
清单 3.程序源代码 (delegatingctor1.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class A{ public: // A(int i)为 A()的委派构造函数 A(): A(0){} // A(int i, int j)为 A(int i)的委派构造函数 A(int i): A(i, 0){} // 委派构造链为 A()->A(int i)->A(int i, int j) A(int i, int j) { num1=i; num2=j; average=(num1+num2)/2; } private: int num1; int num2; int average; }; |
可以看到,在构造函数 A()的初始化列表里,程序调用了 A(0), 这就是委派构造函数的语法。 我们称 A(int i)为 A()的目标构造函数,而 A()为 A(int i)的委派构造函数。同理,A(int i, int j)为 A(int i)的目标构造函数,而 A(int i) 为 A(int i, int j)的委派构造函数。在利用了委派构造函数后,整个程序变得更加的清楚和简洁。目标构造函数和委派构造函数跟其他普通的构造函数一样有相同的接口和语法,它们并没有特殊的处理和标签。从这个例子还可以看到,一个委派构造函数可以是另一个委派构造函数的目标构造函数,委派构造函数和目标构造函数是相对而言的。目标构造函数是通过重载和类参数推导准则而选定的。
在委派过程中,当目标构造函数函数执行完毕后,委派构造函数继续执行它自己函数体内的其他语句。请参考以下的例子:
清单 4.程序源代码 (delegatingctor2.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include < iostream > using namespace std; class A{ public: A(): A(0){ cout << "In A()" << endl;} A(int i): A(i, 0){cout << "In A(int i)" << endl;} A(int i, int j){ num1=i; num2=j; average=(num1+num2)/2; cout << "In A(int i, int j)" << endl; } private: int num1; int num2; int average; }; int main(){ A a; return 0; } |
该例子的输出为:
1 2 3 | In A(int i, int j) In A(int i) In A() |
委派构造函数的异常处理
当目标构造函数抛出异常时,该异常会被委派构造函数中的 try 模块抓取到。并且在这种情况下,委派构造函数自己函数体内的代码就不会被执行了。
在下面的例子中,构造函数 A(int i, int j)抛出一个异常,该异常依次被委派构造函数 A(int i)和 A()抓取到,且 A(int i)和 A()的函数体没有被执行。
清单 5.程序源代码 (exception1.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | #include < iostream > using namespace std; class A{ public: A(); A(int i); A(int i, int j); private: int num1; int num2; int average; }; A:: A()try: A(0) { // A()函数体不会被执行到 cout << "A() body"<< endl; } catch(...) { cout << "A() catch"<< endl; } A::A(int i) try : A(i, 0){ // A(int i)函数体不会被执行到 cout << "A(int i) body"<< endl; } catch(...) { cout << "A(int i) catch"<< endl; } A::A(int i, int j) try { num1=i; num2=j; average=(num1+num2)/2; cout << "A(int i, int j) body"<< endl; // 抛出异常 throw 1; } catch(...) { cout << "A(int i, int j) catch"<< endl; } int main(){ try{ A a; cout << "main body"<< endl; } catch(...){ cout << "main catch"<< endl; } return 0; } |
该例的输出为:
1 2 3 4 5 | A(int i, int j) body A(int i, int j) catch A(int i) catch A() catch main catch |
当委派构造函数抛出异常时,系统会自动调用目标构造函数内已经构造完成的对象的析构函数。在下面的例子中,目标构造函数 A(int i, int j)完成了对象 a 的构造。它的委派构造函数 A(int i)在执行时抛出了一个异常,此时对象 a 马上被析构,且 A(int i)的委派构造函数 A()的函数体不再被编译器执行,这和上例中所描述的原则是一致的。
清单 6.程序源代码 (exception2.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #include < iostream > using namespace std; class A{ public: A(); A(int i); A(int i, int j); ~A(); private: int num1; int num2; int average; }; A::A(): A(0){ // A()函数体不会被执行到 cout << "A()body" << endl; } A::A(int i) try : A(i, 0){ cout << "A(int i) body"<< endl; // 抛出异常,对象 a 将被析构 throw 1; } catch(...) { cout << "A(int i) catch"<< endl; } A::A(int i, int j){ num1=i; num2=j; average=(num1+num2)/2; cout << "A(int i, int j) body"<< endl; } A::~A(){ cout << "~A() body"<< endl; } int main(){ A a; return 0; } |
该例的输出为:
1 2 3 4 | A(int i, int j) body A(int i) body ~A() body A(int i) catch |
委派构造函数和泛型编程
综上所述,委派构造函数可以使程序员规避构造函数里重复的代码。委派构造函数还有一个很实际的应用:它使得构造函数的泛型编程变得更加容易,请参考以下的例子:
清单 7.程序源代码 (generic.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include < iostream > using namespace std; template< typename T> class A{ public: A(int i): A(i, 0){} A(double d): A(d, 0.0){} // 函数模板 A(T i, T j) { num1=i; num2=j; average=(num1+num2)/2; cout << "average=" << average << endl; } private: T num1; T num2; T average; }; int main(){ A<int> a_int(1); A< double > a_double(1.0); } |
该例的输出为:
1 2 | average=0 average=0.5 |
在这个例子中,目标构造函数为函数模板,它在被委派构造函数调用的时候才被实例化。这样的用法十分方便,程序员不需要再书写不同类型的目标构造函数了。
委派构造函数的使用限制
委派构造函数特性简单好用,非常利于新手的学习和掌握,但是在这个特性的使用中也需要注意以下的限制。
如清单 3 中的程序所示, 一个类中的构造函数可以形成一个委派链。但是程序员应该避免委派环的出现。在下面的例子中,构造函数 A(), A(int i), 和 A(int i, int j)形成了一个委派环,这样的程序是有语法错误的。
清单 8.程序源代码 (delegatingcircle.cpp)
1 2 3 4 5 6 7 8 9 10 11 | class A{ public: //形成了委派构造环 A()->A(int i)->A(int i, int j)->A() A(): A(0){} A(int i): A(i, 0){} A(int i, int j): A(){} private: int num1; int num2; int average; }; |
委派构造函数不能在初始化列表中包含成员变量的初始化.也就是说,一个构造函数不能在初始化列表中既初始化成员变量,又委派其他构造函数完成构造。在下面的例子中,A()是 A(int i)的委派构造函数,并且在同时又初始化了类成员变量 average。这样的程序是不被允许的。
清单 9.程序源代码 (delegatingini.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class A{ public: //错误,A()不能同时委派和初始化类成员 A(): A(0), average(0){} A(int i): A(i, 0){} A(int i, int j) { num1=i; num2=j; average=(num1+num2)/2; } private: int num1; int num2; int average; }; |
编译选项
IBM XL C/C++编译器已经实现了委派构造函数这个新特性。在默认情况下,该特性是关闭的,如果程序员需要打开该特性,可以设置下面任意一个编译选项:
- -qlanglvl=[no]delegatingctors
默认值: -qlanglvl=nodelegatingctors
用法: -qlanglvl=delegatingctors 打开委派构造函数特性,
-qlanglvl=nodelegatingctors 关闭委派构造函数特性。
- -qlanglvl=extended0x
该编译选项控制所有的 C++11 特性,当程序员设置了该编译选项时,所有的 IBM XL C/C++编译器实现的 C++11 特性都会被打开,包括委派构造函数特性。
除了 IBM XL C/C++编译器,其他很多编译器也实现了该特性,比如 clang,GNU C++编译器等。
结束语
本文详细的介绍了 C++11 标准新特性-委派构造函数。利用该特性,可以提高程序的可读性和可维护性,减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写将更加容易。但委派构造函数的调用是需要系统开销的,如果可以用带默认参数的构造函数实现和委派构造函数相同的功能,在不影响程序可读性和可维护性的前提下,更推荐程序员使用前者。