1 让自己习惯C++
条款01 视C++为一个语言联邦
C++的四个次语言:
1. C
区块、语句、预处理器、内置数据类型、数组、指针等都来自于C。
但纯C语言没有模板,没有异常,没有重载……
2. Object-Oriented C++
继承、封装、多态
3. STL
容器、迭代器、算法
4. Template C++
这是高阶部分。
C++的泛型编程部分。
即template metaprogramming(TMP,模板元编程)link
内置类型(C-like)->使用pass-by-value更高效(适用于C part of C++、STL的迭代器即指针)
用户自定义类型(user-defined)->使用pass-by-reference-to-const高效(Object-Oriented C++中自定义的构造析构函数、Template C++)
条款02 尽量以const,enum,inline替换#define
一、对于单纯常量,使用const对象或enums替换#define
1. 或许#define不视为语言的一部分
#define ASPECT_RATIO 1.653
编译错误信息可能会提到1.653而不是ASPECT_RATIO,会误导人。
使用const替换宏:
const double AspectRatio = 1.653; // 大写名称通常用于宏,因此这里改变名称写法
2. const的作用域
cpp的const只作用于本文件,不作用于其他文件(和static一样),如果要作用于其他文件,需要加extern。
3. const作为class的专属常量
为了保证常量在类中至多只有一个实体,所以使用static const
PS:区分定义式和声明式:
声明是告知编译器该程序元素的名称以及类型,定义则是使编译器为程序元素分配内存空间。二者最根本的区别就是是否分配内存。声明不会导致内存的分配,而定义会分配内存。在C++程序中声明可以有多次,但是定义只能有一次。link
非static中:
extern int i; //声明
extern int p = 123; //定义
类中的static const:
class GamePlayer {
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns]; //使用该常量
static const double FudgeFactor; //常量声明式
}
// 如果不取地址,可以不用写,大部分编译器此种情况不需要看到一个定义式
const int GamePlayer::NumTurns; //定义式(不用写static)
const double GamePlayer::FudgeFactor = 1.35; //常量定义式(不用写static)
注意:
(1)大多数编译器可以支持在类中声明式指定int和char类型的初始值,但不支持double等类型的初始值。
(2)如果在类中声明式给了初值,则定义式不能再给初值。
4. 使用enum hack替代const或者#define
class GamePlayer {
private:
enum { NumTurns = 5 }; //不能取enum的地址
int scores[NumTurns];
}
二、对于形似函数的宏,使用inline函数替换#define
使用宏函数可能会导致错误。
//写宏的时候多加小括号,但尽管如此还是可能会出错
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a被累加两次
CALL_WITH_MAX(++a, b + 10); //a被累加一次
在调用f之前,a的递增次数竟然取决于“它拿来和谁比较”。
使用template+inline函数替换#define
此方法同时具有宏带来的效率和一般函数带来的安全性。
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
条款03 尽可能使用const
1. 判断顶层还是底层const
const在*的左边,表示被指物是常量(底层const)const Widget* pw
或 Widget const* pw
const在*的右边,表示指针是常量(顶层const)或者Widget* const pw
// T* const 指针是常量
const std::vector<int>::iterator iter = vec.begin();
// const T* 被指物是常量
std::vector<int>::const_iterator iter = vec.begin();
2. const与重载
const修饰成员函数时,可以被重载。
(1)const对象调用它的const成员函数。
(2)非const对象调用他的非const成员函数。
底层const,可以被重载
顶层const,不可以被重载
非指针或引用的类型的const不能被重载
// 此种情况不能被重载
void ReadText(Text t)
void ReadText(const Text t)
// 此种情况可以被重载
void ReadText(Text& t)
void ReadText(const Text& t)
void ReadText(Text* t)
void ReadText(const Text* t)
3. const成员函数
bitwise constness的观点:const成员函数不应该修改class的任何非static的的内容。
logical constness的观点:const成员函数可以修改它所处理对象的某些bits,但只有再客户端侦测不出的情况下才可以如此。
// logical constness情况之一
class CTextBlock {
public:
char& operator[](std::size_t position) const {
return pText[position]; //返回后仍然能修改对象内容,且能通过测试
}
private:
char* pText
}
// logical constness情况之二
// const成员函数可以修改对象中mutable成员的内容。
4. 在non-const成员函数调用const成员函数,避免重复
需要借助于const_cast和static_cast
class TextBlock {
public:
const char& operator[](std::size_t position) const {
...
return text[position];
}
char& operator[](std::size_t position) {
...
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[position]);
}
}
避免重复的方法:
在非const成员函数中,首先将this转换为const,然后调用它的const成员函数,最后将调用结果去除const。
但不能反过来,不能用const成员函数去调用非const的成员函数(因为一般情况下,const成员函数不修改对象内容)
条款04 确定对象被使用前已先被初始化
1. 保证内置类型被手工初始化
若不给值,内置类型的初始化值为随机值
STL的初始化值是一个静态的值
为了不这么麻烦的区分,保证所有对象使用前都被初始化了。
2. 构造函数使用member initialization list初始化对象
使用构造函数初始化时,在{}之前使用成员初始化列去初始化值,效率更高。
使用成员初始化列,是直接使用给定值 构造对象,效率高;
如果在构造函数{}内部初始化,是先调用默认构造函数,再赋值,效率更低。
class成员变量总是以声明的次序初始化。注意成员初始化列的初始化顺序,不是书写顺序,是class中声明的顺序
一般可以让初始化书写顺序和class声明的顺序保持一致。
3. 不同编译单元的全局变量初始化顺序
编译单元(translation unit),是指产出单一目标文件的那些源码。基本是同一源码文件加上其所含入的头文件。
所以static只在同一编译单元中起作用。
对于extern全局变量,保证使用前已经初始化了。
//提供方的文件
class FileSystem {
public:
std::size_t numDisks() const;
};
extern FileSystem tfs; //预留给客户使用的对象
//使用方的文件
class Directory {
public:
Directory(params);
...
};
Directory::Directory(params) {
std::size_t disks = tfs.numDisks(); //这种使用方式没有保证tfs被初始化
}
为了避免上述代码的情况,使用类似于单例的方式。
使用reference-returning函数。
//提供方的文件
class FileSystem {
...
static FileSystem& tfs();
};
FileSystem& FileSystem::tfs() { //
static FileSystem fs;
return fs;
}
//使用方的文件
class Directory {...}
Directory::Directory(params) {
std::size_t disks = FileSystem::tfs().numDisks(); //这种使用方式没有保证tfs被初始化
}
注意需要考虑在多线程下的安全性,防止race conditions(因为懒汉式初始方法,可能会在多个线程中同时创建对象,导致竞争问题出现)
因此需要在程序的单线程启动时就调用一次reference-returning函数,即代码中的FileSystem::tfs(),创建FileSystem对象,后续使用时再调用FileSystem::tfs()。
注意!!
*meyers的单例形式(使用局部静态变量),在C++11之后,是线程安全的。
C++11之前,这种懒汉式的局部静态变量,需要在程序启动时先初始化一次(这样才能保证线程安全),避免多线程同时创建这个对象。
但C++11之后,加入了magic static**的特性:如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
2 构造/析构/赋值运算
条款05 了解C++默默编写并调用哪些函数
1.若没有写,编译器会自动生成拷贝构造函数、拷贝赋值函数、析构函数,默认构造函数,且这些所有函数都是public且inline。
2.编译器产出的析构函数是非虚函数的,除非基类有虚函数。
3.在语义不符合情况下,编译器会拒绝自动生成拷贝赋值函数,主要包括以下几种情况: (如果要使用拷贝赋值函数,均需要自己去实现)
(1)如果class内含有reference成员,则编译器不会自动生成拷贝赋值函数,因为原有的reference不能被更改。
(2)如果class内含有const成员,则编译器不会自动生成拷贝赋值函数,因为原有的const不能被更改。
(3)如果基类把拷贝赋值函数声明为private,则编译器不会自动生成拷贝赋值函数。
template<class T>
class NamedObject {
public:
NamedObject(std::string& name, const T& value);
...
private:
std::string& nameValue; // 因为有reference成员,不能自动生成拷贝赋值函数
const T objectValue; // 因为有const成员,不能自动生成拷贝赋值函数
};
条款06 若不想使用编译器自动生成的函数,就应该明确拒绝
方式一:
将拷贝构造函数或者拷贝赋值函数声明为private,可以阻止用户调用它。
但是这种情况,成员函数和友元函数还是可以调用这个private函数。
方式二:
只有声明,没有定义。
class HomeForSale {
public:
private:
//只有声明,没有定义
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
当用户企图拷贝对象时,编译器会拒绝,当不慎成员函数或友元函数调用它,链接器也会拒绝(因为没有定义)
PS:只有纯虚函数支持只有声明,没有定义的继承。
方式三:
定义一个Uncopytable的基类,当成员函数或友元函数调用它,在编译器就会报错,更早发现问题。
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale: private Uncopyable {
...
};
在编译器拒绝,因为base类的拷贝函数是private的。
这里也可以写public继承,写private继承的好处在于,告诉编译器Uncopyable这个类不存在,我只是在HomeForSale里使用了这个类的public成员函数(如构造函数)。见条款39。
方式四:
C++11定义了delete,将拷贝函数声明为=delete同样可以组织拷贝。
和private的区别有两点:
(1)=delete后,成员函数和友元函数将不可以调用。
(2)在模板中,delete可以单独禁止某一种特殊的类型的拷贝。
class Widget {
public:
template<typename T>
void ProcessPointer(T* ptr) { … }
};
template<>
void Widget::ProcessPointer<void>(void*) = delete; // still public, but deleted
方式五:
将delete定义到一个宏
// 定义一个公共的.h文件
#define DISABLE_COPY_AND_ASSIGN(name) \
name(const name&) = delete; \
name& operator=(const name&) = delete
// 自定义的类需要包含上面的.h文件
class HomeForSale: {
public:
...
DISABLE_COPY_AND_ASSIGN(HomeForSale);
};
条款07 为多态基类声明virtual析构函数
vitural析构函数的的存在原因:
Base* ptk = new Derived();
delete ptk;
// 如果Base的析构函数不是虚函数,则只能析构Base的部分,则会造成内存泄露。
(1) 任何class只要带有virtual函数都几乎确定应该有一个virtual析构函数
(2) 不乱用虚析构函数,因为它会占用空间。
(3)如果想使用抽象class,但是手上没有虚函数,可以将析构函数声明为纯虚函数。但在使用的时候,需要提供一份定义。link
class AMOV {
public:
virtual ~AMOV() = 0;
};
AMOV::~AMOV() {} // 提供一份定义
条款08 别让异常逃离析构函数
1.vector在离开作用域空间时会自动调用其内含的所有classA的析构函数。vector<classA> v
2.C++不禁止在析构函数发生异常,但是不要让析构函数吐出异常,应该让析构函数自己吞掉异常(即捕捉异常,然后处理(不传播)或者结束程序)。
class DBConnection {
public:
static DBConnection create();
void close();
};
为了防止客户不忘记在DBConnection上调用close,一个解决方法是使用一个DBConn来管理这个资源。
class DBConn {
public:
~DBConn() { // 确保数据库总是被关闭了
db.close(); // 但是未处理异常
}
private:
DBConnection db;
};
但这种用法,如果析构发生异常,此析构函数会传播异常,离开此析构函数,所以需要处理这个异常,不要让它离开析构函数。
try catch:
DBConn::~DBConn() {
try { db.close(); }
catch (...) {
// LOG记录,处理异常
std::abort(); // 若没有则表示允许此异常。
}
}
3.将处理问题的机会交给用户,提供一个public普通函数让用户可以对此异常进行处理。
class DBConn {
public:
...
void close() { // 供客户调用的函数,出了异常客户可以自己解决。
db.close();
closed = true;
}
~DBConn() {
if (!closed) { // 可以追踪被管理的类是否被关闭
try {
db.close(); // 若客户不做处理,析构时也可以由提供方处理。
}
catch (...) {
// LOG记录,处理异常
}
}
}
private:
DBConnection db;
bool closed;
};
条款09 绝不在构造和析构过程中调用virtual函数
基类构造期间,虚函数不是虚函数,此时不会下降到继承类阶层,相当于还在调用基类。(virtual、dynamic cast都不行)
class Transaction {
public:
Transaction() {
init();
}
virtual void logTransaction() const = 0;
private:
void init() {
logTransaction(); // 没有办法调用基类的logTransaction函数
}
};
如果要在构造时调用实现不同的方法,可以这样进行,子类构造时调用基类(而非基类调用子类)
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // 非虚函数
};
Transaction::Transaction(const std::string& logInfo) {
logTransaction(logInfo);
}
class BuyTransaction: public Transaction {
public:
BuyTransaction(parameters): Transaction(createLogString(parameters)) { }
// 将log信息由子类传给基类
private:
static std::string createLogString(parameters); // 静态函数
}
}
条款10 令operator= 返回一个reference to *this
这是一个协议,大部分如string、vector等都这样定义
让operator=返回一个reference to *this。(包括拷贝赋值函数,或者普通对=的重载,甚至所有赋值相关运算,如+=、-=、*=)
因此=可以支持连锁赋值。
int x, y, z;
x = y = z = 15;
// 和下面一个意思。先赋值15给z,然后将结果(更新后的z)赋给y,更新后的y赋给x。
x = (y = (z = 15));
可以这样操作的原因是因为=的定义:
class Widget {
public:
// 拷贝赋值函数
Widget& operator=(const Widget& ths) {
// ...
return* this;
}
// 重载+=、-=、*=
Widget& operator+=(const Widget& ths) {
// ...
return* this;
}
// 即使操作符参数不是Widget也适用
Widget& operator+=(int ths) {
// ...
return* this;
}
}
条款11 在operator= 中处理“自我赋值”
a[i] = a[j]
或者*px = *py
这种情况叫自我赋值。在定义拷贝构造函数/定义operator=时,自我赋值需要经过证同测试以保证安全。
1. 保证自我赋值安全
Widget& Widget::operator=(const Widget& rhs) {
if (this == &ths) return *this; // 证同测试
// 如果是自我赋值,则不做任何事。
delete pb; // 如果没有证同测试,delete就不止销毁了当前对象,还销毁了rhs
pb = new Bitmap(*rhs.pb);
return *this
}
虽然它保证了自我赋值安全,但是没有保证异常安全。
当new失败(内存不足或者Bitmap拷贝构造函数异常)的时候,Widget最终会持有一个指针指向一块被删除的Bitmap。
2. 同时保证了异常安全和自我赋值安全
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb; // 记住原来的pb
pb = new Bitmap(*rhs.pb); // 相当于在赋值之后再删
delete pOrig; // 删除原来的pb(先记住这个指针,最后再delete!!!)
return *this
}
在赋值pb所指东西之后删除pb
3. copy and swap技术实现异常安全和自我赋值安全
class Widget {
void swap(Widget& rhs); // 交换*this和rhs的数据
};
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs); // 为rhs数据制作一份副本
swap(temp); // 将*this数据和上述副本的数据进行交换
return *this;
}
或者是运用by value的方式传递一份副本
Widget& Widget::operator=(Widget rhs) { // rhs是实参的一份副本
swap(rhs); // 将*this数据和rhs这个形参副本交换
return *this;
}
这个做法巧妙高效,但是牺牲了清晰性。
条款12 复制对象时勿忘其每一个成分
1. copy函数需copy所有local成分
当使用自定义的拷贝构造函数或者拷贝赋值函数(统称copy函数)的时候,增加新的成员变量一定要记住也要在copy函数中添加,一旦我们自定义了copy函数,编译器将不会警告这种错误。
2. 调用基类的copy函数构造子类的copy函数
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // 调用基类的拷贝构造函数
priority(rhs.priority) {}
PriorityCustomer&
PriorityCustomer::operatoer=(const PriorityCustomer& rhs) {
Customer::operator=(rhs); // 调用基类的拷贝赋值函数
priority = rhs.priority;
return *this;
}
3. 解决拷贝构造函数和拷贝赋值函数代码的重复的方法
不能用拷贝构造函数调用拷贝赋值函数。
不能用拷贝赋值函数调用拷贝构造函数。
如果拷贝构造函数和拷贝赋值函数有相近的代码,应该用一个新的成员函数给两者调用。这样的函数往往被命名为init。
3 资源管理
资源是指一旦用了它,将来必须还给系统。
资源包括比如动态分配的内存、文件描述器、互斥锁、图形界面的字型和笔刷、数据库链接、网络sockets等。
条款13 以对象管理资源
获得资源后立即放入管理对象内,这个观念被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization; RAII)。
管理对象运用析构函数确保资源被释放。
动态分配的内存,可以使用std::auto_ptr(类似于unique_ptr)或者std::tr1::shared_ptr(类似于shared_ptr)管理(C++11之前)。 但是std::auto_ptr与unique_ptr有一些不同,若通过copy构造函数或者copy赋值操作符复制它们,它们会变成null。
void f() {
// createInvestment返回一个raw指针
std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1); // pInv2指向对象,pInv1置为null
pInv1 = pInv2; // pInv1指向对象,pInv2置为null
}
注意,std::tr1::shared_ptr不能用来管理数组,需要自定义delete []array,或者使用vector/string,或者使用boost::scoped_array和boost::shared_array classes,或者使用模板类 default_delete。
std::shared_ptr<int>ptr(new int[10], std::default_delete<int[]>());
第二种方式:
智能指针重载了管理数组的版本。
然而直到c++17前std::shared_ptr都有一个严重的限制,那就是它并不支持动态数组。
#include <memory>
std::shared_ptr<int[]> sp1(new int[10]()); // 错误,c++17前不能传递数组类型作为shared_ptr的模板参数
std::unique_ptr<int[]> up1(new int[10]()); // ok, unique_ptr对此做了特化
std::shared_ptr<int> sp2(new int[10]()); // 错误,可以编译,但会产生未定义行为,请不要这么做
条款14 在资源管理类中小心copying行为
自己建立管理资源类。
1.有时需要禁止复制
class Lock: private Uncopyable { // 禁用copy函数
public:
explicit Lock(Mutec* pm)
: mutexPtr(pm) {
lock(mutexPtr);
}
~Lock() {
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
2.有时对底层资源使用“引用计数法”–浅拷贝
通常只要内含一个tr1::shared_ptr成员变量,RAII classes便可实现出reference-couting copying行为。
但是我们通常是自定义一个删除器,因为我们想要的释放动作是解除锁定而不是删除。
class Lock: {
public:
explicit Lock(Mutec* pm)
: mutexPtr(pm, unlock) { // 以unlock函数作为删除器
lock(mutexPtr.get()); // 条例15谈到get
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
// std::shared_ptr<Mutex, decltype(unlock)*> mutexPtr;
};
3.有时需要复制底部资源–深拷贝
4.转移底部资源所有权。
资源所有权从被复制物转为目标物(比如auto_ptr)
条款15 在资源管理类中提供对原始资源的访问(以及类型转换)
许多API调用的时候,不能使用RAII 类,需要访问原始资源类。
std:tr1::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment* pi);
int days = daysHeld(pInv.get());
这里的get是智能指针提供的成员函数,用来执行显式转换,此外,智能指针还重载了operator->和operator-*,他们允许隐式转换至原始指针。
1. 自定义显示转换(更安全)
FontHandle getFont();
void releaseFont(FontHandler fh);
class Font {
public:
explicit Font(FontHandle fh)
: f(fh) { }
~Font() {
releaseFont(f);
}
FontHandle get() const { // 提供显示转换
return f;
}
private:
FontHandle f;
};
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f.get(), newFontSize);
2. 自定义隐式转换(对客户更方便)
(1)隐式转换有两种,第一种是使用operator()
隐式转换函数的定义格式如下:
class C1 {
public:
// 格式operator 转换后类型() { return 转换后类型的变量; }
operator int() { return i; }
int i = 1234;
};
int i1 = C1;
自定义隐式转换的方法:
FontHandle getFont();
void releaseFont(FontHandler fh);
class Font {
public:
Font(FontHandle fh)
: f(fh) { }
~Font() {
releaseFont(f);
}
operator FontHandle() const { // 隐式转换函数
return f;
}
private:
FontHandle f;
};
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize); // 隐式转换的调用
但是隐式转换可能会导致一些问题,比如原意可能是想拷贝一个Font对象f,但是却将f隐式转换为底部的FontHandle,然后才复制他。如果f被销毁,资源被释放。f2就会成为虚吊的(dangle)。
FontHandle f2 = f;
(2)第二种隐式转换是写一个非explicit且只有一个实参的构造函数。
换言之,一个实参的构造函数可被当做隐式转换,而使用explicit可以禁用这种转换。
条款16 成对使用new和delete时要采取相同形式
C++存储单一对象和存储对象数组的方式可能不同。
一种内存布局方式如:
单一对象 |object|
对象数组 |n|object|object|object|…
new和delete需要保持一致。
如果new或者delete加入[](delete [] arrayn
),则说明是对象数组,若不加[]则只调用了一次析构函数。
如果对单一对象使用delete []的形式,加入内存布局如上述方式,则delete会读取若干内存并将它解释为数组大小,然后开始多次调用析构函数,产生错误。
特别是对数组使用typedef的时候尤其要注意(尽量不对数组形式做typedef)
typedef std::string AddressLines[4];
std::string* pal = new AddressLines;
delete [] pal; // 注意要加[]
条款17 以独立语句将newed对象置入智能指针
不能用如下方式初始化智能指针,如下方式是隐式转换,而智能指针禁用了隐式转换!!!
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
// 现在调用processWidget
processWidget(new Widget, priority()); // 错误,此形式是隐式转换!!!
可以用下述调用通过编译,但尽量不要这么做,有可能产生内存泄露!!
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
因为这三个动作的顺序,在不同C++编译器的优化下可能不一样。
(1)调用priority
(2)执行"new Widget"
(3)调用tr1::shared_ptr构造函数
调用priority有可能出现在第一或第二或第三顺位执行,有可能编译器优化后顺序是:
(1)执行"new Widget"
(2)调用priority
(3)调用tr1::shared_ptr构造函数
如果此时调用priority的调用导致异常,则new Widget的返回的指针将会遗失,因为它尚未置入tr1::shared_ptr内
最安全的方法是使用分离语句
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority()); // 这个调用动作绝不会造成内存泄露。
4 设计与声明
条款18 让接口容易被正确使用,不易被误用
1.阻止误用的方式
(1)使用简单的外覆类型(wrapper types)来区分(建立新类型)
比如表示日期,不单纯传int,而使用
struct Month {
explicit Month(int m) : val(m) {} // 建立新类型
static Month Jan() {return Month(1)}; // 束缚对象值
static Month Feb() {return Month(2)};
...
int val;
};
// 如此可避免单纯传int
Date d(Month::Mar(), Day(30), Year(1995));
(2)限制类型上的操作,比如使用const,可以避免这种赋值(a*b)=c
(3)束缚对象值(见上)
(4)消除客户的资源管理责任,可以使用智能指针
2.考虑接口的一致性很重要
3.智能指针使用定制的删除器,可以防范DLL问题
DLL问题指对象在一个动态链接库被创建,却在另一个动态链接库被销毁。
使用智能指针后,它缺省的删除器是智能指针诞生的时候所在那个DLL的删除器。
条款19 设计class犹如设计type
1.新type的对象应该如何被创建和销毁
2.对象的初始化和对象的赋值有什么样的差别(构造和赋值构造)
3.新type的对象如果被passed by value,意味着什么?(深拷贝)
4.什么是新type的“合法值”,是否需要限制
5.新type需要配合某个继承图系吗?如继承,需要看函数是否virtual。如需要被继承,则析构函数需要为virtual
6.新type需要什么样的转换(见第15条)
7.什么样的操作符和函数对此新type而言是合理的
8.什么样的标准函数需要被驳回(即delete或private某些构造函数)
9.谁该取用新type的成员,public、protected、private
10.什么是新type的“未声明接口”(undeclared interface)
11.你的新type有多么一般化
12.你真的需要一个新type吗?可能单纯定义一个或多个non-member函数或者templates变可以实现此目标。
条款20 宁以pass-by-reference-to-const替换pass-by-value
使用pass-by-reference-to-const的好处:
(1)效率高,pass-by-value有copy构造函数和析构函数的开销,pass-by-reference-to-const没有。
(2)避免对象切割(slicing problem),可以实现动态多态,pass-by-value会被转成基类类型。
使用pass-by-value的情形:(copy构造函数并不昂贵)
内置类型,STL的迭代器,函数对象。
条款21 必须返回对象时,别妄想返回其reference
只有在一种情况下可以返回引用
那就是meyer式单例,使用局部静态变量+reference返回
其他情况都不行,包括:
// 错误一:在栈上创建,别让reference指向某个local对象
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result; // 离开作用域会被销毁,别这样用。
}
// 错误二:在堆上创建,内存泄漏
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result; // 如果调用x*y*z则有两次new,内存泄漏。
}
// 错误三:使用局部静态变量,指向了同一个对象
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
static Rational result;
result = ...;
return *result; // 如果if((a*b) == (c*d)),此表达式一定会成立。
}
正确写法是,就让他返回一个新对象,让编译器来优化它
inline const Rational operator *(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
条款22 将成员变量声明为private
不要将成员变量声明为public,这种形式封装不好,一旦改变它,太多的客户代码均被改变。
应该用public成员函数去访问private成员变量,好处如下:
(1)成员变量被读取或写入时可以感知到,并通知其他对象(比如实时求平均值)
(2)可以验证class的约束条件,以及函数前提和时候状态
(3)可以在多线程中执行同步控制(加锁)
(4)保留了日后变更实现的权利。
protected也并不具有封装性,一旦protected成员变量改变,所有子类代码都需要改变。
条款23 宁以non-member、non-friend替换member函数
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
}
class WebBrower {
public:
// member
void clearEverything(); // 调用三个clear成员函数
}
// non-member、non-friend
void clearEverything(WebBrowser& wb) {
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
使用下面那种non-member、non-friend函数(便利函数)比直接使用成员函数更具有封装性,他提供了更大的包裹弹性(packaging flexibility),较低的编译依赖度,增加了WebBrower的可延伸性。
原因:
(1)愈多东西被封装,我们改变那些东西的能力也越大。反之,愈多函数可以访问它,数据的封装性越低。non-member、non-friend函数并不增加能够访问class内之private成分 的函数数量。
(2)成为class的non-member,也可以成为其他class的member,比如clearEverything可以是另外的工具类的成员函数。
(3)non-member可以位于当前class内,也可以位于class外,同一namespace内。
namespace WebBrowerStuff {
class WebBrower {...};
void clearBrower(WebBrower& wb);
}
(4)namespace可以跨越多个源码文件,class不能,所以可以分离便利函数。让与某一个功能相关的便利函数定义在一个头文件里,另一个功能相关的便利函数在另一个头文件,因此它具有更低的编译依赖度(允许客户只对他们所依赖的一小部分系统形成编译相依),而class必须整体定义,都需要编译。
// webbrower.h class自身
namespace WebBrowerStuff {
class WebBrower{};
... //non member
// 核心机能,所有客户都需要的non member。
}
// webbrowerbookmarks.h
namespace WebBrowerStuff {
...
// 与书签相关的便利函数
}
// webbrowercookies.h
namespace WebBrowerStuff {
...
// 与cookie相关的便利函数
}
所以便利函数定义在namespace中,class外,使得他们分离,编译依赖度低,且支持客户扩展(虽然是在次级机能上扩展)。此为C++标准库的组织方式。
条款24 若所有参数皆需类型转换,请为此采用non-member函数
对于一个有理数的类,如何实现它的乘法。
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const; // 分子
int denominator() const; // 分母
private:
}
如果使用member函数实现,如下:
class Rational {
public:
const Rational operator* (const Rational& rhs) cosnt;
}
除了需要支持Rational相乘外,还需支持Rational和int的相乘:
Ratioanl oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // 正确
result = result * oneEighth; // 正确
result = oneHalf * 2; // 正确,这里的2发生了隐式转换为Rational
result = 2 * oneHalf; // 错误!
注意result = 2 * oneHalf
可以转换成两种形式
result = 2.operator*(oneHalf); // 调用成员函数,但2没有相应的class
result = operator*(2, oneHalf); // 调用非成员函数,但却未定义
所以,当参数均位于参数列内,而非(像a*b
转为a.*b
),才能正确的隐式转换。
所以使用non member函数
class Rational {};
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
这里不把operator*放入friend中的原因是,friend会破坏封装,因为它可以访问private,而non-member只用访问public。
能不用friend的地方就不用friend。
条款25 考虑写出一个不抛异常的swap函数
结合条例29,swap可用在异常安全性编程。
假如有一个类:
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) {
...
*pImpl = *(rhs.pImpl);
...
}
private:
WidgetImpl* pImpl;
}
一旦交换两个Widget对象值,我们希望置换的是其pImpl指针,但swap不知道这一点。
swap的几种实现
1.std的实现
namespace std {
template<typename T>
void swap(T& a, T& b) {
T temp(a);
a = b;
b = temp;
}
}
2.成员函数实现(只想交换pImpl指针)
3.自己实现std::swap的特化版本
这两种实现与STL容器有一致性,因为STL容器也都提供public swap成员函数和std::swap特化版本(用来调用前者)
class Widget {
public:
...
void swap(Widget& other) { // 成员函数的实现
using std::swap; // 暴露std的实现,下面才能调用。
swap(pImpl, other.pImpl);
}
}
namespace std { // 自己实现std::swap的特化版本,调用成员函数
template<>
void swap<Widget>(Widget& a,
Widget& b) {
a.swap(b);
}
}
这种做法和stl容器有一致性,因为所有的stl容器也都提供public swap成员函数和std::swap特化版本(用以调用前者)
4.声明一个non-member swap函数
(模板的特化,偏特化)
如果Widget也是一个模板怎么办?
不可以用像上面一样直接使用template<typename T> void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
,因为C++只允许对class templates偏特化,不允许在function templates上偏特化???
所以需要自己定义一个swap,但不能在std中重载,可以在Widget的namespace中定义。
下面模板类型的non-member swap。
namespace WidgetStuff { // 不可以在std中重载,因为标准委员会禁止我们添加新东西到std。(只允许特化)
...
template<typename T>
class Widget {...};
...
template<typename T>
void swap(Widget<T>& a,
Widget<T>& b) {
a.swap(b);
}
}
调用方式
template<typename T>
void doSomething(T& obj1, T& obj2) {
using std::swap; // 需要暴露std实现,以做兜底!!
// 因此swap的调用优先级是
//(4)一个可能存在的T专属版本而且可能栖身于某个命名空间(当前命名空间)(但不能是std内)(属于重载)
//(3)某个可能存在的特化版本(属于(1)的特化)
//(1)std中的实现版本
swap(obj1, obj2);
}
注意
形式(2)成员版本的swap绝不可抛出异常???,因为swap的一个最好的应用是帮助classes提供强烈的异常安全性(exception-safety)保障。条例29对此主题提供了所有细节。
5 实现
条款26 尽可能延后变量定义式的出现时间
1.尽可能的延后变量定义式的出现时间。
string encrypted;
if () {
throw //这里可能抛出异常后,encrypted被构造又被异构了,但没有使用。
}
// use it.
if () {
throw
}
string encrypted;
//use it.
2.尽量在构造的时候给初值。如果构造后,再赋值,会调动没有意义的默认构造函数。
条款27 尽量少做转型动作
const_cast
向上转型等同于static_cast,向下转型比static_cast多了安全检查,转型错误返回NULL。
reinterpret_cast
一般不使用,低级转型,可以将pointer转成int(或反过来),转换时,执行的是逐个比特复制的操作。reinterpret中文意为“重新解释; 重新诠释;”。
D* pd1 = reinterpret_cast<D*> (pb);
D* pd2 = static_cast<D*>(pb);
在典型情况下,这里pd1和pd2将得到不同的值。pd2将指向传递来的D对象的开始位置,而pd1将指向该p对象的B子对象的开始。
reinterpretcast绝不在类层次中穿行,不会强制去掉const。
effective C++ 只用它来将原始内存写出一个调试用的分配器。
static_cast
注意事项:
1.一般使用新式转型,只在一种情况,委托构造使用旧式转型
2.转型会有一些处理代码,代表的是以base类指针指向derived类。
Derived d;
Base* pb = &d;// 左右两边的指针可能并不相同。
这种情况下会有一个偏移量offset在运行期被施行于Derived*指针上,用以取得正确的Base*值。
所以单一一个Derived对象可能会有两个地址(以Base*指向它时的地址,和以Derived*指向它时的地址)
就算你知道对象在内存中如何布局,但是不同平台有可能也不同。
3.不能用子类的虚函数去通过static_cast转型为父类去调用父类的虚函数。
class Window {
public:
virtual void onResize()
};
class SpecialWindow: public Window {
public:
virtual void onResize() {
static_cast<Window>(*this).onResize(); // 错误,调用的还是子类的虚函数
Window::onResize(); // 正确调用
}
};
4.dynamic_cast的注意事项
Window* ps;
SpecialWindow *psw = dynamic_cast<SpecialWindow*>(ps);
psw->SpecialFunction();
dynamic_cast使用场景:想在一个认定为子类的对象身上执行子类的普通函数,但只有一个基类的指针或引用。
但还是尽量少用dynamic_cast,因为执行速度很慢,可能会用多达四次的strcmp调用,用以比较class名称。
代替方式:
1.使用不同类型的指针指向运行期的不同类型。
2.使用虚函数,对基类定义一个定义是空的虚函数(非纯虚函数)
条款28 避免返回handles指向对象内部成分
Stuct RectData {
Point ulhc;
Point lrhc;
};
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
private:
std::shared_ptr<RectData> pData;
};
upperLeft和lowerRight反回了reference,会造成两个问题:
1.破坏了封装性,成员变量的封装性最多只等于返回其reference的函数的访问级别,所以这里pData是public的封装性。
2.const函数但可变了,虽然upperLeft和lowerRight是const成员函数,但调用者可以使用传出的reference值去改变对象数据。
修改:可以使用const Point&的返回值。
但还有个问题:
class GUIObject {...};
const Rectangle boundingBox(const GUIObject& obj);
// 值传递
// 如果客户这样使用这个函数:
GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
boundingBox的返回对象是临时的Rectangle,在这个语句结束后就被析构了,使得对象的handle(最后返回的pUpperLeft)比对象可能还长久。
条款29 为“异常安全”而努力是值得的
可分为三种保证:
基本承诺:异常安全函数即使发生异常也不会泄露资源或允许数据结构破坏。
强烈保证:如果函数成功,就是完全成功,失败则恢复到调用函数之前的状态。
nothrow保证:内置类型所有操作都提供nothrow保证,但任何动态内存分配(如stl容器)如果没有足够空间,均可以抛出bad_alloc异常。
class PrettyMenu {
public:
void changeBackground(std::istream& imgSrc);
private:
Mutex mutex;
Image* bgImage;
int imageChanges;
};
// changeBackground没有保证异常安全的实现
void PrettyMenu::changeBackground(std::istream& imgSrc) {
lock(&mutex);
delete bjImage; // 直接删除,如果后续异常则有问题。
++imageChanges; // 应该在新对象构建好后再执行这个操作,防止后续出现异常。
bgImage = new Image(imgSrc); // 如果这里异常,mutex不被释放,且原数据被删除掉了。
unlock(&mutex);
}
class PrettyMenu {
...
std::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex); // RAII思想
bgImage.reset(new Image(imgSrc)); // delete一定在new之后
++imageChanges; // 最后在修改这个变量。
}
以上写法几乎已经让changeBackground函数提供了强烈的异常安全保证,但Image构造函数异常,有可能导致输入流的读取记号被移走。
另外一种经典的方式也可有强烈的异常安全保证,即copy and swap技术(拷贝赋值函数常用)。
(1)用一个副本存原始数据(2)在副本上构建新数据(3)副本和原始数据对象做交换。
一般情况下可以去掉(1),(1)耗时。
struct PMImpl {
std::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu {
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
using std::swap;
Lock ml(&mutex);
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImpl, pNew);
}
条款30 透彻了解inlining的里里外外
inline的好处:
(1)免除函数调用成本
(2)方便编译器的最优化机制去浓缩这些不含函数调用的代码。
inline的坏处:
(1)inline会将每一个函数调用都用函数本体替代之,会增加目标码的大小。
导致代码膨胀–>内存增加–>额外的换页行为–>缓存命中率降低–>效率损失
(2)inline无法随着程序库升级而升级,无法动态链接,因为inline大多在编译期,需要重新编译。
inline的时期:
inline通常在编译过程,某些构建环境可能在链接期,甚至.NET CLI在运行期inline。他和templates一样通常定义在头文件,为了让编译器将它具象化(虽然某些时候是链接期)
(inline放在头文件的原因:inline是加在实现上,就算加在声明上,编译器也会忽略掉。内联展开是在编译时进行的,只有链接的时候源文件之间才有关系。所以内联要想跨源文件必须把实现写在头文件里。如果一个inline函数会在多个源文件中被用到,那么必须把它定义在头文件中)。
inline是个申请
class定义式中的函数(通常会定义成员函数),和inline标识的函数,会申请inline。
inline是个申请,编译器可加以忽略。如果一个inline申请无法最终被inline,编译器会给出警告信息。
inline申请被拒绝的情况:(取决于构建环境,主要是编译器)
1.编译器拒绝将太过复杂(例如含有循环或递归)的函数inlining。
2.虚函数也会拒绝inline(因为virtual意味着等待,在运行期才确定调用哪个函数)
3.如果程序要取某个inline函数的地址,编译器通常会为函数生成一个outlined函数本体。
inline void f() {...}
void (*pf) () = f; // pf指向f
f(); // 这个调用将被inlined,因为它是个正常调用
pf(); // 这个调用或许不被inlined,因为它通过函数指针达成。
4.有时候编译器还会为构造函数或析构函数生成outline副本。且本来构造函数或析构函数就不是好的inline对象。
class Base {
public:
...
private:
std::string bm1, bm2;
};
class Derived: public Base {
public:
Derived() {} // 这里其实不是空,可能不会被inline,具体调用见下方。
private:
std::string dm1, dm2, dm2;
};
// 编译期为Derived构造函数的具体代码大概是这样。
Derived::Derived() {
Base::Base();
try { dm1.std::string::string(); }
catch (...) {
Base::~Base(); // 异常,销毁构造好的基类部分。
throw; // 传播该异常。
}
try { dm2.std::string::string(); }
catch (...) {
dm1.std::string::~string(); // 销毁构造的dm1;
Base::~Base();
throw;
}
try { dm3.std::string::string(); }
catch (...) {
dm2.std::string::~string(); // 构造析构顺序相反。
dm1.std::string::~string();
Base::~Base();
throw;
}
}
如果string的构造函数恰巧也被inline,则Derived构造函数将会获得五份string构造函数的代码副本。
inline的选择
二八定律,一个程序80%的执行时间在20%的代码上,选对目标,着重优化20%的代码,将其inline。
条款31 将文件间的编译依存关系降至最低
接口和实现分离,实现修改,客户不需要重新编译。
1.使用object references或者object pointers代替objects。
直接定义object,Person必须知道它的定义才行,且Person需要知道各个对象的大小,因此需要include它的定义式头文件,这种方式不太好:
#include <string>
#include "date.h"
#include "address.h"
class Person {
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName; // 实现细目
Date theBirthDate; // 实现细目
Address theAddress; // 实现细目
};
所以使用引用或指针的形式,则Person类不需要再获取各个对象的定义了,结合前置声明后,Person头文件也不需要include它的头文件了。如果修改了子对象定义,Person不再需要重新编译。(前置申明的优缺点)
甚至为了分离,我们还可以把Person分为两个类,一个负责提供接口Person,一个负责实现该接口PersonImpl。
// Person.h
#include <string>
#include <memory>
class PersonImpl; // 前置申明
class Date;
class Adress;
class Person {
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::shared_ptr<PersonImpl> pImpl; // 智能指针。pointer to implementation。
};
// 在Person.cpp中再包含PersonImpl.h,并实现Person构造函数和PersonImpl构造函数。
2.尽量以声明式替代定义式。
3.为声明式和定义式提供不同的头文件,可以把写在单独一个头文件中。
(iostream、sstream、fstream、streambuf的前置声明头文件是iosfwd,所以不实现的情况下只需要#include <iosfwd>
)(前置申明的优缺点)
#include "datefwd.h" // 前置声明头文件。
Date today()
4.另外一种接口实现分离的方式是使用抽象基类,也即interface class。
抽象基类相当于没有实现,只有声明,所以抽象基类放入头文件中。在使用抽象基类时,只使用它的指针引用,不使用实体。
一旦实现被改变,接口不需要修改,客户不需要重新编译。
且抽象基类可以使用factory(工厂)函数创建不同子类的对象。(就好像实现了虚构造函数的功能,实际构造函数不能virtual),他们返回指针,指向动态分配的对象。
class Person {
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
static std::shared_ptr<Person>
create(const std::string& name,
const Date& birthday,
const Address& addr);
...
};
// 假设Person有个具象类(concrete class)RealPerson类,则create函数实现可如下:
std::shared_ptr<Person> Person::create(const std::string& name,
const Date& birthday,
const Address& addr) {
// 如果还有其他具象类可以有不同的判断和构造。
return std::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
抽象基类缺点:空间和时间开销,函数跳跃、虚函数指针、对象指针等等开销,但不是关键。
6 继承与面向对象设计
1.接口继承与实现继承
纯虚函数–接口继承
普通虚函数–接口和实现都继承
普通函数–继承基类的接口和一份强制性实现。
private和复合,has-a的关系,实现继承
2.不能被继承的类:
(1)使用final(c++11)
(2)构造函数通过private修饰,但是这个时候,是无法在栈上创建对象的,只能是借助于静态成员函数在堆上创建对象。
条款32 确定你的public继承塑模出is-a关系
“public”继承以为这is-a,适用于base class身上的每一件事情也适用于derived class上,因为每一个derived class对象都是一个base对象。
但注意,用企鹅继承鸟、用正方形继承矩形,可能不如我们所愿,企鹅不会飞,正方形也不能只改变某一条边的长度,所以这些属性不能继承。
条款33 避免遮掩继承而来的名称
1.作用域查找路径:(只查找名字,不查找重载的类型,当前作用域找到就返回)
当前作用域–>继承类作用域–>基类作用域–>基类的namespace–>global作用域
2.作用域遮掩
首先是简单情况:
int x;
void someFunc() {
double x;
std::cin >> x; // 这里的x是double的,遮掩了int的全局变量。
}
继承的情况:(继承也是作用域的形式,子类之于基类,相当于局部之于全局)
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
};
Derived d;
int x;
d.mf1(); // 没问题,调用Derived::mf1
d.mf1(x); // 错误!因为Derived::mf1遮掩了Base::mf1 !!
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); // 没问题,调用Derived::mf3
d.mf3(x); // 错误,因为Derived::mf3遮掩了Base::mf3!!
可见无论函数是不是虚函数/纯虚函数/非虚函数,无论函数的参数怎样,只要名字相同,父类的函数都会被子类遮掩。
3.解决方式:
(1) 使用using。(让所有基类同名函数在子类可见并且是public的)
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived: public Base {
public:
using Base::mf1; // 让所有基类名为mf1的函数在子类可见并且是public的。
using Base::mf3; // 让所有基类名为mf3的函数在子类可见并且是public的。
virtual void mf1();
void mf3();
void mf4();
};
Derived d;
int x;
d.mf1(); // 没问题,调用Derived::mf1
d.mf1(x); // 没问题,调用Base::mf1(int)
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); // 没问题,调用Derived::mf3
d.mf3(x); // 没问题,调用Base::mf3(double)
(2) 使用inline转交函数(forwarding function)
using会将所有同名函数暴露,但是inline转交函数只会暴露对应参数的函数。
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
};
class Derived: private Base {
public:
virtual void mf1() {
Base::mf1() // 此为转交函数,且是inline的
}
};
Derived d;
int x;
d.mf1(); // 没问题,调用Derived::mf1
d.mf1(x); // 错误!Base::mf1()被遮掩了,相当于只暴露了mf1(),而没有暴露mf1(int)
条款34 区分接口继承和实现继承
1.成员函数的接口总是会被继承。
2.声明一个纯虚函数的目的是为了让子类只继承函数接口,子类必须提供实现,一般抽象基类不需要提供实现。
3.纯虚函数若提供实现,调用它的唯一方式是指出class名称,如基类:函数()
。
主要作用在于:即给了子类一个缺省实现,又告诉子类必须要自己定义实现。
4.声明普通虚函数的目的是为了让子类继承接口和缺省实现
一般来说子类需要提供自己的实现,如果不提供,则继承父类的缺省实现。(子类可不提供接口和实现,直接继承父类的缺省实现,但不建议这样做,可用虚函数要求子类必须提供自己的实现)
5.第4点中,如果子类忘了提供实现,直接继承缺省实现有问题,但又想提供一个缺省实现给子类,让子类在明白要求的情况下去调用。则有以下两种方式解决:
第一种方式是,纯虚函数+protected非虚函数。
class Airplane {
public:
// 子类必须给实现(虽然可以选择调用缺省实现)
virtual void fly(const Airport& destination) = 0;
protected:
// 用户不给直接调用
void defaultFly(const Airport& destination);
};
void Airplaine::defaultFly(const Airport& destination) {
// 给子类的缺省实现。
}
//子类A,使用缺省实现
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
};
//子类B,自定义实现
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination) {
//自定义实现
}
};
第二种方式是,纯虚函数+抽象基类中定义实现(即第3点)
class Airplane {
public:
// 子类必须给实现(虽然可以选择调用缺省实现)
virtual void fly(const Airport& destination) = 0;
};
void Airplaine::fly(const Airport& destination) {
// 给子类可以手动调用的缺省实现
}
//子类A,使用缺省实现
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination) {
Airplane::fly(destination);
}
};
//子类B,自定义实现
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination) {
//自定义实现
}
};
6.声明非虚函数的目的是为了让子类继承基类的接口和一份强制性实现。
即意味着子类不能有和基类不同的行为。非虚函数表现出来的不变性>>特异性。
条款35 考虑virtual函数以外的其他选择
1.Non-Virtual Interface(NVI)实现Template Method模式
之前模式的另一种实现。
class GameCharacter {
public:
// 这个non-virtual函数又称为virtual函数的外覆器(wrapper)
int healthValue() const {
int retVal = doHealthValue();
return retVal;
}
// private或者protected,protected可提供缺省实现
private:
virtual int doHealthValue() const {
...
}
}
注意:继承类可以重新定义虚函数,即使他们不能调用它!!
优点:
NVI方法可以做一些事前工作(如互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件)和事后工作(互斥器解除锁定、验证函数的事后条件、再次验证class约束条件)、derived class可以决定“如何是想功能”,base class决定“函数何时被调用”)
缺点:
(1)private需要每一个derived class自定义功能(可以改为protected)
(2)还是借助了虚函数。
扩展:虚函数和private
virtual 机制是 dynamic 的,它发生在运行时,而 access control(public,private) 是 static 的,它发生在编译时。virtual 和 access control 是两个没有多大关系的概念,你可以在基类声明一个 public virtual,然后在派生类把他搞成 private 或 protected 的,可以通过基类指针调用这个子类的虚函数。
access control 仅仅检查一个成员是否属于当前对象的静态类型(与它实际的运行时类型无关),只要通过了这个检查,整个程序的行为例如虚函数等,就完全不受 access control 的影响了。
2.使用函数指针实现Strategy模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
// 函数指针
typedef int (*HealthCalcFunc) (const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf) {}
int healthValue() const {
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
优点:
(1)同一类型的不同实体可以有不同的“健康计算函数”。
(2)某实体的健康计算函数可在运行期变更。
缺点:
(1)函数指针形式不支持 参数/返回值 隐式转换的函数!!!
(2)此健康计算函数不是成员函数,无法访问类中non-public的部分。
缺点的解决方式:
弱化class的封装,将健康计算函数生命为friends友元函数。
3.使用function仿函数实现Strategy模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
// HealthCalcFunc可以是任何可调用物。
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf) {}
int healthValue() const {
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
优点:
function可以持有任何的可调用物(callable entity),包括函数指针、函数对象、成员函数指针、并可兼容参数/返回值会隐式转换的函数
// 返回值为short不是int,隐式转换
short calcHealth(const GameCharacter& );
// 函数对象
struct HealthCalculator {
int operator() (const GameCharacter&) const {
...
}
};
// 成员函数
class GameLevel {
public:
float health(const GameCharacter&) const;
};
class EvilBadGuy: public GameCharacter {
...
};
class EyeCandyCharacter: public GameCharacter {
...
};
// 兼容隐式转换函数
EvilBadGuy ebg1(calcHealth);
// 兼容函数对象
EyeCandyCharacter ecc1(HealthCalculator());
// 兼容成员函数,使用bind可以转换参数
// bind使得接收两个参数的函数(一个GameLevel、一个GameCharacter)转化为了接收一个参数的函数(一个GameLevel)
GameLevel currentLevel;
EvilBadGuy ebg2(
// 成员函数必须用&
std::bind(&GameLevel::health,
// 类对象可以加&也可以不加
currentLevel,
_1)
);
(其中bind的用法见:https://www.sidney.wiki/cpp/725)
4.传统的Strategy模式实现
以设计模式中的strategy模式的思路去分离继承体系,HealthCalcFunc是另一个继承体系的根类。
class GameCharacter;
class HealthCalcFunc {
public:
...
virtual int calc(const GameCharacter& gc) const {
...
}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthCalc(phcf) {}
int healthValue() const {
return pHealthCalc->calc(*this);
}
private:
// 另一继承集体的类
HealthCalcFunc* pHealthCalc;
};
条款36 绝不重新定义继承而来的non-virtual函数
D x;
B* pB = &x
pB->mf(); // 调用了B的mf
D* pD = &x
pD->mf(); // 调用了D的mf。
// 若重新定义,则同一个x类型调用的mf函数不一致。
// 不要这样去使用静态绑定,必要时可以用virtual动态绑定。
// non-virtual的不变性>>特异性
条款37 绝不重新定义继承而来的缺省参数值
绝不重新定义继承而来的virtual函数的默认参数值。
virutal函数是动态绑定的,但缺省参数是静态绑定的!!
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virturl void draw(ShapeColor color = Red) const = 0;
};
class Rectangle: public Shape {
public:
virturl void draw(ShapeColor color = Green) const
}
// pr的静态类型是Shape,使用Shape的缺省参数Red;
// pr的动态类型是Rectangle,调用Rectangle的虚函数。
Shape* pr = new Rectangle;
pr->draw(); // 调用Rectangle::draw(Shape::Red);
不要在子类重新定义缺省参数了,即使是和基类的缺省参数定义保持一致,都会因为相依性导致后续修改容易出错。
除了只在子类定义缺省参数外,还有一种解决方式是使用NVI方法。
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const {
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle: public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
};
条款38 通过复合塑模出has-a或“根据实物实现出”
区分has-a和is-a。
is-a是一种继承关系,has-a是类中的一个类的对象。
Set<T>
是否应该继承List<T>
?
Set<T>
元素不重复,List<T>
元素可重复,所以Set<T>
不是一种List<T>
,应该是Set<T>
has-a List<T>
(is-implemented-in-terms-of)。
Set<T>
的定义和实现可如下:
// Set定义
template<class T>
class Set {
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep;
};
// Set实现
template<typename T>
bool Set<T>::member(const T& item) const {
return std::find(rep.begin(), rep.end(), item) != rep.end();
}
template<typename T>
void Set<T>::insert(const T& item) {
if (!member(item)) rep.push_back(item);
}
template<typename T>
void Set<T>::remove(const T& item) {
typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item);
if (it != rep.end()) rep.erase(it);
}
template<typename T>
std::size_t Set<T>::size() const {
return rep.size();
}
条款39 明智而审慎地使用private继承
private继承的子类不能转化为父类
private继承意味着只有实现部分private部分被继承,接口部分被略去。
解释是:private继承后,用户不能访问父类的public接口!但是继承类可以访问public接口!
private是一种实现技术,继承类也可以重新实现基类的virtual函数。
private不能阻止derived class重新定义虚函数!
private的使用场景:(is-implemented-in-terms-of情况下)
1.使用private将父类的protected、public成员变成自己的private成员。(和条款18的has-a类似)
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const; // 定时器每滴答一次,改函数就被调用一次。
};
class Widget: private Timer {
private:
virtual void onTick() const;
};
private继承代表这里的Widget不是一种(not is-a)Timer。
有一种方式可以代替private。(使用public继承+复合)
class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick() const;
};
WidgetTimer timer;
};
public继承+复合 此种实现方式可以阻止继承类再重新定义virtual函数。
(这种复合方式如果使用指针,还可以减少编译相依关系,不用include timer.h)
2.private继承还可以用来继承一个空类。
没有non-static成员、没有虚函数的空类,定义后的大小是1字节。
但如果继承它,则基于EBO(empty base optimization:空白基类最优化),基类将不占用空间。若使用复合,则会占用空间。
这种空类不是真正的空类,一般含有typedef、enums、static成员变量、non-virtual函数等,stl里unary_function、binary_function就是这种类型。
条款40 明智而审慎地使用多重继承
如果从一个以上的base继承了相同名字的函数(不需要限制参数,不管是否是private还是public,只要名字相同就有歧义),则会导致歧义。
如果必须要调用,必须要指明是哪一个函数。
class BorrowableItem {
public:
void checkOut();
};
class ElectronicGadget {
private:
bool checkout() const;
};
class MP3Player:
public BorrowableItem,
public ElectronicGadget
{ ... };
MP3Player mp;
mp.checkOut(); // 歧义,调用的函数不明。
mp.BorrowableItem::checkOut();
另一种做法是使用虚继承,则只会有一个文件名称。
C++对于两种方案都支持(缺省方法是执行复值),但正确做法是使用virtual base class;但需要注意虚继承的virtual base的初始化任务由最后的继承类负责。
但需注意:
1.多重继承比单一继承更复杂,他可能导致新的歧义性。
2.多重继承的确有一点重要用途。其中一个情节就是涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相结合。
3.虚继承会增加大小、速度、初始化(及赋值)复杂度等成本。如果virtual base classes不带任何数据,将是最具使用价值的情况。
7 模板与泛型编程
C++ template机制自身是一部完整的图灵机,他可以用来计算任何可计算的值,于是导出了模板元编程(template metaprogramming)
条款41 了解隐式接口和编译器多态
template<typename T>
void doProcessint(T& w) {
if (w.size() > 10 && w != someNastyWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
普通class是显式接口,T是确定的如Widget。
而模板形式T必须支持一组隐式接口,包括size、normalize、swap成员函数、copy构造函数、比较计算。
模板具现化发生在编译期。以不同的template参数具现化function templates,叫做编译期多态。
仔细分析w.size() > 10 && w != someNastyWidget
发现,并不一定要求T支持operator>,只需要w.size的返回值和int(10的类型)可以调用一个已经定义的operator>的函数便可(w.size的返回值不一定是T类型)。同理operator!=符号也是。T并不需要支持自身的不等比较,只需要支持和someNastyWidget的不等比较即可。
条款42 了解typename的双重意义
1.当我们声明类型参数时,class和typename的意义完全相同。
// 以下两者等价。
template<class T> class Widget;
template<typename T> class Widget;
2.当我们想要在template中指涉一个嵌套从属类型名称时,就必须在紧邻它的前一个位置放上关键字typename。
从属是指依赖模板一个参数,嵌套是指“::”。
原因如下:
template<typename C>
void print2nd(const C& container) {
C::const_iterator* x;
}
这里的const_iterator有可能是一个参数,也有可能是一个类型,有歧义。一般来说编译器遇到嵌套从属名称时,会默认它不是一个类型!!所以我们需要加上typename告诉编译器这是一个类型。
template<typename C>
void print2nd(const C& container) {
typename C::const_iterator* x;
}
同理!!
std::vector<int>::const_iterator p; // 不需要加typename,编译器可以确定具体指的什么。
typename std::vector<T>::const_iterator p; // 需要加typename告诉编译器是一个类型。
3.但有两种特殊情况不用typename,一种是在base classes list内的嵌套从属名称之前,一种是在member initialization list(成员初值列)中作为base class修饰符。
template<typename T>
class Derived: public Base<T>::Nested { // base class list中不允许出现typename
public:
explicit Derived(int x)
: Base<T>::Nested(x) { // 此处调用基类构造函数,在成员初始列中不允许出现typename
typename Base<T>::Nested temp; // 但这里需要加上typename
}
};
4.typename 修饰的类型可以加typedef重命名。
template<typename IterT>
void workWithIterator(IterT iter) {
typename std::iterator_traits<IterT>::value_type temp(*iter);
}
std::iterator_traits<IterT>
的value type是指类型为IterT的对象所指之物的类型,比如IterT为vector<int>::iterator
时,它的value_type就是int。
使用typedef的方式:
template<typename IterT>
void workWithIterator(IterT iter) {
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
}
5.typename有移植性问题,有点编译器typename表现不一致,有的不允许typename(主要是老版本)。
条款43 学习处理模板化基类内的名称
假设我们需要写一个程序,它能够传送信息到若干不同公司去。信息要不以密码方式,要不以原文方式。如果编译期间我们有足够信息来决定信息传递给哪一个公司,则可以用模板的解法。
class CompanyA {
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
class CompanyB {
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
Class MsgInfo { ... } // 这个class用来保存信息
template<typename Company>
class MsgSender {
public:
void sendClear(const MsgInfo& info) {
std::string msg;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info) {
...
}
};
假设我们还有一个表示每次传递信息的LOG的类,可以使用derived class。
template<typename Company>
// 个人认为这里的public改成private更好,base是为了帮助derived实现。
class LoggingMsgSender: public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info) {
// 将“传送前”的信息写入log
sendClear(info); // 调用base class函数,但这段代码无法通过编译!!
// 将“传送后”的信息写入log
}
}
无法编译的原因是,其中的Company是个模板参数,不到后来(LoggingMsgSender被具现化时)无法确切知道它是什么,也就不知道它里面是否有一个sendClear函数。(有可能一个特化版本没有这个函数)
比如一个CompanyZ的定义里没有sendClear函数。
class CompanyZ {
public:
void sendEncrypted(const std::string& msg);
};
则MsgSender模板定义了一个全特化版本。
(注:全特化是指一旦CompanyZ参数被定义后,再也没有其他模板化参数会变化,偏特化可能还有其他模板化参数会变化)
template<>
class MsgSender<CompanyZ> {
public:
void sendSecret(const MsgInfo& info) {
...
}
};
如此而来,编译器知道MsgSender可能被特化,而那个特化版本的接口只有一个sendSecret,所以编译器拒绝了在模板化基类MsgSender<Company>
中寻找继承而来的名称SendClear。
所以要继承模板化基类,并调用模板化基类的函数,有以下三种方法:
(1)使用this指针
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info) {
// 将“传送前”的信息写入log
this->sendClear(info); // 成立,假设sendClear将被继承。
// 将“传送后”的信息写入log
}
}
(2)使用using声明式
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear;
void sendClearMsg(const MsgInfo& info) {
// 将“传送前”的信息写入log
sendClear(info); // 告诉编译器,假设sendClear位于MsgSender<Company>::sendClear内
// 将“传送后”的信息写入log
}
}
(3)直接指明调用的函数
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info) {
// 将“传送前”的信息写入log
MsgSender<Company>::sendClear(info); // 成立,假设sendClear将被继承。
// 将“传送后”的信息写入log
}
}
第三种方法有不足,如果被调用的是虚函数,上述明确资格修饰(explicit qualification)会关闭virtual绑定行为。
如果采用了上述三种方式,下列关于CompanyZ的代码将编译失败,即编译器还是会检查,只不过是在后面调用时检查。
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
zMsgSender.sendClearMsg(msgData); // 错误,无法通过编译。
条款44 将与参数无关的代码抽离templates
1.模板函数只在被使用时才被具现化。
2.但如果模板函数被具现化多次,可能会产生多个目标码,而造成代码重复,内存上升,换页,缓存命中率下降。特别是模板有非类型参数(non-type parameter)时
template<typename T, std::size_t n>
class SquareMatrix {
public:
void invert(); // 求逆矩阵
};
// 这里生成的目标码不同,会造成代码膨胀。
SquareMatrix<double, 5> sm1;
sm1.invert();
SquareMatrix<double, 10> sm2;
sm2.invert();
解决方式:(使用条款43的方式)
template<typename T>
class SquareMatrixBase {
protected:
void invert(std::size_t matrixSize); // 以给定尺寸求逆矩阵
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert; // 避免遮掩base版的invert,条款33
public:
void invert() {
this->invert(n);
// 制造一个inline调用,这里用this的原因见条款43
}
}
这里的base class知识为了帮助derived classes实现,不是is-a的关系。
但是这个版本的缺点是没有办法存储矩阵的数据。
所以新优化一个版本,继承类存储数据,基类使用指针访问数据。
template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(std::size_t n, T* pMem)
: size(n), pData(pMem) { } // 存储矩阵的大小和指向矩阵数据的指针
void setDataPtr(T* ptr) { pData = ptr; } // 重新赋值给pData
private:
std::size_t size;
T* pData;
};
这允许derived classes决定内存分配方式。
derived的固定内存分配版本
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix()
: SquareMatrixBase<T>(n, data) { } // 送出矩阵大小和数据指针给base class
private:
T data[n*n];
};
但这个版本的对象大小可能太大。
下面是动态内存分配的版本
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix()
: SquareMatrixBase<T>(n, 0),
pData(new T[n*n]) {
this->setDataPtr(pData.get());
// 将base的数据指针设为null,为矩阵内容分配内存,将指向该内存的指针存储起来,然后将它的一个副本交给base class
}
private:
boost::scoped_array<T> pData; // 关于boost::scoped_array见条款13,智能指针数组(定义了delete []__)
};
对比尺寸专属版和共享版本:
(1)尺寸是一个编译期常量,尺寸专属版可能可以因此可以由常量的广传达到最优化。
(2)共享版本理论上可以节省内存,但可能会增加空间复杂度,也会让事情变得更复杂和难以理解。
(3)1、2具体谁表现更好可能需要具体测试。
除了而非类型模板参数(non-type template parameters)、类型参数(type parameters)也会导致代码膨胀,虽然有些连接器(linkers)会合并完全相同的函数实现码。
对于指针类型,可以使用由无类型指针(untyped pointers,即void*)代替操作强行指针(strongly typed pointers,即T*)
条款45 运用成员函数模板接受所有兼容类型
1.初识成员函数模板–用于拷贝构造函数
提供模板类中T成员之间的隐式转换。
利用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数
比如自己定义一个智能指针,需要定义一个构造模板,实现不同底层类型间的转换。这个模板成为泛化拷贝构造函数。
template<typename T>
class SmartPtr {
public:
// U类型到T类型, 成员函数模板
template<typename U>
SmartPtr(const SmartPtr<U>& other);
};
2.成员函数模板的模板类型转换可以限制
我们并不希望,模板之间的原始指针可以任意转换,比如我们不希望SmartPrt<Base>
可以构造SmartPtr<Derived>
。
我们需要这个模板所创建的成员函数进行挑选筛除。这个模板只有当“存在某个隐式转换”可以将U*转换为T*指针才能通过编译。
比如下面通过原始指针的转换关系来限制。
template<typename T>
class SmartPtr {
public:
// U类型到T类型
template<typename U>
SmartPtr(const SmartPtr<U>& other)
: heldPtr(other.get()) {...}
T* get() const { return heldPtr; }
private:
T* heldPtr;
};
3.成员函数模板进阶使用–支持各种赋值操作
template<class T>
class shared_ptr {
public:
// 构造函数,兼容所有内置指针
template<class Y> explicit shared_ptr(Y* p);
// 拷贝构造函数
// 兼容shared_ptr
template<class Y> (shared_ptr<Y> const& r);
// 兼容weak_ptr
template<class Y> explicit (weak_ptr<Y> const& r);
// 兼容auto_ptr
template<class Y> explicit (auto_ptr<Y> const& r);
// 拷贝赋值函数
// 兼容shared_ptr
template<class Y> shared_ptr& operator=(shared_ptr<Y> const& r);
// 兼容auto_ptr
template<class Y> shared_ptr& operator=(auto_ptr<Y> const& r);
};
注意:
(1)从一个内置指针或者其他智能指针隐式转换到shared_ptr不被允许,从其他shared_ptr隐式转换到shared_ptr被允许。
(2)auto_ptr没有声明const,因为赋值会改变原值,其他都是const
(3)weak_ptr总结
4.如果你声明成员函数模板用于“泛化copy构造”或“泛化赋值操作”,你还是需要声明正常的拷贝构造和赋值构造函数,如果没有声明,编译器还是会默认创建一个自己的non-template的copy/赋值构造函数。
template<class T>
class shared_ptr {
public:
shared_ptr(shared_ptr const& r); // 普通copy构造函数
template<class Y> shared_ptr(shared_ptr<Y> const& r); // 泛化拷贝构造函数
shared_ptr& operator=(shared_ptr const& r); // 普通赋值构造函数
template<class Y> shared_ptr& operator=(shared_ptr<Y> const& r); // 泛化赋值构造函数
};
条款46 需要类型转换时请为模板定义非成员函数
条款24的模板式方法。
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0,
const T& denominator = 1);
const T numerator() const;
const T denominator() const;
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs) {
...
}
Rational<int> oneHalf(1, 2); // 这个例子来源于条款24,唯一不同的是现在是模板
Rational<int> result = oneHalf * 2; // 不能通过编译
条款24非模板形式中,可以将2隐式转换为Rational类型,所以没有问题。
但模板形式中,template在实参推导过程中不能将隐式类型转换函数纳入考虑。
详细原因:在调用一个函数前,必须知道那个函数存在,而为了知道它,必须先为相关的function template推导出参数类型,然后才可以将适当的函数具现化出来。然而,template在实参推导过程中不能将隐式类型转换函数纳入考虑。
解决方式:
使用friend声明式可以指涉某个特定函数的特点,让编译器在class Rational
template<typename T>
class Rational {
public:
...
friend
const Rational operator*(const Rational& lhs,
const Rational& rhs);
// 上面是简略表达方式,等同于下面这种形式。
// 声明时可省略<T>!!!
const Rational<T> operator*(const Rational<T>& lhs, // 注意这里没有template<typename T>!!
const Rational<T>& rhs); // 相当于在使用原始类型的T!!
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs)
注意class内friend声明时template<typename T>
不写,使用原始类的T,在原始类创建是推导T,避免了调用时类型推导。
如何继续为operator*
提供定义式:
(1)写在class内部,自动inline
template<typename T>
class Rational {
public:
...
friend
const Rational operator*(const Rational& lhs,
const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
};
(2)写在class外部(需要Rational前置声明)
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs,
const Rational<T>& rhs) {
return Rational<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
};
template<typename T>
class Rational {
public:
...
friend
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs) {
return doMultiply(lhs, rhs);
}
};
条款47 请使用traits classes表现类型信息
1.advance初识
以stl中advance这个template来介绍traits,advance用来将某个迭代器移动某个给定距离。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);
// 将迭代器向前移动d单位,d<0则向后移动
// 相当于iter+=d(但只有random access才支持)
方向:后------->>>>>前
2.stl中五种迭代器分类
(1)Input迭代器,只能向前移动,只能读取 一次。比如istream_iteratiors
(2)Output迭代器,只能向前移动,只能涂写 一次。比如ostream_iteratiors
(3)forward迭代器,只能向前移动,可以读或写 一次以上。比如slist
(4)Bidirectional迭代器,可以双向移动,可以读或写 一次以上。比如set,multiset,map,multimap,list
(5)random access迭代器,可以双向移动,可以读或写 一次以上,支持迭代器算数(iter+=d)。比如vector,deque,string
对于上述迭代器,标准库提供专属的卷标结构(tag struct)加以确认。
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
3.期望的advance实现形式
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (iter is a random access iterator) {
iter += d;
} else {
if (d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
}
如何在编译期获取类型信息呢,使用traits技术。
4.traits技术的实现。(以iterator_traits为例)
针对迭代器的iterator_traits实现:针对每一个类型IterT,在struct iterator traits<IterT>
内一定声明某个typedef名为iterator_category。使用typedef来确认迭代器的分类
比如deque和list。
template<...>
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
template<...>
class list {
public:
class iterator {
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};
iterator_traits真正的模板实现
// 类型IterT的iterator_category其实就是用来表现“IterT说它是自己是什么”
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
};
指针类型需要特化,指针的行径类似于ramdom access。
// 指针类型的迭代器需要单独实现,使用特化版本。
template<typename IterT>
struct iterator_traits<IterT*> {
typedef random_access_iterator_tag iterator_category;
};
5.advance函数的实现
(1)借助typeid
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag)) {
iter += d;
}
else {
if (d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
}
缺点:
1)if else,typeid,运行时判断,浪费时间,可执行文件膨胀。
2)实际上上面这种实现还可能造成编译期问题,假如我们使用list<int>::iterator
实例化上,它是一个bidirectional迭代器,并不支持+=,但编译器必须保证所有源码都生效,纵使是不会执行起来的代码,而当iter不是random access迭代器时,iter+=d无效。
(2)利用重载在编译期判断
编译器对重载的处理是,调用最匹配实参的f,否则调用第二匹配的,再否则调用第三匹配的(可实现编译器条件句)。
// 这份实现用于random access迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d,
std::random_access_iteratior_tag) {
iter += d;
}
// 这份实现用于bidirectional迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d,
std::bidirectional_iteratior_tag) {
if (d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
// 这份实现用于input迭代器
// 由于继承关系,input_iteratoir_tag版本也能够处理forward迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d,
std::input_iteratior_tag) {
if (d < 0) {
throw std::out_of_range("Negative distance");
}
while(d--) ++iter;
}
实现代码:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
doAdvance(iter, d,
typename std::iterator_traits<IterT>::iterator_category());
}
6.其他traits
(1)标准库的四种traits
iterator_category 迭代器类型
value_type 指针指向类型
char_traits 字符类型的相关信息
numeric_limits 保存数值类型的相关信息(numeric_limits<T>::max()
numeric_limits<float>::epsilon()
)
(2)TR1中的常用判断类型信息的新traits
is_fundamental\<T> 判断T是否为内置类型
is_array\<T> 判断T是否为数值类型
is_base_of<T1, T2> 基类判断(T1,T2相同,或者T1是T2的基类,返回true)
条款48 认识template元编程
Template metaprogramming(TMP,模板元编程),基于C++11的模板元编程介绍,续篇的链接在文章内容末尾。
优点:
(1)模板元编程是执行与C++编译器内的程序,将工作从运行期转移到编译期,使得原本通常在运行期才能发现的错误,在编译器就可以找出来。
(2)通常TMP的C++程序有更小的可执行文件、较短的运行期、较少的运行内存,但编译时间更长。
条款47中的traits实现就是用了模板元编程。
TMP是图灵完全的,循环效果由递归完成,是函数式语言。
递归模板具现化示例:
template<unsigned n>
struct Factorial {
// 不需要typename,因为value不是类型?
enum { value = n * Factorial<n-1>::value };
};
// 递归终止条件是一个特化
template<>
struct Factorial<0> {
enum { value = 1 };
};
TMP的用处
(1)确保度量单位准确(距离变量/时间变量=速度变量 的表示),在编译期确保度量单位的组合正确。
(2)优化矩阵运算,使用expression templates,消除临时对象并合并循环。
(3)用于设计模式/智能指针,用来生成”基于政策选择组合“(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。可作为殖生式编程(generative programming)的一个基础。
8 定制new和delete
条款49 了解new-handler的行为
1.初识new-handler
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够的内存。
new-handler默认是null(此时operator new分配不成功会抛异常),也可以是客户指定的错误函数的指针。
客户可以使用set_new_handler定义一个需要调用的new_handler函数指针,返回值也是个指针,指向set_new_handler被调用前正在执行的那个new-handler函数。
std中set_new_handler的声明如下:
namespace std {
typedef void (*new_handler) (); //函数指针
new_handler set_new_handler(new_handler p) throw();
}
2.一个良好的new_handler应该做的事
(1)让更多的内存可被使用。
比如,在程序一开始执行就分配一大块内存,而当new-handler第一次被调用,将它们释放给程序使用。
(2)安装另一个new-handler。
1)如果目前这个new-handler无法获得更多可用内存,或许它知道另外哪个new-handler有此能力,可以安装另外那个new-handler以替换自己。
2)每一次调用new-handler都有一些修改自己的行为,所以每一次都是不同的调用(或者说可能做不同的事情)。实现方式是让new-handler修改“会影响new-handler行为”的static、namespace、global数据。
(3)卸除new-handler,将null指针传给set_new_handler,使得operator new在内存分配不成功时抛异常。
(4)抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
(5)不返回,通常调用abort或exit
3.class专属的new-handler
类专属的new-handler需要自己实现。令class提供自己的set_new_handler和operator new即可。
需要是static的,因为这个对象还未分配。
class Widget {
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
static成员必须在class定义式之外被定义(除非它们是const而且是整数型)。
// 类外初始化static成员。
std::new_handler Widget::currentHandler = 0; //初始化为null
专属set_new_handler的实现
std::new_handler Widget::set_new_handler(std::new_handler p) throw() {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
专属的operator new需要做的事情:
(1)调用std里的set_new_handler,将Widget里的currentHandler安装为global的new-handler。
(2)调用global operator new,global operator new调用global new-handler,global new-handler是我们之前安装的那个handler。
(3)在global operator new分配内存不成功,会抛出bad_alloc异常,此时Widget类里的operator new必须恢复原本的global new-handler,然后再传播该异常。使用RAII的方式。
(4)Widget类的析构函数也需要恢复原本的global new-handler。
我们可以使用RAII,在new开始时赋新的new-handler,建立RAII保存原来的global new-handler,在new结束时析构RAII资源,恢复global new-handler。
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh)
: handler(nh) {}
~NewHandlerHolder() {
std::set_new_handler(handler);
}
private:
std::new_handler handler; // 记录下来
NewHandlerHolder(const NewHandlerHolder&); // 阻止copying
NewHanlderHolder& operator=(const NewHandlerHolder&);
};
专属类中的operator new的实现
void* Widget::operator new(std::size_t size) throw(std::bad_alloc) {
// 注意,currentHandler是之前调用Widget::set_new_handler时定义的
NewHandlerHolder
h(std::set_new_handler(currentHandler)); // 安装Widget的new-handler
return ::operator new(size); // 恢复global new-handler
}
客户使用方式
void outOfMem();
Widget::set_new_handler(outOfMem); //设置outOfMem为专属的new-handler
Widget* pw1 = new Widget;
Widget::set_new_handler(0); // 设置null为专属的new-handler
Widget* pw2 = new Widget;
4.建立“mixin”风格的模板基类。
建立“mixin”风格的模板基类可以方便复用,提供“设定calss专属的new-handler”的能力
且可以确保每个子类是实体互异的(static currentHandler不共用)
(PS:模板的不同具现化实体,static成员变量不共用!!)
template<typename T>
class NewHandlerSupport {
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc) {
NewHanlderHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
// 为每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHanlder = 0;
此模板继承运用了CRTP(curiously recurring template pattern),可以理解为Do it For Me。
5.少用nothrow new
nothrow 形式的new定义在头文件<new>中
class Widget { ... };
Widget* pw1 = new Widget;
if (pw1 == 0) ...
Widget* pw2 = new(std::nothrow) Widget;
if (pw2 == 0) ...
nothrow版本的operator new,虽然当前不会抛出异常,但Widget的构造函数还可能又new了一些内存,但没人强迫这些new操作是nothrow new,所以nothrow new依然可能抛出异常,所以没有使用它的必要。
条款50 了解new和delete的合理替换时机
定制new delete需要考虑的问题(齐位问题、可移植性、线程安全性、内存碎片等)
定制new delete的作用:
1.检测错误
static const int signature = 0XDEADBEEF;
typedef unsigned char Byte;
void* operator new(std::size_t size) throw(std::bad_alloc) {
using namespace std;
size_t realSize = size + 2 * sizeof(int);
void* pMem = malloc(realSize);
if (!pMem) throw bad_alloc();
// 将signature写入内存的最前段落和最后段落。
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)
+ realSize - sizeof(int))) = signature;
// 返回指针,指向位于第一个signature之后的位置。
return static_cast<Byte*>(pMem) + sizeof(int);
}
但上述实现有一些问题,比如并未反复调用new-handler函数。
没有考虑齐位问题。
因为返回的是malloc且偏移一个int大小的指针,没人能保证他的安全,比如我用它来new一个8字节的double,可能就会获得一个未有适当齐位的指针,导致程序崩溃或者执行速度变慢。
2.强化效能,增加分配和归还的速度,降低缺省内存管理器带来的空间额外开销。
定制大块内存、小块内存、混合型内存分配。
接纳各种分配形态,包括少量区块动态分配,大量短命对象持续分配和归还。
更好的内存碎片问题(fragmentation),降低内存开销。
但有时候会减低速度,比如Boost的Pool库考虑了线程安全,单线程就不太适用。
3.收集使用上的统计数据
记录分配区块的大小分布?寿命如何?倾向于FIFO或者LIFO的次序分配和归还?最大动态分配量(高水位)是多少?
4.弥补缺省分配器中的非最佳齐位。
5.为了将相关对象成簇集中。
降低内存页错误(page faults)的频率。创建另一个heap,将它们成簇集中在尽可能少的内存页(pages)上,如使用placement版本就有可能完成这样的行为。
6.为了获得非传统行为。
比如自定义operator delete,将归还内容覆盖为0,增加应用程序的数据安全性。
条款51 编写new和delete时需固守常规
operator new的注意事项
(1)operator new内含一个每次失败都要调用的new-handler循环,只有当new-handler函数的指针返回是null时,operator new才抛出异常。
(2)operator new需要处理0byte申请,C++规定,即使客户要求0bytes,operator new也得返回一个合法指针。
伪码如下:
void* operator new(std::size_t size) throw(std::bad_alloc) {
using namespace std;
if (size == 0) {
size = 1;
}
while (true) {
if (分配成功) {
return (一个指针,指向分配得来的内存);
}
// 分配失败:找出目前的new-handler函数!!见第3点。
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
// 解释了条款49为什么new-handler需要干这么多事情:(为了结束循环)
// 让更多内存被使用,安装另一个new-handler,卸除new-handler,直到new-handler为null,抛出bad_alloc异常
// 这里之所以将new-handler设置为null后马上又恢复原样,是为了获取原始的globalHandler并调用。
if (globalHandler) {
(*globalHandler)();
} else {
throw std::bad_alloc();
}
}
}
(3)为了获取new-handler函数指针,我们不得已把new-handler设为0(得到返回值),然后又立刻恢复原样。
(4)operator new 需要考虑继承问题,如果不考虑继承情况,则应该调用标准的::operator new(使用size)检查。
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived::public Base { //假设Derived未声明operator new
...
};
Derived*p = new Derived; // 这里调用的是Base::operator new
处理继承问题:
void* Base::operator new(std::size_t size) throw(std::bad_alloc) {
if (size != sizeof(Base)) {
// 由于sizeof(Base)不可能为0,所以这里也考虑了size为0的情况!!!
return ::operator new(size);
}
}
(5)基类的operator new[]是可以将内存分配给元素为子类的array使用,所以不能在Base::operator new[]内假设array的每个元素大小是sizeof(Base),这也意味着不能假设array的元素个数是 (bytes申请大小)/sizeof(Base),此外,传给operator new[]的size_t参数,有可能将被填补对象的内存数量更多。
operator delete的注意事项
1.C++保证”删除null指针永远安全“,所以需要考虑这种情况。
2.你的class专属operator new将大小有误的分配行为转交给::operator new执行时,你也必须将大小有误的删除行为转交给::operator delete执行。(所以需要传入大小size)
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void* rawMemory, std::size_t size) throw();
};
void Base::operator delete(void* rawMemory, std::size_t size) throw() {
if (rawMemory == 0) return; // 检查null指针!!
if (size != sizeof(Base)) {
::operator delete(rawMemory);
return ;
}
}
3.如果被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++ 传给operator delete的size_t数值可能不准确。见条款7,这是需要虚析构函数的理由之一。
条款52 写了placement new也要写placement delete
1.placement new 的含义
placement new是指operator new除了接受含有size_t那个参数之外还有其他参数。
人们常说的placement new是指“接受一个指针指向对对象该被构造之出”的那个版本(已被纳入标准库):void* operator new(std::size_t, void* pMemory) throw();
2.placement new可能造成的内存泄露
对于一个new表达式Widget* pw = new Widget;
,共有两个函数被调用,一个是用以分配内存的operator new,另一个是Widget的default构造函数;
假设第一个函数调用成功,第二个函数抛出异常。这样的话,步骤一的内存分配所得必须取消并恢复旧观,否则会造成内存泄露(memory link)。
一般来说,这件事由C++运行期系统来完成,它会调用operator new对应签名式(signature)的operator delete。
正常的new和delete签名式是这样的:
// new一个对象或者new数组都是这个形式
void* operator new(std::size_t) throw(std::bad_alloc);
// delete一个对象
void operator delete(void* rawMemory) throw();
// delete数组
void operator delete(void* rawMemory, std::size_t size) throw();
假设写了一个class专属的operator new,需要接收一个ostream,用来log相关分配信息,这个时候就是placement形式的new.
class Widget {
public:
...
// 这是一个placement new
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
// 这个设计有问题,它只能用户正常的显示调用。
// 但如果一阶段operator new正常,构造函数出错,则会找不到这个delete。
static void operator delete(void* pMemory, std::size_t size)
throw();
// 正确的delete应该这样定义,添加placement new对应参数的placement delete;
static void operator delete(void* pMemory, std::ostream& logStream)
throw();
...
};
// 调用方式
Widget* pw = new(std::cerr) Widget;
在构造函数失败时,运行期系统会寻找“参数个数和类型都与operator new相同”的某个operator delete。
由于客户有可能显示调用delete pw
,所以我们需要同时提供一个正常的operator delete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常抛出)的版本
3.防止遮掩正常的operator new或operator delete
我们还必须小心避免class专属的news遮掩客户期望的其他news(包括正常版本)。
如下面这两种情况会造成遮掩。
// class遮掩global
class Base {
public:
static void* operator new(std::size_t size, std::ostream& logStream) {
throw(std::bad_alloc); // 这个new会遮掩正常的global形式。
}
};
// 注,子作用域是直接不看参数遮掩子作用域的,而非进行重载(除非添加了using后才不遮掩)!!
// 作用域遮掩详情可以看Effective C++的33条
Base* pb = new Base; // 错误,因为正常形式的operaotr new被遮掩了
Base* pb = new(std::cerr) Base; // 正确,调用Base的placement new。
// Derived遮掩Base
class Derived: public Base {
public:
static void* operator new(std::size_t size) {
throw(std::bad_alloc);
}
};
Derived* pd = new(std::clog) Derived; // 错误,因为Base的placement new被遮掩了。
Derived* pd = new Derived; // 没问题,调用Derived的operator new
缺省情况下,C++在global里会提供一下形式的operator new;
void* operator new(std::size_t) throw(std::bad_alloc); // normal new;
void* operator new(std::size_t, void*) throw(); // placement new;
void* operator new(std::size_t, const std::nothrow_t&) throw(); // nothrow版本的new
解决这个遮掩问题的简单办法是,建立一个base class,内含所有正常形式的new和delete:
class StandardNewDeleteForms {
public:
// normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc)
{ return ::operator new(size); }
static void operator delete(void* pMemory) throw()
{ ::operator delete(pMemory); }
// palcement new/delete
static void* operator new(std::size_t size, void* ptr) throw(std::bad_alloc)
{ return ::operator new(size, ptr); }
static void operator delete(void* pMemory, void* ptr) throw()
{ ::operator delete(pMemory, ptr); }
// nothrow new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) throw(std::bad_alloc)
{ return ::operator new(size, nt); }
static void operator delete(void* pMemory, const std::nothrow_t& nt) throw()
{ ::operator delete(pMemory); }
};
凡是想以自定形式扩充标准形式的客户,可利用继承机制及using声明式取得标准形式。
class Widget: public StandardNewDeleteForms {
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 添加一个自定义的placement new
static void* operator new(std::size_t size,
std::ostream& logStream)
throw(std::bad_alloc);
// 添加一个对应的placement delete
static void operator delete(void* pMemory,
std::ostream& logStream)
throw();
};
9 杂项讨论
条款53 不要轻忽编译器的警告
1.不要忽略编译器的警告信息,确定了解它意图说的精确意义。
比如下面这段代码的告警。
class B {
public:
virtual void f() const;
};
class D: public B {
public:
virtual void f();
};
warnning: D::f() hides virtual B::f()
条款33说明了这个告警的问题,它几乎肯定会导致错误的程序行为。
2.不同编译器的警告信息可能不同,他们也有不同的警告标准。
条款54 让自己熟悉包括TR1在内的标准程序库
TR1也是当前C++11后的std新支持的类型
(1)智能指针
tr1::shared_ptr、tr1::auto_ptr、tr1::weak_ptr
(2)tr1::function
可表示任何callable entity(可调用物),只要签名符合目标(包括可以转化类型的情况),见条例35
(3)函数绑定工具
tr1::mem_fn,第一代绑定工具
tr1::bind,第二代绑定工具
(4)hash tables
(5)正则表达式
(6)tuple,可持有任意多个多项的std::pair
(7)tr1::array,大小固定,并不使用动态内存的数组
(8)tr1::reference_wrapper
它是一个引用包裹器,可以包裹一个指向对象或者指向函数指针的引用,既可以通过拷贝构造,也可以通过赋值构造
(9)随机数
(10)数学函数
(11)type traits
(12)tr1::result_of
result_of主要用于目标函数定义的类型推导中,在C++中auto也会自动推导类型,但是初始值不赋值时,auto是不能推导出目标类型,但result_of是可以推导出类型。
条款55 让自己熟悉Boost
Boost可应付的主题至少包括:
(1)字符串和文本处理。printf-link格式化动作,正则表达式,语汇单元分割和解析
(2)容器
(3)函数对象和高级编程lambda
(4)泛型编程,一大组traits classes
(5)模板元编程,针对编译器assertions而写的库,以及Boost MPL程序库
// 创建一个list-like编译期容器,其中收纳三个类型:
// (float, double, long double),并将此容器命名为“floats”
typedef boost::mpl::list<float, double, long double> floats;
// 再创建一个编译期间用以收纳类型的list,以“float”内的类型为基础,
// 最前面再加上“int”,新容器取名为“types”
typedef boost::mpl::push_front<floats, int>::type types;
(6)数学和数值,有理数、八元数、四元数等。
(7)正确性与测试
(8)数据结构,类型安全的unions,以及tuple程序库。
(9)语言间的支持,包括C++和python之间的无缝互操作性(seamless interroperability)
(10)内存,覆盖Pool程序库,支撑指针,scoped_array(一个auto_ptr like的管理数组的智能指针,见条例13)
(11)杂项,包括CRC检验、日期和时间的处理、在文件系统上来回移动等。