Effect C++笔记
Effective C++
Effective学习笔记
让自己习惯C++
条款01
C++高效编程时情况而变,取决于你使用C++的那一部分
条款02:尽量以const,enum,inline替换#define
Perfer consts enum and inlines to #define
总结:
✦For simple constants, prefer const objects or enums to #defines.
✦For function-like macros, prefer inline functions to #defines.
条款03:尽可能使用const
Use const whenever possible
const成员函数
优点
他们使class接口比较容易被理解,可以得知那个函数可以改动对象成员,那个不能
他们使用“操作const对象”成为可能。
constness
当两个成员函数如果只是常量性不同,就可以用const来重载
1 | class TextBlock { |
在const和non-const成员函数中避免重复
总结
✦Declaring something const helps compilers detect usage errors. constcan be applied to objects at any scope, to function parameters andreturn types, and to member functions as a whole.
✦Compilers enforce bitwise constness, but you should program usinglogical constness.
✦When const and non-const member functions have essentially identi-cal implementations, code duplication can be avoided by having thenon-const version call the const version.
条款04:确定对象被使用前已经先被初始化
Make sure that object are initialized before they're used
初始化与赋值
规则
永远在使用对象之前将它初始化。对于内置内省必须要手工完成初始化
对于自定义类型来说,初始化职责放在构造函数上
赋值与初始化
1 | class PhoneNumber { ... }; |
初始化
成员变量初始化动作发生在进入构造函数本体之前,相对于赋值来说发生时间更早。
初始化列表
1 | ABEntry::ABEntry(const std::string& name, const std::string& address, |
初始化优点
通常效率较高;
赋值版本:先调用构造函数为成员变量赋予初值,然后立刻在对他们赋予新值(传入的实参)。因此会浪费一部分时间
初始化版本:成员初始列中针对各个成员变量而设的实参,被拿去作为各成员之构造函数的实参。
必须初始化的情况
例如:成员变量为const或references,必须要进行成员初始列表
综述: 一般情况总是使用成员初始列表,可以针对普遍的变量,而且比赋值更高效
成员初始化次序
base class -> derived class
成员变量以被声明的次序进行初始化因此对于示例ABEntry 中theName 最先被初始化,然后是theAddress,再是thePhone,最后是numTimesConsulted.
static对象
生命周期
从被构造出来直到程序结束,因此不是堆栈对象。
定义
一般这种对象被定义为global对象、定于于namspace作用域、classes、函数内、以及file作用内声明为static的对象
local static与non-local static对象
long static对象:定义域函数内,因为它对于函数而言是local
non-local static对象:其他剩余的static对象
多源文件跨编译单元编译
一般至少应该non-local static对象
问题
某编译单元内的某个non-local static对象初始化动作使用了另一个编译单元内的某个编译单元内的某个non-local static对象,它所用到的这个对象可能没有被初始化,因为c++对于 ”定义在不同编译单元的non-local对象那个“初始化次序没有明确的规定
non-local static 对象初始化次序问题
示例
1 | class FileSystem {// from your library’s header file |
1 | class Directory {// created by library client |
假设:用户决定创建应该Directory对象,用来放置临时文件
Directory tempDir{params};
初始化次序的重要性
除非tfs在tempDir先被初始化,否则tempDir会用到尚未初始化的tfs。(但是tfs和tempDir是被不同人在不同时间创建的),他们是不同编译单元的non-local对象
解决方案
将每个non-local对象搬到总结专属的函数内(函数也要声明为static)、
这些函数返回应该reference指向它所含的对象。然后用户调用这些函数
C++保证
函数内的local static对象会在”该函数调用期间“”首次遇到该对象的定义式”时被初始化。
因此如果用”函数调用“(返回应该reference指向local static对象)替换”直接访问non-local static对象“,就获得了保证,保证你所获得reference将指向应该历经初始化的对象。
优点
从未调用non-local static对象的”仿真函数“,就绝不会引发构造和析构成本
优化代码
1 | class FileSystem { ... }; // as before |
总结:
✦Manually initialize objects of built-in type, because C++ only some-times initializes them itself.
✦In a constructor, prefer use of the member initialization list to assignment inside the body of the constructor. List data members in the initialization list in the same order they’re declared in the class.
✦Avoid initialization order problems across translation units by re-placing non-local static objects with local static objects.
构造/析构/赋值运算
条款05:了解C++默认编写和调用了哪些函数
Know what functions C++silently writes and calls.
总结:
✦Compilers may implicitly generate a class’s default constructor, copyconstructor, copy assignment operator, and destructor
条款06:若不想使用编译器自动生成的函数,就因该明确的拒绝
Explicitly disallow the use of compiler-generated functions you do not want.
总结:
✦To disallow functionality automatically provided by compilers, declare the corresponding member functions private and give no imple-mentations. Using a base class like private is one way to do this.
条款07:为多态基类声明virtual析构函数
1 |
|
运行结果
1 | B constrctor |
总结:
✦Polymorphic base classes should declare virtual destructors. If aclass has any virtual functions, it should have a virtual destructor.
✦Classes not designed to be base classes or not designed to be usedpolymorphically should not declare virtual destructors.
条款08:别让异常逃离析构函数
prevent exception from leaving destruction
总结
✦Destructors should never emit exceptions. If functions called in adestructor may throw, the destructor should catch any exceptions,then swallow them or terminate the program.
✦If class clients need to be able to re act to exceptions thrown duringan operation, the class should provide a regular (i.e., non-destruc-tor) function that performs the operation.
条款11:在operator=中“自我赋值”
Handle assignment to self in operator=
以widget窗口类为示例
1 | class Bitmap{...} |
方法一:证同测试
1 | Widget& Widget::operator=(const Widget& rhs) |
异常问题:
new Bitmap出现异常(分配内存错误导致错误)this->pb会指向一块被删除的内存
方法二:异常安全性
1 | Widget& Widget::operator=(const Widget& rhs) |
证同测试效率问题
应用证同测试会导致代码效率变低,if分支导致执行速度下降
副本:
不是指向一个对象只是一个值容器,如函数传参
方法三:copy-swap(异常安全性)—-条款29
1 | class Widget{ |
A variation on this theme takes advantage of the facts that
(1) a class’scopy assignment operator may be declared to take its argument byvalue and
(2) passing something by value makes a copy of it (seeItem20)
1 | Widget& Widget::operator=( Widget rhs)//一个副本修改它不会修改其对象本身 |
总结
✦Make sure operator= is well-behaved when an object is assigned to itself. Techniques include comparing addresses of source and target objects, careful statement ordering, and copy-and-swap.
✦Make sure that any function operating on more than one object be-haves correctly if two or more of the objects are the same.
资源管理
条款14:在资源管理类中小心copying行为
think carefully about copying behavior in resource-managing classes
1 | class Lock |
Clients use Lock in the conventional RAII fashion:
1 | Mutex m;// define the mutex you need to use |
RAII对象copied的选择
禁止复制(prohibit copying)
对底层资源使用“引用计数法”
总结
✦ 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
✦普遍而常见的RAII classes copying 行为时:一直copying、实行引用计数法。
条款15:在资源类中提供对原始资源的访问
provide access to raw resources in resource-managing classes
原因: 编译器无法将RAII类(shared_ptr)作为指针,会将其作为一个对象,无法发生隐式转换而报错
示例 :
1 | std::tr1::shared_ptr<Investment> pInv(createInvestment()); |
解决方案:
convert an object of the RAII class (in this case,tr1::shared_ptr) into the raw resource
(1)explicit conversion:
smart_ptr offer a get number function to perform explicit conversion to return a raw pointer inside this smart_ptr
1 | int days = daysHeld(pInv.get( ));// fine, passes the raw pointer |
(2)implicit conversion:
1 | class Str{ |
总结
✦APIs often require access to raw resources, so each RAII classshould offer a way to get at the resource it manages.
✦Access may be via explicit conversion or implicit conversion. In gen-eral, explicit conversion is safer, but implicit conversion is more con-venient for clients.
设计与声明
条款20:宁以pass-by-reference-to-const 替换pass-by-value
1 | class Person{ |
用pass-by-reference-to-const的好处
高效,不会创建临时对象,因此不会再次调用construt和deconstruct。
准确且正确的多态,pass-by-value无法识别准确的object,如test1中,real object 都是person无法调用student的多态,pass-by-reference-to-const是可以准确调用student对象
总结
✦Prefer pass-by-reference-to-const over pass-by-value. It’s typicallymore efficient and it avoids the slicing problem.
✦The rule doesn’t apply to built-in types and STL iterator and func-tion object types. For them, pass-by-value is usually appropriat
条款21:必须返回对象时,别妄想返回其reference
Don't try return a reference when you must return an object
1 | class Rational{ |
该段代码描述有理数的相乘的类的设计
当以reference作为返回值时的几大误区
1 | ==================on the stack=================== |
on the stack :reference作为返回值时,result对象是一个stack值,当其返回时离开了作用域,result对象会被销毁,reference返回时会导致其指向了一个被销毁的值
on the heap :heap分配的内存,new出来的对象,无法得知合适对该对象进行delete,因此会导致内存泄漏
static local :会引起多线程问题,例如数据共享问题
正确做法:
1 | inline const Rational operator*(const Rational &lhs, |
说明:虽然会付出一点点的成本代价(construct和deconstruct),但从长远的角度和综合对比可知,此方法是最合适的做法
总结:
✦Never return a pointer or reference to a local stack object, a refer-ence to a heap-allocated object, or a pointer or reference to a localstatic object if there is a chance that more than one such object willbe needed. (Item4 provides an example of a design where returninga reference to a local static is reasonable, at least in single-threadedenvironments.)
条款22:将成员变量声明为private
Declare data member private
private(提供封装)和其他(提供封装)
对于protect声明的成员变量,当其出现问题后或是被取消后,将会影响整个派生类
总结:
✦Declare data members private. It gives clients syntactically uniformaccess to data, affords fine-grained access control, allows invariantsto be enforced, and offers class authors implementation flexibility.
✦protected is no more encapsulated than public(protect 与 public 差不多)
条款23:宁以non-member、non-friend替换member函数
Perfer non-member、non-friend to member function
问题描述
示例 :
1 | class WebBrower |
我们更愿意使用non-member代替member函数
原因
1.non-member的封装性比member函数的更强
2.non-member对类的相关机能有较大的包裹弹性
封装:它使我们能够改变事物而只影响有限客户
愈多东西被封装,愈少人能看见它,因此能够有愈大的弹性去改变它
愈多函数可以访问封装的函数,数据的封装性越低
条款22:成员变量应该是private,以为如果他们不是,就有无数的函数可以去访问他们,没有封装可言
访问限制
能够访问private成员变量的函数只有class和member函数与friend函数可以访问
程序设计问题
1 | namespace WebBrowserStuff{ |
设计特点
namespace 与classes不同,namespace可以支持跨多个源码文件而class不能
non-member、non-friend函数为用户提供较好的程序机能以及封装性
1 | //头文件webbrowsers。 |
上述案例运用了namespace的跨越多个源码文件的特点
标准库组织方式
在STL头文件中std命名空间内的每一个东西,每个头文件声明std的某些机能
例如只想用vector,那就只用vector头文件,这允许客户支队他们所用的小部分系统形成编译依赖,但该方法不能适用于member函数,以为class必须整体定义,不可分割
将所有便利函数放在多个头文件内但隶属同一命名空间,意味用户可以轻松的扩展。需要只做到则是,在该命名空间添加non-member、non-friend函数。
总结:
✦Prefer non-member non-friend functions to member functions. Do-ing so increases encapsulation, packaging flexibility, and functionalextensibility.
条款24: 若所有参数皆需类型转换,请为此采用non-member函数
Declare non-member functions when type conversions should apply to all parameters
示例:Rational
运用条款3,20,21书写正确的operator*
1 |
|
注意事项
在隐式转换时,构造函数需要写出默认参数,不然不能进行隐式转换,因为Rational类构造函数有两个参数;
在构造函数加上explicit就可以防止隐式转换
2 * r1 不能成功运行,是不是不满足交换律?
重写上述例子
1 | result = r1.operator*(2); |
因为r1内含一个operator函数的class的对象,并且将2进行隐式转换r1.operator (Rational(2))
对于2.operator(r1)显然时错误的,没有这个重载函数
将operator写为non-member函数
1 | const Rational operator*(const Rational& lhs,const Rational& rhs) |
优点
non-member函数如条款23所述封装性更强
支持所有参数的隐式类型转换
总结
✦If you need type conversions on all parameters to a function (includ-ing the one that would otherwise be pointed to by the this pointer),the function must be a non-member.
条款25:考虑写出一个不抛出异常的swap函数
consider support a no-throwing swap
前置知识
无法交换值、无法交换指针
普通swap函数实现细节
1 | void swap(int* a,int* b){ |
陷阱
对于c/c++的初学者来说,犯下一个很大的陷阱,那就是我们只传过去一个地址对于者个地址值只是一个副本信息,并不是原对象,导致无法交换地址值更无法交换指针值。我们只是通过传参过来的一个 (地址)值(副本),这是我们只是对原对象的指针副本做了交换
无法交换地址、可以交换指针
1 | void swap(int* a,int* b){ |
解析
解引用后,此时a、b是a、b地址对应内存的值,也就是原对象值,改操作进行的是对(原对象)内存的进行置换,但此时并不会改变地址值,地址值仍是一个副本
可以交换值和地址
1 | void swap(int** a,int** b){ |
解析
p1和p2是一个地址
我们用双层指针,写出一个swap的置换函数,我们传入地址进去,此时a、b的值是指向p1和p2的地址。对a、b进行解引用,则对应的a 、b是对应p1、p2的地址原对象。
进行置换则交换的是p1和p2的值,那么地址交换,相应改变的是p1和p2的对象名所对应的地址而已,而实际的内存并没有置换而已。
引用方法
1 | std::swap; |
这种方法既可以改值也可以改地址,因为引用的本身就是绑定一个原对象,并不是副本
标准库swap
std::swap
1 | namespace std { |
只要T支持copying(通过copy构造函数和assignment操作符完成),默认的swap会实现代码就会将类型为T的对象进行置换
示例:swap
实践Widget类
- 用pimpl手法将Widget的数据成员封装到WidgetImpl中
1 | class WidgetImpl {// class for Widget data; |
设计问题
置换两个Widget对象过于复杂,浪费空间和效率(对于置换Widget对象值,我们只需要做的是置换impl指针,但默认的swap要交换Widget类更需要交换WidgetImpl)
可以直接交换指针的地址,改变指针指向的内存
置换其impl指针
1 | namespace std{ |
问题
pImpl是属于Widget的private成员因此此函数肯定是无法编译通过的,我们可以将其声明为friend函数但其封装性较弱,可以将swap声明为member函数如下:
声明public成员函数置换
1 | class Widget { |
优点
能够通过编译,并且具有STL容器的一致性,以为std::swap也提供了有pulic swap成员函数的和std::swap的特化版本
缺点
对于Widget class templates而非classes 将数据类型加以参数化
1 | template<typename T> |
1 | namespace std { |
错误分析
企图偏特化一个function template(std::swap),但C++只能对class templates偏特化,在function templates身上时不能偏特化的。因此无法编译。
偏特化function template
1 | namespace std {template<typename T>// an overloading of std::swap |
问题
重载function templates是没问题的,但std是一个特殊的命名空间:
可以全特化std内的templates
不可以添加新的templates(class或function)到std里面
标准做法
高效正确的做法:non-member的swap、member的swap函数相结合
1 | namespace WidgetStuff { |
swap实现效率不足的解决(class或template运用了pimpl手法)
提供一个public的swap函数,让它高效地置换你的类型的两个对象值,而其不能抛出异常
在你的class或template所在的命名空间提供一个non-member swap,并令他调用上述swap成员函数
如果编写一个class(而非class template),为你的class特化一个std::swap,并令他调用你的swap的成员函数
必须使用using std::swap,以便其能够在函数类曝光可见,然后报价namspace修饰符
总结
✦Provide a swap member function when std::swap would be inefficientfor your type. Make sure your swap doesn’t throw exceptions.
✦If you offer a member swap, also offer a non-member swap that callsthe member. For classes (not templates), specialize std::swap, too.
✦When calling swap, employ a using declaration for std::swap, then callswap without namespace qualification.
✦It’s fine to totally specialize std templates for user-defined types, butnever try to add something completely new to std.
实现
条款26:尽量将变量的声明置后
Postpone variable definitions as long as possible
变量声明前置示例
1 | // this function defines the variable "encrypted" too soon |
问题
使用:正常执行的时候需要用到encrypted
未使用:当异常抛出时,encrypted对象的构造成本已经造成,而析构要离开作用域后才会启用。
变量置后
1 | // this function postpones encrypted’s definition until it’s truly necessary |
循环中的变量声明
1 | // Approach A: define outside loop |
两种方法的成本对比
■Approach A: 1 constructor + 1 destructor + n assignments.
■Approach B: n constructors + n destructors.
效率分析
■Approach A:可见性强,当n较大时效率更好
■Approach B:n小时效率高
(1) assignment相比constructor-destruction(对)更加便宜
(2) 如果您正在处理代码中对性能敏感的部分,则应默认使用方法B。
总结:
✦Postpone variable definitions as long as possible. It increases pro-gram clarity and improves program efficiency.
条款27:尽量少做转型操作
Minimize casting
C语言转型语法
1 | //C风格的转型 |
C++新式转型(new-style)
1 | const_cast<T>(expression) |
const_cast通常被用来将对象的常量性去除
dynamic_cast 主要用来执行“向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。
reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,不可以移植。例如将pointer to int 转型为一个int。
static_cast用来强制隐式转换,例如将non-const对象转为const对象,或将int转为double等等。它可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derivered。但它无法将cosnt转为non-const——这个只有const_cast才能办到。
新式转换的优点
在代码中容易被识别出来(grep工具和人工识别),找出类型系统在那个地点被破坏
各转型动作的目标越窄化,编译器越可能诊断错误地点,例如将constness去除,除非使用新式转型中的const_cast否之无法通过编译
旧式类型转换的使用场景:
1 | class Widget |
在function-style看起来更像类型转换。可进行显式构造函数中类型转换。
RTII(Run Time Type Identification)
含义
令编译器编译出运行期间执行的代码
1 | int x,y; |
将int 转型为double会产生一些代码,因为int与double的底层描述不相同
1 | class Base{...}; |
上述两个指针的值并不相同(&d,pb),这个时候会又一个偏移量运行期施加在Derived*指针上取得正确的指针值。
转型容易写出似是而非的代码
1 | class Window {// base class |
问题所在
static_cast(this).onResize(),调用的onResize并不是当前对象上的函数,而是稍早转型动作所建立的一个 “this对象的base class成分” 的暂时副本身上的onResize。
(函数就是函数,它只是一个成员仅此一份,关键在于成员函数都含有个隐藏的this指针,因此会影响成员操作数据)
换句话来说就是会丢失在当前对象base-class中对数据的操作
it does not invoke that function on the current object! Instead, the cast creates a new, temporary copy of the base class part of *this, then invokes onResize on the copy!
实际示例
1 | class A{ |
如上述例子,我们的操作仅仅只是在*this指针强转得到一个副本上调用了函数,因此在当前对象上并没有调用base-class的成员函数,所有A::get_size中的size++不会在当前对象上作用,所以得到的结果为1.
解决办法:去除类型转换
1 | class SpecialWindow: public Window { |
dynamic__cast
使用场景
只有指向base-class的一个pointer或reference时,想要去操作认定为derived-class对象身上执行derived-class操作函数,依靠该dynamic__cast转型方法实现
缺陷
dynamic_cast的执行速度相当的慢,而且向下转型本就是一个不安全的行为,因此有两个办法用来取代dynamic__cast
做法一
使用容器并在请汇总存储直接指向derived-class对象的指针(通常为只能指针),消除了”通过base-class接口处理对象函数“的需要。
示例
假设先前的window/specialwindow继承体系只有specialwindows才支持闪烁效果
1 | class Window { ... }; |
修改
1 | typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW; |
缺陷在于无法在同一容器内存储指向window的任何派生类。处理多窗口需要多个容器,他们都具备类型安全性
做法二
通过base-class接口处理所有window的所有派生类,就是在base-class内提供virtual函数做任何想多window派生类做的时
示例
虽然specialwindow可以闪烁,但或许将闪烁函数声明在base-class中并提供一份空的默认函数
1 | class Window { |
无论是那种做法——”类安全容器“还是”virtual函数往继承体系上方移动“,都只是一个可行方案,需要靠自己判断
总结:
✦Avoid casts whenever practical, especially dynamic_casts in perfor-mance-sensitive code. If a design requires casting, try to develop acast-free alternative.
✦When casting is necessary, try to hide it inside a function. Clientscan then call the function instead of putting casts in their own code.
✦Prefer C++-style casts to old-style casts. They are easier to see, andthey are more specific about what they do.
条款28:避免返回handles(reference,pointer,iterators)指向对象内部成分
Avoid returning “handles” to object internals
示例
假设您正在处理一个涉及矩形的应用程序,每个矩形可以由其左上角和右下角表示。要使矩形对象保持较小,可以决定定义其范围的点不应存储在矩形本身中,而应存储在矩形指向的辅助结构中
1 | class Point { // class for representing points |
对于Rectangle类我们添加两个const-reference member function,为何使用const-reference在条款20
中有说明。但因此会导致以下问题。
破坏封装性
1 | Point coord1(0, 0); |
由上述代码可以得知,我们不仅仅能对矩形的点进行读,并且能够进行修改,那么我们定义的数据成员与public就没有什么两样。(虽然我们在upperLeft()函数添加了const定义,但我们只是不能对指向Rectdata的智能指针进行修改,可以对该对象内部的值进行修改)
修改
1 | class Rectangle { |
经过将返回值加上const我们可以让const成员限定符不在是个fake,我们只能对数据进行读写。
但是这种方式仍然会引起下述问题
dangling handles 所指对象不存在
1 | class GUIObject { ... }; |
问题分析
上述问题中会调用boundingBox(*pgo)函数对象,它所返回的值是一个临时的Rectangle副本(temp),当我们用upperLeft去作用与temp身上,返回一个reference指向temp的一个内部成分
错误
当我们结束这段语句是会产生一个问题,就是我们的boundBox的返回值(temp),将会被析构,也就是这个temp对象的内部成员都不复存在,那么我们的pUpperLeft就指向了一个不复存在的值。
例外:operator[]
在vector和string容器中有个成员函数operator[]可以选择个别的元素,这个函数就是返回reference指向“容器内的数据”,但那些数据会随着容器的销毁而销毁。这仅仅只是有个例外。
总结
✦Avoid returning handles (references, pointers, or iterators) to object internals. Not returning handles increases encapsulation, helpsconst member functions act const, and minimizes the creation of dangling handles.
条款29:为”异常安全“而努力是值得的
Strive for exception-safe code
异常问题
以class用来表现夹带背景图案的GUI菜单,运用于多线程
1 | class PrettyMenu{ |
”异常安全“的条件
不泄漏任何资源:new Image(imgSrc)导致异常,对unlock的调用就绝不会执行。
不允许数据败坏:new Iamge(imgSrc)抛出异常,bgImage就是指向一个被删除的对象,imageChanges以及被累加,而其实并没有新的图像被成功安装起来
RAII解决方案
1 | void prettMenu::changeBackground(std::istream& imgSrc) |
不在需要调用unlock
”异常安全函数“保证
基本承诺:异常抛出时,不会使对象或数据结构会因此而破坏,就数据保持异常抛出钱的状态
强烈保证:异常抛出使,程序状态不会改变。(函数失败恢复到”调用函数之前“的状态)
不抛掷保证:它们总能完成总能的原先承诺的功能
异常安全函数解决问题
智能指针解决问题:
1.引用智能指针类管理内存
2.将计数器的次序交换
1 | class PrettMenu{ |
优点:
不需手动delete旧图像,而且删除操作是在对象被成功创建的之后,因此new成功后才会成功调用reset函数,Image(imgSrc)的临时对象也会在reset中释放掉(delete)
问题:
Image构造函数会抛出异常(输出流的读取记号已经被移走)
Copy and Swap
原则介绍:
为你打造修改的对象(原件)做出一个副本,然后再那副本身上做一切的修改。若修改发生错误,源对象仍能保存原始状态。修改成功,则原件和副本做置换操作。
修改对象数据副本,一个不会抛出异常的函数(swap)中将修改后的数据和原件置换
”隶属对象数据“ pimpi idiom
从原对象放进一个另一个对象内,然后赋予原对象一个指针,指向那个实现对象(副本)、
1 | struct PMImpl{//pImpl是一个private成员具有封装性 |
总结:
✦Exception-safe functions leak no resources and allow no data struc-tures to become corrupted, even when exceptions are thrown. Suchfunctions offer the basic, strong, or nothrow guarantees.
✦The strong guarantee can often be implemented via copy-and-swap,but the strong guarantee is not practical for all functions.
✦A function can usually offer a guarantee no stronger than the weak-est guarantee of the functions it calls.
条款30:透彻了解inlining的里里外外
Understand the ins and outs of inlining
inline的细节
- inline函数代码量不易过大会导致程序体积过大,导致代码膨胀以至于额外的换页行为
- inline只是对编译器的申请并不是强制命令,class内的函数被隐喻的称为inline
- inline通常被置于头文件,在编译过程中进行inlining,而为将一个“函数调用”替换为“被调用函数的本体”
inline的声明
隐式声明为inline的函数
- member函数
- friend函数
不应被声明为inline的函数
- 构造以及析构函数
总结
✦Limit most inlining to small, frequently called functions. This facili-tates debugging and binary upgradability, minimizes potential codebloat, and maximizes the chances of greater program speed.
✦Don’t declare function templates inline just because they appear inheader files.
条款31:将文件间的编译依存关系降至最低
Minimize compilation dependdencies between files
示例
1 | class Person { |
要想让Person class编译需要加入以下头文件的类或函数声明式
include < string >
include “date.h”
include “address.h”
但这样会导致这些文件中形成一种编译的依存关系,所依赖的头文件发生改变都会让Person class的头文件进行重新文件
将class的实现细目至于class的定义式中
1 | namespace std { |
问题
string前置声明错误,正确的也复杂
前置声明每一个东西困难的是,编译器必须知道对象的大小
1 | int main(){ |
编译器清楚的知道int需要多大,而Person需要询问class的定义式。
针对于Person类可以用以下方法:将Person分割为两个classes,一个只提供接口,一个只负责实现该接口。将负责实现的Implementation class取名为PersonImpl,Person将定义如下
pimpl idiom(pointer to implementation)
pimpl 惯例是一种新式 C++ 技术,用于隐藏实现、最小化耦合和分离接口。 Pimpl 对于”指向实现的指针”是短的。你可能已熟悉概念,但通过其他名称(如 Che一 cat 或编译器防火墙惯例)了解它。
下面是 pimpl 惯例如何改进软件开发生命周期:
最大程度地减少编译依赖项。
接口和实现分离。
可移植性。
优点
有较好的封装性以及减少客户端的文件依赖性
1 | // 使用Pimpl |
1.减少了需要包含的头文件;2.当内部实现发生变化时,客户端的代码不需要重新编译。例如:客户端在gcc编译中只需要连接上其动态连接库或者静态库文件,这时候服务端已经将所需的文件的编译完了,可以减少客户端编译的时间
由此修改以上Person代码
1 |
|
上述代码main class中内涵一个
这样的设计下,Person的客户就完全与Dataes,Address以及Persons的实现的细目分离。因此哪些classes的任何实现修改都不需要Person客户断重新编译。“接口与实现分离”
关键
这个分离在于以“声明的依存性”替换为“定义的依存性”,编译最小化的本质:现实中让头文件尽可能的自我满足,万一做不到,则让他与其他文件内的声明式(并非定义式)相依
设计策略
■Avoid using objects when object references and pointers will do
■Depend on class declarations instead of class definitions whenever you can.
声明函数而它用到某个class式,你并不需要改class的定义:纵使函数以by value方式传递改类型的参数:
1 | class Date;// class declaration |
声明这两个函数的无需Date的定义式,但是当有人调用哪些函数式,调用之前需要让Date的定义式曝光
如果将”提供class定义式“(通过#include完成)的义务冲”函数声明所在“之头文件转移到”内涵函数调用“之客户文件,便可将”并非真正必要的类型定义“与客户端之间的编译依存去除掉
■Provide separate header files for declarations and definitions
需要两个头文件,一个用于声明式,一个用于定义式。文件必须保持一致性,如果声明式被改变,两个文件都需要改变。#include一个声明文件而非前置声明若干函数
1 |
|
C++中提供关键字export,允许将template声明式和template定义式分割与不同的文件内,但式这个关键字在有些编译器里不支持
Handle classses
像Person这样使用pimpl idiom的classes,被称为Handle classes。
方法一
将他们的所有函数转交给一个相应的实验类并由后者完成实际工作。例如卖弄Person的两个成员函数的实现
1 |
|
Person构造函数以new调用PersonImpl构造函数,以及Person::name函数内调PersonImpl::name,让Person百年城一个Handle class但不会改变他做的事,只会改变它做事的方法
Interface classes
令Person称为一个特殊的抽象基类,称为interface class。这汇总class的目的事猫叔derived的接口,因此他通常不带有成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtal函数。
1 | class Person { |
class的用户必须以Person的pointer和reference来写应用程序,因为他不可能针对”内含pure virtual函数“的person classes具体出实体。
interface class的客户必须有办法为这种class创建新的对象。
如下
1 | class Person { |
客户将会这样使用这些接口
1 | std::string name; |
支持interface class接口的那个concrete class 必须被定义出来,而其真正的构造函数必须被调用。一切都在virtual构造函数实现所在的文件内放生
1 | class RealPerson: public Person |
一个更现实的Person::create实现代码会创建不同类型的derived class对象。取决于额外参数值、读自文件或数据库的数据、环境变量。
RealPerson示范实现了Interface class 的两个最常见的机制之一:从Interface class继承接口规格,然后实现出接口所覆盖的函数。第二点则是多重继承
总结:
✦The general idea behind minimizing compilation dependencies is todepend on declarations instead of definitions. Two approachesbased on this idea are Handle classes and Interface classes.
✦Library header files should exist in full and declaration-only forms.This applies regardless of whether templates are involved.
继承与面向对象设计
条款32:确定你的public继承塑造出的is-a关系
Make sure public inheritance models "is-a"
通过public继承出的关系为“is-a”关系,如下所示
1 | class Person { ... }; |
如上述关系可以表述出学生是人,但人这个抽象类却不一定是人
is-a的误区
1 | class Bird { |
错误
这个继承体系中说明企鹅是鸟的派生类,那么它应该含有鸟类的所有行为,但是企鹅却不会飞,这点显得不是特别的严谨。我们应该让is-a有较佳的真实性
方法一:双class继承体系
示例
1 | class Bird { |
方法二:运行期错误
1 | void error(const std::string& msg);// defined elsewhere |
此处声明出企鹅是不会飞的,那么说企鹅会飞则是一种错误的认知,在运行期的时候会被检测出来
总结:
✦Public inheritance means “is-a.” Everything that applies to baseclasses must also apply to derived classes, because every derivedclass object is a base class object.
条款33:避免遮掩继承而来的名称
Avoid hiding inherited names
命名查找规则(作用域)
1 | class Base { |
Base的作用域大于Derived的作用域,根据命名查找法,当我们在Derived类中查找mf2时,选择方向Derived->Base->global。小一级的作用域会将其覆盖。
名称可视性(name visibility)
1 | class Base { |
Base内名为mf1和mf3的重载函数都被Derived内的mf1和mf3函数所遮掩。从名称查找观点来看Base::mf1和Base::mf3不在被Derived继承。
1 | Derived d; |
更具以上代码可知,当我们在重载函数时,在子类中就只能对Derived作用域的函数名可见,但是对于重载函数是不可见的。不论是virtual还是non-virtual都是一样。
解决继承来的名称的遮掩行为
违反is-a关系
当public继承而又不继承哪些重载函数就是违反base和deriver class之间的is-a关系
using声明表达式
1 | class Base { |
1 | Derived d; |
用using声明,derived类继承了base并加上了重载函数,此时也可以重写一部分重载函数将base的函数给覆盖。
forward function转交函数
使用场景
不想继承base的所有函数,在“is-a”中会违背其含义
private继承
1 | class Base { |
inline转交函数的用途
哪些不支持using声明式,将继承而得的名称汇入derived作用域
总结
✦Names in derived classes hide names in base classes. Under publicinheritance, this is never desirable.
✦To make hidden names visible again, employ using declarations orforwarding functions.
条款34:区分接口继承和实现继承
Difference between inheritance of interface and inheritance of implementation
public继承概念
函数接口继承和函数实现继承
示例
1 | class Shape { |
成员函数的接口总是会被继承
public为is-a关系继承,所有对Base class为真的事件对于Derived class也为真。
接口与实现
- 接口:是(对外或者对继承)可视的,定义一个的对象实体可以通过(对外可视的)接口去访问该对象
- 实现:是一个实体,可以看作是接口所要做到事,对外不一定可见,对内一定可见
pure virtual函数
1 | class Shape { |
pure virtual函数的特性:
必须被他们所继承的具象类所重新声明
抽象类中通常没有对该函数的定义
pure virtual函数(子类必须重写)
让derived class只继承接口
shape class无法对shape::draw函数提供合理的默认实现,比较其模棱两可(椭圆和矩形的画法),因此在具象derived class 中必须提供一个draw函数,并且不干涉如何实现
1 | Shape *ps = new Shape;// error! Shape is abstract |
impure virtual函数(可选是否重写)
让derived class继承函数的接口和默认实现
1 | class Shape { |
Shaped::error的声明式要求derived classes必须支持一个error函数,但如果不想自己写一个,那么就可以使用Shaped class提供的默认版本
non-virtual函数(不能重写)
让derived class继承函数的接口及一份强制性实现
1 | class Shape { |
non-virtual函数:并不打算在derived classes中有不同的行为。实际上non-virtual函数表现的不变性(invariant)大于其特异性(specialization)。
例如:Shape::objectID是有特定计算ID的一个函数,该方法是由其定义式决定的,任何derived class都不应该修改其行为,不应该在derived class中被重新定义。(破坏多态性)
总结
✦Inheritance of interface is different from inheritance of implementa-tion. Under public inheritance, derived classes always inherit baseclass interfaces.
✦Pure virtual functions specify inheritance of interface only.
✦Simple (impure) virtual functions specify inheritance of interfaceplus inheritance of a default implementation.
✦Non-virtual functions specify inheritance of interface plus inherit-ance of a mandatory implementation.
条款35:考虑virtual函数以外的其他选择
Consider alternatives to virtual function
设计一个计算人物生命值的函数healthValue()
1 | class GameCharacter { |
我们一impure virtual函数去声明函数,那么当子类不提供函数重写那么人物将采用默认的声明值的计算方法
Template Method模式
Non-virtual interface实现方法
1 | class GameCharacter { |
基本设计
令客户通过public non-virtual成员函数去间接的调用private virtual函数,因此称为non-virtual(NVI)手法。
优点
NVI手法的优点在于“do ‘before’ stuff”和“do ‘after’ stuff”在上述的注释代码中,在Wrapper(healthValue)中设定好virtual函数的应用场景
“do ‘before’ stuff”:locking a mutex, making a log entry(日志记录项), verifying that class invariants and function preconditions aresatisfied, etc
“do ‘after’ stuff”:unlocking a mutex, veri-fying function postconditions, reverifying class invariants(再次验证class的约束条件), etc.
疑问
NVI手法中涉及到derived class时base class private virtual 函数我们无法调用,但是我们需要redefining这些我们不会调用的private virtual 函数,看起来十分的矛盾。但时调用virtual函数表示它在”何时“被完成,但重定义virtual函数表示”如何“完成,这两者并不冲突。
NVI允许derived重新定义virtual函数,从而赋予了它如何具体实现的机能,但base class仍然保留函数合适被调用的权力
特别的
NVI手法中的virtual函数并不是非得是private。某些继承体要求在derived class中对应的实现必须调用器base class的对应兄弟,为了合法,那么就必须的设置为protect。
Strategy Pattern via Function Pointers
设计主张
”人物的健康指数的计算与每个人物的类型无关“,这样的计算不需要人物这个成分。
1 | class GameCharacter; |
优点
相比virtual函数继承,这种设计模式提供了更好的弹性
实例
- 在同一类型的不同的实体中应用不同的计算函数
1 | class EvilBadGuy: public GameCharacter { |
- 已知人物的健康指数计算函数可在运行期变更。例如:base类可以提供一个成员函数setHealthCalculator,用来替换当前的健康计算函数
Strategy Pattern via tr1::function
函数指针的限制
对template以及他们的的隐式接口的使用,基于函数的指针的做法就十分的死板。不够灵活,例如返回类型只能是int,函数对象不能是member function
tr1::function
改用tr1::function的对象替代函数指针,这样的对象可持有任何可调用物(callable entity 函数指针、函数对象、成员函数指针)。
1 | class GameCharacter; // as before |
在这个实例中我们用tr1::function instantiation来代替目标签名式。那个签名代表的函数时”接受一个reference 指向const GamCharacter“,并返回int。这个tr1::function类型产生的对象可持有任何与此签名式兼容的可调用物。例如可调用物的参数可以被隐式的转换为const GameCharacters&,其返回类型可以被隐式转换为int
tr1::function对象相当于指向函数的泛化指针。
更具与弹性
1 | short calcHealth(const GameCharacter&);// health calculation |
解析ebg2 -> bind
1 | EvilBadGuy ebg2( // character using a |
GameLevel::health接受两个参数,一个是隐式参数currentLevel,也就是this指向的那个、另一个是reference指向GameCharactor。
GameCharacters的健康计算函数值接受单一参数:GameCharacters。
使用GameLevel::health作为ebg2的健康计算函数,我们需要以特殊方式转换,取出GameLevel其中的健康计算函数。
本例中用currentLevel作为ebg2的健康函数所需的GameLevel的对象。_1意味着用currenLevel作为GamLevel的对象
the “Classic” Strategy Pattern
在该图中指示了GameCharacte是某个继承体系的根类,EviBadGuy与EyeCandyCharacter都是derived classes:HealthCalcFunc是另一个继承体系的根类。
实现代码
1 | class GameCharacter; // forward declaration |
总结
✦Alternatives to virtual functions include the NVI idiom and variousforms of the Strategy design pattern. The NVI idiom is itself an ex-ample of the Template Method design pattern.
✦A disadvantage of moving functionality from a member function to afunction outside the class is that the non-member function lacks ac-cess to the class’s non-public members.
✦tr1::function objects act like generalized function pointers. Such ob-jects support all callable entities compatible with a given target sig-nature.
条款36:绝不重新定义继承而来的non-virtual函数
Never redefine an inherited non-virtual function
non-virtual性质
在条款34中描述了non-virtual函数会给class建立一个不变性(invariant),凌驾其特异性(specialization)
示例
1 | class B { |
上述示例中我们都会调用B::mf() 版函数,但是如果我们在D class中重写mf()那么我们会发现以下问题
1 | class D: public B { |
non-virtual函数是一种静态绑定(statically bound)
在子类中重写non-virutal函数,它会根据其**声明式** (也就是D* ,B*)来选取函数执行,但实际上pB与pD指向都是**同一对象**,按理来说应该调用同一对象的函数,因此重写non-virtual函数会**导致破坏多态性**。
public继承关系
public继承关系”is-a”关系,那么non-vitual函数的作用(不变性凌驾于特异性):
适用于B对象的每一件事,也适用与D对象
B的derived classes一定会继承mf的接口与实现,因为mf是B的一个non-virutal函数
多态性的虚构问题
条款7: virtual析构函数,对于B pd = new D();由于声明的是non-virtual的析构函数,那么执行的时候会根据*声明式来定义 静态绑定调用函数,因此在多态中只会使用B的析构函数,对于D的析构则不会调用,这时候有些D类的成员不能被析构,会导致内存泄漏问题
总结
✦Never redefine an inherited non-virtual function.
条款37:绝不重新定义继承而来的默认参数值
Never redefine a function's inherited default parameter value
virtual函数系动态绑定,然而默认的却是静态绑定
静态绑定
静态绑定义
在程序中被**声明时**所采用的类型,静态绑定容易造成的问题如:继承类重写non-virtual函数
简单来说就是调用对象是采用声明对象的一部分行为
1 |
|
继承关系
指针(静态类型)
1 | Shape *ps; // static type = Shape* |
动态绑定
动态绑定定义
普遍的来说是多态性,由一个**静态类型**的对象指针(引用)指向一个子类对象,在运行其就会将行为于其指向的对象进行绑定,调用子类对象的行为。
简单来说就是调用指向对象的行为
动态类型
1 | ps = pc;// ps’s dynamic type is// now Circle* |
调用
1 | pc->draw(Shape::Red);// calls Circle::draw(Shape::Red) |
问题分析:动态绑定与静态绑定冲突
在pr->draw();中出现了问题,pr的动态类型为Rectangle调用为virtual函数,但Rectangle::draw默认参数为应该时GREEN,但由于pr的静态类型为Shape*,所以此一调用的默认阐述时来自于Shape class,而不是来之于Rectangle class。这个函数时两个类共同完成的
NVI解决方案
1 | class Shape { |
non-virtual 函数一个不会被derived classes重写(条款36),所以这个设计很清楚地使用了color的默认值为Red,相当于强制性不让动态绑定选择静态绑定的参数,NVI手法将动态绑定和静态绑定通过**private在继承中可见性**,进行了巧妙的结合
总结
✦Never redefine an inherited default parameter value, because defaultparameter values are statically bound, while virtual functions — theonly functions you should be redefining — are dynamically bound.
条款38:通过复合塑造出has-a或“根据某物实现出”
Model "has-a"or"is-implement-in-terms-of" throught composition
复合类型
在一个类中的数据成员是一个或者多个自定义数据类型
1 | class Address { |
has-a与is-a
在上述Person中定义了name、address、voiceNumber、faxNumber,我们都很容易说,这个人有一个名字、地址、号码,但我们不能说这个人是一个名字,另外is-a是一种继承关系
复合类型中的has-a和“根据某物实现出”区分
has-a是应用域
is-implement-in-terms-of是实现域
用Set< T >继承list< T >声明如下
1 | template <typename T>//将list应用于Set |
根据上述继承关系很容易区分出错误,list可以插入相同元素,Set不能含有相同元素,因此在逻辑上,Set不适用于list的逻辑,因此也不是is-a关系,所以对于这两种关系不能用public来实现.
正确做法
1 | template<class T>// the right way to use list for Set |
通过复合类型可以很明显的看出关系,Set只是依赖list来实现
1 | template<typename T> |
总结
✦Composition has meanings completely different from that of publicinheritance.
✦In the application domain, composition means has-a. In the imple-mentation domain, it means is-implemented-in-terms-of.
条款39:谨慎的使用“private”继承
use private inheritance judiciously
Private 继承
示例
1 | class Person { ... }; |
private继承
根据以上代码的展示,我们会发现private继承与public继承不是同一个含义,同样的public继承会产生一些逻辑上的错误,例如学生不是人
- private继承不会有隐式转换,同样不能多态
- private的语义为根据某物实现( is-implemented-in-terms-of),不会对外呈现父类的接口
- private继承中base的成员都会变为private无论是protected还是public
因此,private只在软件的实现中会有意义,在软件的设计中毫无意义
复合与private继承的抉择
private继承与复合都有根据某物实现( is-implemented-in-terms-of)的概念。
取舍
尽可能的选择复合,必要时才会使用private(当protected成员或virtual函数被牵扯进来)
使用Private继承
1 | class Timer { |
对于virtual函数,我们需要用private继承
上述代码当我们需要对一个Widget类进行计时,在运行期中周期性的检查Widget类。对于Timer这一个计时器,Widget中可以重新定义Timer内的virtual函数,但用public检查就说明Widget是一个Timer那肯定是不符合实际的。对于private继承确实是完美的选择:
- Widget会拥有Timer的一些实现,因此也是根据某物实现。
- 用户也不会造成接口的滥用,该此Timer实现也是对Widget对象内可见的。
复合实现
1 | class Widget { |
用复合类的方法实现private继承同样也是可以的,但是略显复杂
总结
✦Private inheritance means is-implemented-in-terms of. It’s usually inferior to composition, but it makes sense when a derived classneeds access to protected base class members or needs to redefineinherited virtual functions.
✦Unlike composition, private inheritance can enable the empty baseoptimization. This can be important for library developers who strive to minimize object sizes
条款40:明智而谨慎地使用多重继承
多重继承
1 | class BorrowableItem {// something a library lets you borrow |
解决方案
1 | mp.ElectronicGadget::checkOut(); |
因为在MP3player中又两个相同的接口,因此在调用是会无法识别到底调用那个,因此只能指定数据成员
但是会得到一个尝试调用private成员的错误
菱形继承
1 | class File { ... }; |
以上的继承路线有两个条那么,假设File有一个filename数据成员分别继承到InputFile和OutputFile,当IOFile进行多重继承,那么我们会得到两份filename(InputFile::filename,Output::filename)。
得到的IOFile中如果要操作filename这数据成员要指定来自于那个父类同上。但再逻辑上这是不符合逻辑,一个文件不可能拥有两个名字。这时候就需要虚拟继承。
虚拟继承
含义
解决多继承时的命名冲突和冗余数据问题,使得在派生类中只保留一份间接基类的成员。
防止二义性问题,共享Top-Base类数据。
虚拟继承与普通继承的区别
时间在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。(虚拟就是运行期进行选择)
空间由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之 多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证 这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。
示例
1 | class File { ... }; |
这样的做法就会使得再最终的outputFile中仅有一个一份filename,且不会产生二义性
虚拟继承的成本
空间
virtual继承的class产生的non-virtual的继承体积大
时间
访问virtual继承的成员变量时,比访问non-virtual base classe的速度慢
虚拟继承的初始化规则更复杂
初始化职责有继承中的最底层承担
- classes若派生自virtual bases而需要初始化,必须要知道其virtual bases
- 当一个新的derived class加入继承体系中,它必须承当其virtual bases的初始化职责
虚拟继承的抉择
- 非必要不用虚拟继承,就用non-virtual 继承
- 必须使用virtual base classes,尽可能避免再其中放置数据
总结
✦Multiple inheritance is more complex than single inheritance. It canlead to new ambiguity issues and to the need for virtual inheritance.
✦Virtual inheritance imposes costs in size, speed, and complexity ofinitialization and assignment. It’s most practical when virtual baseclasses have no data.
✦Multiple inheritance does have legitimate uses. One scenario in-volves combining public inheritance from an Interface class withprivate inheritance from a class that helps with implementation.CPersonIPersonPersonInfo{private}
模板与泛型编程
条款41:了解隐式接口和编译期多态
Understand implicit interfaces and compile-time polymorphism
面对对象
- 提供显示接口
- 运行期多态
1 | class Widget { |
分析
- 显示接口:在本例中我们在
doProcessing
中调用w的接口,我们可以在源文件中找到这些接口(.h文件),必须知道接口的实现。 - 运行期多态:在条款37中有动态类型绑定,widget中virtual函数表现出来的为运行期多态
Template以及泛型编程( generic programming )
面对对象的规则依然存在但是被弱化了
- 隐式接口
- 编译期多态
1 | template<typename T> |
- w所支持的接口,是由template中的w身上的操作来决定,w需要提供一系列的隐式接口
- w的任何函数的调用,入operator> and operator!=,有可能造成template的据具现化(instantiated),这些函数的调用都是具现化在编译期。通俗来讲就是,通过模板类型T去判断T中是否有以下行为(接口),如果没有则会编译失败,如果存在则进行选择
运行期多态与编译期多态
区别
- 运行期多态:那个virtual函数被选择
- 编译期多态:那个重载函数被调用
隐式接口与显式接口
显式接口
通常是由函数的签名式(函数名称、参数类型、返回类型)构成
1 | class Widget { |
如上public接口有一个构造函数、析构函数、各个成员函数及其参数类型、返回类型、常量性构成。
隐式接口
它不基于函数签名式,是由有效表达式(valid expression)组成。
1 | template<typename T> |
如上:
T的隐式接口有一系列的约束
- 必须提供size函数
- 必须提供operator !=的类型比较函数(假设
someNastyWidget
的类型为T)
隐式接口同样需要支持T类型
总结
✦Both classes and templates support interfaces and polymorphism.
✦For classes, interfaces are explicit and centered on function signatures. Polymorphism occurs at runtime through virtual functions.
✦For template parameters, interfaces are implicit and based on valid expressions. Polymorphism occurs during compilation through template instantiation and function overloading resolution.
条款42:了解typename的双重意义
Understand the two meaning of typename
class and typename 声明
1 | template <class T> class widget; |
class 与 typename没有任何不同。当声明template类型参数,class和typename的意义完全相同
typename的另一重意义
声明“类型”
当有static成员变量与T::~定义的类型命名冲突时会造成编译器的警告,因此需要typename的显式的声明这是命名为一个类型,而不是变量
示例
1 | template<typename C>// typename allowed (as is “class”) |
上述的C不是嵌套从属类型名称,所以声明container时并不需要typename为签到,但C::iterator是个嵌套从属类型因此需要typename作为前置声明
例外
typename不能出现在base classes list内的嵌套从属类型名称之前,也不可以在member initalization list中作为base class修饰符
1 | template<typename T> |
typename 与 typedef连用
1 | template<typename IterT> |
总结
✦When declaring template parameters, class and typename are inter-changeable.
✦Use typename to identify nested dependent type names, except inbase class lists or as a base class identifier in a member initializa-tion list
条款43 :学习处理模板化基类内的名称
Know how to access names in templatized base classes
示例
假设一个程序可以发送消息给不同公司,信息可以以密文或则明文的方式发送,用类模板这可以在编译期对公司进行选择
1 | class CompanyA { |
MsgSender类的调用不会用任何的问题
添加派生类LoggingMsgSender
1 | template<typename Company> |
:ice_cream: 这个派生类在新的一个non-virtual member函数中调用了父类的non-virtual member函数,这个函数解决了(条款33和条款36)non-virtual函数在派生类中出现的一系列问题,但是这个代码在不同的编译器是不能编译的
问题所在
编译器遇到LoggingMsgSender的模板定义,不知道它是继承的那个类。
因为Company是一个模板参数,它是不确定的一个参数,只有当LoggingMsgSender被实例化后Company才会确定,因此在MsgSender< Company >的派生类中会出现无法确定继承的父类的问题。
更加明确的说是不知道Company中是否有sendClear这个函数
模板全特化
1 | class CompanyZ {// this class offers no |
通用的MsgSender模板是不适用于CompanyZ的,因为模板提供的sendClear函数是对CompanyZ没有意义的。因此在仍然会出现上述问题,在Company中找不到sendClear的声明(因为CompanyZ的特例化没有定义sendClear函数)
解决方案:
方案一:在base class函数调用动作之前加上this->
1 | template<typename Company> |
方案二:使用using声明
在条款33中找不到基类重载继承下来的隐藏的函数,是因为被派生类所隐藏
在本例中是编译器不搜索基类作用域
1 | template<typename Company> |
方案三:显式的指定你的函数位于base class中
1 | template<typename Company> |
这是一个不太好的方法,因为被调用的是virtual函数会导致virtual函数的绑定行为被关闭
总结
✦In derived class templates, refer to names in base class templatesvia a “this->” prefix, via using declarations, or via an explicit base class qualification.
条款44:将于参数无关的代码抽离templates
Factor parameter -independent code out of templates
不恰当的使用template可能会导致代码膨胀(code bloat):其二进制带着重复的代码、数据
解决方法:当两个函数实现的实质相同
抽离两个函数中共同的部分,将他放入第三个函数中,然后将他们调用这个新函数。class也是同样的道理,使用继承或则复合
示例
用固定尺寸的方阵编写一个template。
1 | template<typename T,// template for n x n matrices of |
其中的类型为size_t的参数是一个非类型参数(non-type parameter)。
对上述代码进行调用
1 | SquareMatrix<double, 5> sm1; |
分析
上述代码中,将会具现化两份invert函数,但这两份函数是完全相同,因为其中一个操作的55矩阵而另一个是10 10的矩阵,除了常量5和10其他部分完全相同,这将是一个典型的代码膨胀示例
解决方案一
1 | template<typename T>// size-independent base class for |
上述解决方案中
在所有方阵实体中只会共享一个父类的invert实现,这样就有效的防止的代码膨胀
- 避免derived class代码重复:父类使用了protect代替了public。注
在调用时会如果时public(实体对象仍然可以调用该接口)的话也同样会产生不同版本的代码
- 调用其代码的成本为0,因为derived classes的inverts调用base clas的版本是inline调用
- this->调用表示模板化基类反之函数名称被隐盖
- 使用private的继承关系表现的是一种is-a关系
问题:在该方案中没有解决父类与子类之间联系的问题,因为在子类中需要带入矩阵的相关数据,因此需要加入一个指针或者引用。但是反复的传参,这样也会影响效率
解决方案二:父类中存储一个指针,指向所在的内存
1 | template<typename T> |
这种方法在子类中调用了父类的构造函数,用来初始父类中的数据成员
当数据成员特别大的时候可以使用动态内存分配
1 | template<typename T, std::size_t n> |
总结
✦Templates generate multiple classes and multiple functions, so anytemplate code not dependent on a template parameter causes bloat.
✦Bloat due to non-type template parameters can often be eliminatedby replacing template parameters with function parameters or classdata members.
✦Bloat due to type parameters can be reduced by sharing implemen-tations for instantiation types with identical binary representations.
条款45:使用成员函数template接受所有可以兼容的类型
Use member function template to accept “all compatible type”
智能指针
行为像指针的对象,并提供指针没有的技能。STL容器几乎都是用到智能指针,但是我们不会使用“++”的运算符将一个内置的指针从link list的节点移动到另一个节点,所以需要用到迭代器
真实指针(raw pointer)
支持隐式转换。例如:Derived class 指针可以隐式的转换为base class指针(提供多态的选择),“指向non-const对象”的指针可以转为”const对象”
1 | class Top { ... }; |
自定义智能指针
1 | template<typename T> |
上述同一个template的不同具现体(instantiation)之间不存在继承关系,所以SmartPtr< Top >与SmartPtr< Middle >是完全不同的class
Templates和泛型编程(Generic Programming)
生产需求:自定义指针构造函数的编写
当我们添加一个新继承关系的对象时,那没有添加转型的构造函数的情况下,就会反复的在SmartPtr中添加构造函数
class BelowBottom: public Bottom { ... };
member function tempaltes——泛化copy构造函数
1 | template<typename T> |
上述代码中对于任意类型T与任意类型U,可以根据SmartPtr< U >生成一个SmartPtr< T >——因为SmartPtr< T >有个构造函数接受有个SmartPtr< U >参数。
泛化的copy构造函数并未被声明为explicit,因为原始指针之间(base class与derived class之间)的转换是隐式的转换,无需明白的写出转型动作(cast)
提供原始资源的成员函数
1 | template<typename T> |
使用成员初始化列表来初始化SmartPtr< T >之内类型为T的成员变量,并以类型为U的指针作为初值。
member initialization templates
成员初始化列表的作用不限于构造函数,另一个作用是支持赋值操作
摘录
1 | template<class T> |
上述所有构造函数都是explicit,唯有”泛化copy构造函数”除外,那么从某个shared_ptr类型隐式转为另一个shared_ptr是允许的,但是从某个内置指针或从其他智能指针进行隐式转换则是不允许的(显示的转换倒是可以)。
总结:
✦Use member function templates to generate functions that accept allcompatible types.
✦If you declare member templates for generalized copy constructionor generalized assignment, you’ll still need to declare the normalcopy constructor and copy assignment operator, too
条款46:需要类型转换是请为模板定义非成员函数
Define non-member function inside templates when type conversion are desired
这个条款换个说法是:当我们需要进行模板函数参数类型需要隐式转换时,将模板函数定义为friend函数
示例:条款24的例子转为模板
1 | template<typename T>class Rational { |
进行以下混合式(mixed-mode)运算
1 | Rational<int> oneHalf(1, 2);// this example is from Item24, |
出现以下问题:
分析
以上问题是因为没有找对应的operator * 操作符对应的函数,也就是推导失败。
以上代码中operator*这个non-member函数的两个参数进行隐式类型推导时出现了问题:
- 第一个实参是onehalf,所以T一定是int,能够顺利的推导出
- 第二个实参是2,编译器无法将其推导为Rational< int >
经过以上考虑,因该是将隐式转换类型转换函数出现调用失败的问题,无法将non-member函数指定为operator*
friend函数声明
template class中friend声明式可以指定特定的函数,Rational< T >可以声明operator是*class的一个friend函数
class template并不依赖template的实参推导,所以能够在class Rational< T >具现化时得知T,换句话说就是T的类型被确定后,就已经指定调用operator*函数
1 | template<typename T>class Rational { |
此时混合式调用可以通过编译,当对象onehalf被声明为一个Rational< int >,class Rational< int >被具现化出来了。而作为过程的一部分,friend函数operator(接受Rational< int > 参数)也就被自动声明出来。后者身为*一个函而非函数模板,因此编译器可以调用它时使用隐式转换函数。
上述friend函数同样可以声明为
1 | friend |
链接错误
虽然经过修改我们能过通过编译,但是在链接时会出现上述问题了,因为friend函数只有一个声明式存在,并没有被定义,因此会导致连接器无法找到对应的实现
函数本体与声明式结合(简单版)
1 | friend |
friend特殊意义
在本条款中虽然使用了friend却和它的传统意义不同(访问non-public成分),但是在此的意义却是让类型转换发生于所有实参身上,我们需要一个non-member函数(条款24);为了使这个函数自动具现化(隐式转换构造函数的指定),我们需要将它声明在class内部;而在class内部声明non-member函数的唯一方法就是将其声明为friend函数
non-member与friend member合作
1 | template <typename T> |
总结
✦When writing a class template that offers functions related to thetemplate that support implicit type conversions on all parameters,define those functions as friends inside the class template.