《深入理解C++11:C++ 11新特性解析与应用》——3.2 委派构造函数

3.2 委派构造函数

类别:类作者

与继承构造函数类似的,委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写将更加容易。

首先我们可以看看代码清单3-9中构造函数代码冗余的例子。

在代码清单3-9中,我们声明了一个Info的自定义类型。该类型拥有2个成员变量以及3个构造函数。这里的3个构造函数都声明了初始化列表来初始化成员type和name,并且都调用了相同的函数InitRest。可以看到,除了初始化列表有的不同,而其他的部分,3个构造函数基本上是相似的,因此其代码存在着很多重复。

读者可能会想到2.7节中我们对成员初始化的方法,那么我们用该方法来改写一下这个例子,如代码清单3-10所示。

在代码清单3-10中,我们在Info成员变量type和name声明的时候就地进行了初始化。可以看到,构造函数确实简单了不少,不过每个构造函数还是需要调用InitRest函数进行初始化。而现实编程中,构造函数中的代码还会更长,比如可能还需要调用一些基类的构造函数等。那能不能在一些构造函数中连InitRest都不用调用呢?

答案是肯定的,但前提是我们能够将一个构造函数设定为“基准版本”,比如本例中Info()版本的构造函数,而其他构造函数可以通过委派“基准版本”来进行初始化。按照这个想法,我们可能会如下编写构造函数:

Info() { InitRest(); }
Info(int i) { this->Info(); type = i; }
Info(char e) { this->Info(); name = e; }

这里我们通过this指针调用我们的“基准版本”的构造函数。不过可惜的是,一般的编译器都会阻止this->Info()的编译。原则上,编译器不允许在构造函数中调用构造函数,即使参数看起来并不相同。

当然,我们还可以开发出一个更具有“黑客精神”的版本:

Info() { InitRest(); }
Info(int i) { new (this) Info(); type = i; }
Info(char e) { new (this) Info(); name = e; }

这里我们使用了placement new来强制在本对象地址(this指针所指地址)上调用类的构造函数。这样一来,我们可以绕过编译器的检查,从而在2个构造函数中调用我们的“基准版本”。这种方法看起来不错,却是在已经初始化一部分的对象上再次调用构造函数,因此虽然针对这个简单的例子在我们的实验机上该做法是有效的,却是种危险的做法。

在C++11中,我们可以使用委派构造函数来达到期望的效果。更具体的,C++11中的委派构造函数是在构造函数的初始化列表位置进行构造的、委派的。我们可以看看代码清单3-11所示的这个例子。

可以看到,在代码清单3-11中,我们在Info(int)和Info(char)的初始化列表的位置,调用了“基准版本”的构造函数Info()。这里我们为了区分被调用者和调用者,称在初始化列表中调用“基准版本”的构造函数为委派构造函数(delegating constructor),而被调用的“基准版本”则为目标构造函数(target constructor)。在C++11中,所谓委派构造,就是指委派函数将构造的任务委派给了目标构造函数来完成这样一种类构造的方式。

当然,在代码清单3-11中,委派构造函数只能在函数体中为type、name等成员赋初值。这是由于委派构造函数不能有初始化列表造成的。在C++中,构造函数不能同时“委派”和使用初始化列表,所以如果委派构造函数要给变量赋初值,初始化代码必须放在函数体中。比如:

struct Rule1 {
    int i;
    Rule1(int a): i(a) {}
    Rule1(): Rule1(40), i(1) {} // 无法通过编译
};

Rule1的委派构造函数Rule1()的写法就是非法的。我们不能在初始化列表中既初始化成员,又委托其他构造函数完成构造。

这样一来,代码清单3-11中的代码的初始化就不那么令人满意了,因为初始化列表的初始化方式总是先于构造函数完成的(实际在编译完成时就已经决定了)。这会可能致使程序员犯错(稍后解释)。不过我们可以稍微改造一下目标构造函数,使得委派构造函数依然可以在初始化列表中初始化所有成员,如代码清单3-12所示。

在代码清单3-12中,我们定义了一个私有的目标构造函数Info(int, char),这个构造函数接受两个参数,并将参数在初始化列表中初始化。而且由于这个目标构造函数的存在,我们可以不再需要InitRest函数了,而是将其代码都放入Info(int, char)中。这样一来,其他委派构造函数就可以委托该目标构造函数来完成构造。

事实上,在使用委派构造函数的时候,我们也建议程序员抽象出最为“通用”的行为做目标构造函数。这样做一来代码清晰,二来行为也更加正确。读者可以比较一下代码清单3-11和代码清单3-12中Info的定义,这里我们假设代码清单3-11、代码清单3-12中注释行的“其他初始化”位置的代码如下:

type += 1;

那么调用Info(int)版本的构造函数会得到不同的结果。比如如果做如下一个类型的声明:

Info f(3);

这个声明对代码清单3-11中的Info定义而言,会导致成员f.type的值为3,(因为Info(int)委托Info()初始化,后者调用InitRest将使得type的值为4。不过Info(int)函数体内又将type重写为3)。而依照代码清单3-12中的Info定义,f.type的值将最终为4。从代码编写者角度看,代码清单3-12中Info的行为会更加正确。这是由于在C++11中,目标构造函数的执行总是先于委派构造函数而造成的。因此避免目标构造函数和委托构造函数体中初始化同样的成员通常是必要的,否则则可能发生代码清单3-11错误。

而在构造函数比较多的时候,我们可能会拥有不止一个委派构造函数,而一些目标构造函数很可能也是委派构造函数,这样一来,我们就可能在委派构造函数中形成链状的委派构造关系,如代码清单3-13所示。

代码清单3-13所示就是这样一种链状委托构造,这里我们使Info()委托Info(int)进行构造,而Info(int)又委托Info(int, char)进行构造。在委托构造的链状关系中,有一点程序员必须注意,就是不能形成委托环(delegation cycle)。比如:

struct Rule2 {
    int i, c;
    Rule2(): Rule2(2) {}
    Rule2(int i): Rule2('c') {}
    Rule2(char c): Rule2(2) {}
};

Rule2定义中,Rule2()、Rule2(int)和Rule2(char)都依赖于别的构造函数,形成环委托构造关系。这样的代码通常会导致编译错误。

委派构造的一个很实际的应用就是使用构造模板函数产生目标构造函数,如代码清单3-14所示。


在代码清单3-14中,我们定义了一个构造函数模板。而通过两个委派构造函数的委托,构造函数模板会被实例化。T会分别被推导为vector::iterator和deque::iterator两种类型。这样一来,我们的TDConstructed类就可以很容易地接受多种容器对其进行初始化。这无疑比罗列不同类型的构造函数方便了很多。可以说,委托构造使得构造函数的泛型编程也成为了一种可能。

此外,在异常处理方面,如果在委派构造函数中使用try的话,那么从目标构造函数中产生的异常,都可以在委派构造函数中被捕捉到。我们可以看看代码清单3-15所示的例子。

在代码清单3-15中,我们在目标构造函数DCExcept(int, double)抛出了一个异常,并在委派构造函数DCExcept(int)中进行捕捉。编译运行该程序,我们在实验机上获得以下输出:

going to throw!
caught exception.
terminate called after throwing an instance of 'int'
Aborted

可以看到,由于在目标构造函数中抛出了异常,委派构造函数的函数体部分的代码并没有被执行。这样的设计是合理的,因为如果函数体依赖于目标构造函数构造的结果,那么当目标构造函数构造发生异常的情况下,还是不要执行委派构造函数函数体中的代码为好。

其实,在Java等一些面向对象的编程语言中,早已经支持了委派构造函数这样的功能。因此,相比于继承构造函数,委派构造函数的设计和实现都比较早。而通过成员的初始化、委派构造函数,以及继承构造函数,C++中的构造函数的书写将进一步简化,这对程序员尤其是库的编写者来说,无疑是有积极意义的。

时间: 2024-10-25 11:25:01

《深入理解C++11:C++ 11新特性解析与应用》——3.2 委派构造函数的相关文章

《深入理解C++11:C++ 11新特性解析与应用》——第3章 通用为本,专用为末 3.1 继承构造函数

第 3 章 通用为本,专用为末 C++11的设计者总是希望从各种方案中抽象出更为通用的方法来构建新的特性.这意味着C++11中的新特性往往具有广泛的可用性,可以与其他已有的,或者新增的语言特性结合起来进行自由的组合,或者提升已有特性的通用性.这与在语言缺陷上"打补丁"的做法有着本质的不同,但也在一定程度上拖慢了C++11标准的制定.不过现在一切都已经尘埃落定了.在本章里读者可以看到这些经过反复斟酌制定的新特性,并体会其"普适"的特性.当然,要对一些形如右值引用.移动

深入理解C# 3.x的新特性(1): Anonymous Type

在C#3.0中,引入了一个新的Feature:Anonymous Method,允许我们已Inline的方式来定义Delegate,为Developer在Coding的时候带来了很大的便利.在C#3.0中,我们又有了另一个相似的Feature:Anonymous Type.Anonymous Type允许我们已Inline的方式的创建一个基于未知类型.具有所需数据结构的对象. 一.Anonymous Type Overview  在传统的编程模式中,对象依赖于一个既定的Type,我们只能在Typ

深入理解C# 3.x的新特性(2):Extension Method[上篇]

在C#3.0中,引入了一些列新的特性,比如: Implicitly typed local variable, Extension method,Lambda expression, Object initializer, Anonymous type, Implicitly typed array, Query expression, Expression tree. 个人觉得在这一系列新特性的,最具创新意义的还是Extension method,它从根本上解决了这样的问题:在保持现有Type

深入理解C# 3.x的新特性(2):Extension Method[下篇]

四.Extension Method的本质 通过上面一节的介绍,我们知道了在C#中如何去定义一个Extension Method:它是定义在一个Static class中的.第一个Parameter标记为this关键字的Static Method.在这一节中,我们来进一步认识Extension Method. 和C# 3.0的其他新特性相似,Extension Method仅仅是C#这种.NET Programming Language的新特性而已.我们知道,C#是一种典型的编译型的语言,我们编

深入理解C#3.x的新特性(4):Automatically Implemented Property

深入理解C#3.x的新特性系列在沉寂一个月之后,今天继续.在本系列前3部分中,我们分别讨论了Anonymous Type,Extension Method 和Lambda Expression,今天我们来讨论另一个实用的.有意思的New feature:Automatically Implemented Property. 一.繁琐的private field + public property Definition 相信大家大家已经习惯通过一个private field + public pr

深入理解C# 3.x的新特性(5):Object Initializer 和 Collection Initializer

深入理解C# 3.x的新特性系列自开篇以后,已经有两个月了.在前面的章节中,我们先后深入讨论了C# 3.x新引入的一些列新特性:Anomynous Type.Extension Method.Lambda Expression.Automatically Implemented Property,今天我们来讨论本系列的涉及的另外两个简单的Feature: Object Initializer 和 Collection Initializer. 一.           为什么要引入Object

[原创]深入理解C# 3.x的新特性(3):从Delegate、Anonymous Method到Lambda Expression

较之前一个版本,对于C# 3.x和VB 9来说,LINQ是最具吸引力的.基本上很多的新的特性都是围绕着LINQ的实现来设计的.借助Extension Method,我们可以为LINQ定义一系列的Operator.通过Lambda Expression我们可以为LINQ编写更加简洁的查询.我们可以说这些新的特性成就了LINQ,也可以说这些新特性就是为了实现LINQ而产生,但是我们应该明白,对于这些新引入的特性,LINQ并非他们唯一的用武之地,在一般的编程中,我们也可以使用它们. 继上一章,介绍Ex

《深入理解C++11:C++ 11新特性解析与应用》——2.14 本章小结

2.14 本章小结 在本章中,我们可以看到C++11大大小小共17处改动.这17处改动,主要都是为保持C++的稳定性以及兼容性而增加的. 比如为了兼容C99,C++11引入了4个C99的预定的宏.__func__预定义标识符._Pragma操作符.变长参数定义,以及宽窄字符连接等概念.这些都是错过了C++98标准,却进入了C99的一些标准,为了最大程度地兼容C,C++将这些特性全都纳入C++11.而由于标准的更新,C++11也更新了__cplusplus宏的值,以表示新的标准的到来.而为了稳定性

《深入理解C++11:C++ 11新特性解析与应用》——2.9 扩展的friend语法

2.9 扩展的friend语法 类别:部分人 friend关键字在C++中是一个比较特别的存在.因为我们常常会发现,一些面向对象程序语言,比如Java,就没有定义friend关键字.friend关键字用于声明类的友元,友元可以无视类中成员的属性.无论成员是public.protected或是private的,友元类或友元函数都可以访问,这就完全破坏了面向对象编程中封装性的概念.因此,使用friend关键字充满了争议性.在通常情况下,面向对象程序开发的专家会建议程序员使用Get/Set接口来访问类

《深入理解C++11:C++ 11新特性解析与应用》——第1章 新标准的诞生 1.1 曙光:C++11标准的诞生

第 1 章 新标准的诞生 从最初的代号C++0x到最终的名称C++11,C++的第二个真正意义上的标准姗姗来迟.可以想象,这个迟来的标准必定遭遇了许多的困难,而C++标准委员会应对这些困难的种种策略,则构成新的C++语言基因,我们可以从新的C++11标准中逐一体会.而客观上,这些基因也决定了C++11新特性的应用范畴.在本章中,我们会从设计思维和应用范畴两个维度对所有的C++11新特性进行分类,并依据这种分类对一些特性进行简单的介绍,从而一览C++11的全景. 1.1 曙光:C++11标准的诞生