委派构造函数(delegating constructor)
委派构造函数是在C++11标准新引入的一项改进,主要目的是为了减少程序员在构造函数方面花费的工作。通过委派构造函数使之对于构造函数的编写更加简单。
场景
在下面这段代码中,定义了一个Class Info。这个类中有俩个成员变量,三个构造函数,但是三个构造函数都共同调用了InitRest方法。所以我们看到各个构造函数都有或多或少的相似之处。为此C++11引入了委派构造函数。
class Info { public: Info() :type(1), name('a') { InitRest(); } Info(int i) :type(i), name('a') { InitRest(); } Info(char ch) :type(1), name(ch) { InitRest(); } private: void InitRest(){ /* 其他初始化 */ } int type; char name; };
使用成员初始化方式进行优化
这里其实我们可以通过成员初始化方式,可以使得初始化列表变得简洁化。
class Info { public: Info() { InitRest(); } Info(int i) :type(i) { InitRest(); } Info(char ch) :name(ch) { InitRest(); } private: void InitRest() { /* 其他初始化 */} int type {1}; char name {'a'}; };
使用委派构造函数法方式进行优化
首先我们需要了解一下概念:委派构造函数说白了就是将构造的任务分派给一个目标构造函数来完成。
委派构造函数:初始化列表中调用“基准版本”的构造函数就是委派构造函数。
目标构造函数:被调用“基准版本”构造函数就是目标构造函数。
需要注意的是委派构造函数不能使用初始化列表 初始化成员变量
// 通过委派构造函数进行优化 class Info { public: Info() { InitRest(); } Info(int i) : Info() { type = i; } Info(char ch) : Info() // 委派构造函数不能使用初始化列表 初始化成员变量 { name = ch; } private: void InitRest() { /* 其他初始化 */} int type{ 1 }; char name{ 'a' }; };
然而委派构造函数也不全是优点,构造函数中委派构造和初始化列表是互斥的,所以变量初始化就只能放在函数体内了。
但是初始化列表的调用总是优先于构造函数,所以这就可能会给自己挖坑
。
委派构造函数与初始化列表我全都要
为了使用委派构造时还能使用初始化列表,我们可以定义一个private 的目标构造函数,并将初始化列表放在这个private的目标构造函数中,这样其他委派构造函数就可以通过委派这个目标构造来实现构造的功能。
// 委派构造函数 + 初始化列表 class Info { public: Info() :Info(1, 'a') {} Info(int i) : Info(i, 'a') {} Info(char ch) : Info(1, ch) {} private: Info(int i, char ch) :type(i), name(ch) {/*其他初始化信息*/} int type{ 1 }; char name{ 'a' }; };
对上述俩种委派构造进行分析
上述俩种委派构造看似优化了代码,但是却存在着看不到坑。
下面这段测试就会导致预期与实际不否。(注:为了方便测试,将成员变量的作用域设为public)
这段测试看似结果正确,但真就是我们详细的那样吗?
分析:
首先Info(int)
会委派Info()
,然后调用InitRest
方法对type
进行加1
,此时type=2
,然后在执行Info(int)
构造函数内部,这时type=5
。可见这个5
”来之不易“啊。
所以这俩种委派构造在这种场景下都不算一个好的代码。
链状委派构造
在构造函数较多情况下,会形成链状的委派构造关系。
// 链状委派构造函数 class Info { public: Info() :Info(1) {} Info(int i) : Info(i, 'a') {} Info(char ch) : Info(1, ch) {} private: Info(int i, char ch) :type(i), name(ch) {} public: int type{ 1 }; char name{ 'a' }; };
在上面这段代码种,Info()委派Info(int)进行构造工作,然后Info(int)委派Info(int, char)。这就是链状委派。
委派环(delegation cycle)
链状委派没有任何问题,但是形成环形链状委派也叫委派环(delegation cycle)就会导致构造死循环。
例如下面这个例子Info(int)和Info(char)相互委派。
// 委派环 class Info { public: Info() :Info(1) {} Info(int i) : Info('c') {} Info(char ch) : Info(2) {} private: int type{ 1 }; char name{ 'a' }; };
在VS2022就会提醒你,这个构造函数直接或者间接委派给自己,Info的构造函数创建了一个委派周期。
所以一定要注意委派环。
使用场景
构造模板
使用构造模板函数产生目标构造函数。我们创建了一个class Test,其中有一个private的构造模板,还有俩个委派构造函数,这样就可以实现动态进行构造不同的容器了。有泛式编程那味了。
#include <list> #include <vector> #include <deque> using namespace std; class Test { private: template<class T> Test(T first, T last):l(first, last){} public: Test(vector<int>& v) :Test(v.begin(), v.end()){} Test(deque<int>& d) : Test(d.begin(), d.end()){} private: list<int> l; };
异常处理
在异常处理中,我们可以在委派构造函数中使用try,然后再目标构造函数中抛出异常,这将可以在委派构造函数中被捕获。
class Test { public: Test(double d) try : Test(1, d) { cout << "Run the body" << endl; } catch (...) { cout << "caught exception." << endl; } private: Test(int i, double d) { cout << "going to throw" << endl; throw 0; } int type; double data; }; int main() { Test info(5); return 0; }
运行结果: