读书笔记系列1:《C++必知必会》

时间来到2022年4月13日,已过而立,回忆之前的生涯,如过眼浮云,平淡而不知所踪。此刻尝试记录工作和学习中的点滴,就从第一篇读书笔记开始吧。
本文是读书笔记系列的第一篇,书名是《C++必知必会》,英文名《C++ Common Knowledge: Essential Intermediate Programming》。

Item4 STL

The STL consists of three major kinds of components: containers, algorithms, and iterators. Containers contain and organize elements. Algorithms perform operations. Iterators are used to access the elements of a container. This is nothing new, as many traditional libraries have these components, and many traditional libraries are implemented with templates. The STL’s inspired idea is that containers and the algorithms that operate on them need no knowledge of each other. This sleight of hand is accomplished with iterators.

  • STL包含3大组件:容器,算法,迭代器。另见:容器适配器
  • STL的优秀思想:容器和作用在容器上的算法,无需关心彼此
  • STL定义了7个标准容器(算上string是8个)参考博客cppreference.com STL
  • 算法和迭代器简述

An STL algorithm is an abstraction of a function, implemented as a function template (see Generic Algorithms [60, 221]). Most STL algorithms work with one or more sequences of values, where a sequence is defined by an ordered pair of iterators. The first iterator refers to the first element of the sequence, and the second iterator refers to one past the last element of the sequence (not to the last element). If the iterators refer to the same location, they define an empty sequence.

  • 容器适配器

Containers may also be adapted with container adapters that modify the interface of the container to be that of a stack, queue, or priority queue.

  • STL的特性

The STL conventions do not specify implementation details, but they do specify efficiency constraints on the implementation. In addition, because the STL is a template library, much optimization and tuning can take place at compile time. Many of the naming and information conventions mentioned previously are there precisely to allow significant compile-time optimization. Use of the STL generally rivals the efficiency of hand-coding by an expert, and it beats hand-coding by the average nonexpert or by any team of programmers hands down. The result is also generally clearer and more maintainable.

Item5 引用是别名而非指针

引用和指针3大区别:

  1. 不存在空引用(null reference)
  2. 引用必须初始化
  3. 引用永远指向初始化它的对象

引用的特性:
非常引用(reference to non-const)不可以用字面值或临时量初始化,但是常引用(reference to const)可以。

常量引用特性:

const double& cd = 12.3;//cd指向用1.23初始化的一个临时变量,而非直接指向字面值12.3

When a reference to const is initialized with a literal, the reference is set to refer to a temporary location that is initialized with the literal. Therefore, cd does not actually refer to the literal 12.3 but to a temporary of type double that has been initialized with 12.3.

Item6 数组形参

C++的数组形参会自动退化(decay)为指针(指向数组第一个元素),但是会丢失边界。同理,函数参数也会退化为函数指针,只不过会保留参数类型和返回值类型。

一方面,由于边界会丢失,当函数需要接受一个元素序列的对象,而非单个元素的指针时,最好声明如下:

int average(int arr[]);//形参arr仍然是int *

另一方面,如果需要保留数组边界,可声明如下:

int average(int (&arr) [12]);//只能接受大小为12的整形数组

泛化形式:

template <int n>
int average(int (&arr) [n]);//编译器会推导n的值

//传统做法
int average(int arr [], int arr_sz);

多维数组情形跟一维数组类似,因此其形参是一个指向数组的指针,需要注意的是第2维(以及后续维度)的边界没有退化。

void process(int (*arr)[20]);//arr是一个指针,指向有20个元素的int数组

Item9 新式类型转换操作符

四大转换符:

  • const_cast
  • static_cast
  • reinterpret_cast
  • dynamic_cast

传统类型转换:

char * pc = (char *)0x00ff0000;//传统C风格转换

C++提供了函数风格转换:

typedef char *  PChar;
pc = PChar(0x00ff0000);

以上二者均应避免使用,而是使用新式的转型操作符,他们表达的意思更明确。

const_cast

const_cast可以添加或移除表达式类型中的const或volatile关键字,其作用范围比传统转换威力更小,更精确。

举例:

//如下2个函数
const Person * getEmployee1();
const Employee * getEmployee2();

//转换1,二者差别不大
Person * p1 = (Person *)getEmployee1();
Person * p2 = const_cast<Person*>(getEmployee1());

//转换2
Person * p3 = (Person *)getEmployee2();//编译通过
Person * p4 = const_cast<Person*>(getEmployee2());//编译将不能通过

static_cast

static_cast相对而言用于可跨平台移植的转换,最常见的是在继承体系中,用于将基类指针或引用向下转型为派生类指针或引用。

注意:static_cast无法像const_cast那样改变类型修饰符。

示例:

const Shape * getNextShape();

Circle * circle = static_cast<Circle*>(const_cast<Shape*>(getNextShape()));// downcast

reinterpret_cast

c++标准并没有对reinterpret_cast的表现做太多保证。见名知意,它从位(bit)的角度看待一个对象,从而允许将一个东西视作另一个完全不同的东西。

示例:

This sort of thing is occasionally necessary in low-level code, but it’s not likely to be portable.

foo = reinterpret_cast<int *>(0x00ff0000);//将int视作一个指针
bar = reinterpret_cast<char *>(foo);//将int*视作char*

注意在向下转型时与static_cast的差异,reinterpret_cast只是将基类指针假装为派生类指针,而不改变其值;而static_cast(传统类型转换也是如此)将执行正确的地址操作。

dynamic_cast

谈到继承体系下的转换,不得不提dynamic_cast。dynamic_cast用于执行基类指针向派生类指针的安全转换。不同于static_cast的是,前者只用于多态类型的向下转型(被转换的表达式类型必须是一个带有虚函数的类类型指针),并且执行运行期检查工作,来判定转型的正确性。这意味着前者要付出显著的运行时开销。

//转型失败时,cp为空
if(const Circle * cp = dynamic_cast<const Circle *>(getNextShape()){
	//...
}

另一个不常见的做法:

const Circle & cl = dynamic_cast<const Circle &>(*getNextShape()){
	//...
}

以上操作,当转型失败时,将抛出一个std::bad_cast异常,而不是返回一个null pointer。不存在空引用。

Idiomatically, a dynamic_cast to a pointer is asking a question (“Is this Shape pointer actually pointing to a Circle? If not, I can deal with it.”), whereas a dynamic_cast to a reference is stating an invariant (“This Shape is supposed to be a Circle. If it’s not, something is seriously wrong!”).

附录:什么是不变式(invariant)?

Item10 常量成员函数的意义

The type of the this pointer in a non-const member function of a class X is X * const. That is, it’s a constant pointer to a non-constant X (see Const Pointers and Pointers to Const [7, 21]). Because the object to which this refers is not const, it can be modified.
The type of this in a const member function of a class X is const X * const. That is, it’s a constant pointer to a constant X. Because the object to which this refers is const, it cannot be modified. That’s the difference between const and non-const member functions.

示例:

class X {
  public:
    X() : buffer_(0), isComputed_(false) {}
    //...
    void setBuffer() {
        int *tmp = new int[MAX];
        delete [] buffer_;
        buffer_ = tmp;
    }
    
    // 编译通过,但是不道德。并未修改X对象,只是修改X对象buffer_成员所指向的一些数据
    void modifyBuffer( int index, int value ) const // immoral
        { buffer_[index] = value; }

    // if内的2个赋值语句编译错误,此为const成员函数,不允许修改X对象(成员)
    int getValue() const {
        if( !isComputed_ ) {
            computedValue_ = expensiveOperation(); // error!    
          	isComputed_ = true; // error!                         
        }
        return computedValue_;
    }
  private:
    static int expensiveOperation();
    int *buffer_;
    bool isComputed_;
    int computedValue_;
};

在const成员函数中修改对象

取巧转换,应该抵制这种操作:

int getValue() const {
    if( !isComputed_ ) {
        X *const aThis = const_cast<X *const>(this); // bad idea!
        aThis->computedValue_ = expensiveOperation();
        aThis->isComputed_ = true;
    }
    return computedValue_;
}

正确操作,将要修改的成员声明为mutable

class X {
  public:
    //...
    int getValue() const {
        if( !isComputed_ ) {
            computedValue_ = expensiveOperation(); // fine...
            isComputed_ = true; // also fine...
        }
        return computedValue_;
    }
  private:
    //...
    mutable bool isComputed_; // can now be modified
    mutable int computedValue_; // can now be modified
};

类的非静态成员可以被声明为mutable,这样将允许类的常量成员函数(当然也包括非常量成员函数)修改他们的值。

成员函数的重载:常量和非常量版本

对成员函数的this指针加上const修饰符,可以解释如何实现重载:

class X {
  public:
    //...
    int &operator [](int index);
    const int &operator [](int index) const;
    //...
};

重载的二元成员操作符的左实参,是通过this指针传入的。当对X对象执行索引操作时,X对象的地址即为this指针:

int i = 12;
X a;
a[7] = i; // this is X *const because a is non-const
const X b;
i = b[i]; // this is const X *const because b is const

另一个例子,两个参数的非成员二元操作符:

X operator +( const X &, const X & );

该函数的等价形式:

class X {
  public:
    //...
    X operator +( const X &rightArg ); // left arg is non-const! 
    X operator +( const X &rightArg ) const; // left arg is const
    //...
};

Item11 编译器会在类中放东西

当一个类中声明一个或多个虚函数时,编译器会在该类的每个对象中插入一个指向虚函数表的指针(实际上C++标准并未要求如此,但所有的C++编译器都是这样实现的)。

不同编译器对于虚函数表指针的实现不同,有的在对象开头,有的在对象末尾,而如果涉及多继承,那么若干个虚函数表可能散步于对象之中。因此如果需要编写可移植的代码,不要想当然做任何假定。

虚继承(virtual inheritance):对象将会通过嵌入的指针,嵌入的偏移或其他非嵌入的信息来保持对其虚基类子对象位置的跟踪。因此即便没有声明虚函数,也有可能被插入一个虚函数表指针vtable/__vfptr。

知识点:

  • POD(Plain Old Data). int, double等内建类型及包含它们的struct和union;有
// a POD-struct
struct S { 
    int a;
    double b;
};

// no longer a POD-struct!
struct S { 
    int a;
    double b;
  private:
    std::string c; // some maintenance
};

// not a POD
struct T { 
    int a_; // offset of a_ unknown
    virtual void f(); // offset of vptr unknown
};
  • 应该在高层(ctor/dtor/assignment)操作类对象,而不应该把它当成一组位的集合。在不同平台上,高层的操作会做相同的事情,而底层实现可能不同。
  • 例如复制一个类对象,不要使用memcpy这样的标准内存块复制函数,因为它是用来复制存储区,而不是用来复制对象的(参考Item35 placement new)。而是应该使用对象的初始化或赋值操作。
  • 对象的构造函数是编译器建立隐藏机制的地方,该隐藏机制实现对象的虚函数,以及此类的东西。
  • 同样,复制一个对象时,也不要覆盖这些类的内部机制。例如,赋值操作不应改变对象的虚函数表指针的值,它们由构造函数设置,并且在整个生命周期内保持不变
  • 参考POD struct T. 不要假定类对象的一个成员位于一个特定位置。如:虚函数表指针位于0偏移处,或声明的第一个数据成员位于0偏移处。

Item12 赋值和初始化不同

赋值和初始化是不同的操作,它们具有不同的用途和实现。
除了赋值操作时,其他操作均为初始化:声明,函数返回值,参数传递,异常捕获。
赋值和初始化本质上是不同的操作,不仅仅因为它们用于不同的上下文,而且还因为它们做的事情不同。

Assignment occurs when you assign. All the other copying you run into is initialization, including initialization in a declaration, function return, argument passing, and catching exceptions.

  • 通过非标准String来看,相比初始化,赋值有点像一个析构动作后跟一个构造动作。在String赋值过程中,目标在采用源重新初始化之前,必须被清理掉。
String &String::operator =( const char *str ) {
    if( !str ) str = "";
    char *tmp = strcpy( new char[ strlen(str)+1 ], str );
    delete [] s_;
    s_ = tmp;
    return *this;
}

由于一个赋值操作会清理掉左边的实参,因此永远不要对一个未初始化的存储区执行用户自定义赋值操作:

String *names = static_cast<String *>(::operator new( BUFSIZ ));
names[0] = "Sakamoto"; // oops! delete [] uninitialized pointer!

Item13 复制操作

复制构造和复制赋值是两种不同的操作,从技术角度说,它们没有任何关联;然而却又总是被放到一起,同时出现,且必须兼容。

对于类X,二者分别声明如下:

X(const X&);				//copy construction
X& operator=(const X&);		//copy assignment

复制赋值与复制构造虽然是不同的操作,但它们的结果却是相同的。以下两种方式,b的结果相同:

//使用Handle的复制赋值将a赋给b
Handle a = ...
Handle b;
b = a;

//使用Handle的复制构造用a初始化b
Handle a = ...
Handle b(a);

当使用标准容器时,这一点尤为重要,因为它们的实现常常使用复制构造来代替复制赋值。

Item14 函数指针

以下是一个返回值为void,参数为1个int的函数指针:

void (*fp)(int); // ptr to function

跟指向数据的指针一样,指向函数的指针也可以为空,否则它就应该指向一个其类型的函数。

注意:将一个函数的地址初始化或赋值给一个函数指针时,无需显式地取得函数地址,编译器知道隐式地取得函数地址,因此&操作符可有可无:

extern void h( int );

fp = h; // OK, point to h
fp = &h; // OK, take address explicitly

同样的,函数指针也无需显式解引用:

fp(12);//隐式解引用
(*fp)(12);//显式解引用
  • 和void*指针可以指向任何数据类型的数据不同,不存在可以指向任何函数类型的函数指针
  • 非静态成员函数的地址不是一个指针,因此不可以将一个函数指针指向一个非静态成员函数。
  • 函数指针的一个传统用途是实现函数回调。
  • 一个函数指针指向内联函数是合法的。但是,通过函数指针调用内联函数不会导致内联式的函数调用,编译器无法在编译期确定(函数指针?)将会调用什么函数。
  • 函数指针持有一个重载函数的地址是合法的。会在候选(重载)函数中挑选最佳匹配的函数。

标准库中函数指针作为回调函数的应用:标准函数std::set_new_handler用于设置回调,当全局operator new函数无法履行一个内存分配请求时,该函数被调用。

void begForgiveness() {
    logError( "Sorry!" );
    throw std::bad_alloc();
}
//...
std::new_handler oldHandler =
    std::set_new_handler(begForgiveness);

//The standard typename new_handler is a typedef:
typedef void (*new_handler)();

//回旋式获得当前回调
std::new_handler current
    = std::set_new_handler( 0 ); // get...
std::set_new_handler( current ); // ...and restore!

Item15 指向类成员的指针并非指针

指向类成员的指针这个术语其实并不合适,因为它们既不包含地址,行为也不像指针。

int classname::* ip;//一个指针,指向classname的一个int成员

与常规指针不同,一个指向成员的指针并不指向一个具体的内存位置它指向一个类的特定成员,而不是指向一个特定对象的特定成员。通常最清晰的做法是将指向数据成员的指针看作一个偏移量。

C++标准对于一个指向数据成员的指针并未说明其细节,只说明了它的语法以及必须表现出来的行为。
大多数编译器都将指向数据成员的指针实现为一个整数,其中包含被指向成员的偏移量,另外加上1(加1是为了让值0可以表示一个空的数据成员)。这个偏移量说明了特定成员的位置距离对象的起点有多少个字节。

此用法不如指向成员函数的指针常用。

Item16 指向成员函数的指针并非指针

获取非静态成员函数的地址时,得到的不是一个地址,而是一个指向成员函数的指针。

class Shape {
  public:
    //...
    void moveTo( Point newLocation );
    bool validate() const;
    virtual bool draw() const = 0;
    //...
};
class Circle : public Shape {
    //...
    bool draw() const;
    //...
};

一个指向非静态成员函数的指针:

void (Shape::*mf1)( Point ) = &Shape::moveTo; // not a pointer

和指向常规函数的指针不同,指向成员函数的指针可以指向一个常量成员函数

bool (Shape::*mf2)() const = &Shape::validate;

语法生涩,并不常用。

Item17 处理函数与数组声明

指向函数的指针声明和指向数组的指针声明很容易混淆,主要原因在于函数和数组修饰符的优先级比指针修饰符的优先级高,因此需要使用圆括号:

int * f();//一个返回值为int*的函数
int (*fp)();//一个返回值为int的函数指针

具有高优先级的数组修饰符同样存在此问题:

const int N = 12;
int *a1[N]; // array of N int *
int (*ap1)[N]; // ptr to array of N ints

通常结合typedef使用,增强代码可读性,更易于维护:

//声明
typedef void (*new_handler)();
//使用
new_handler set_new_handler( new_handler );

另有,函数引用,不常用,类似常量函数指针

int aFunc( double ); // func
int (&rFunc)(double) = aFunc; // ref to func

int (*const pFunc)(double) = aFunc; // const ptr to func

Item18 函数对象

智能指针类似,函数对象也是一个普通的类对象。

智能指针通过重载->和星号(可能还有->*)来模仿指针的行为;而函数对象类型则通过重载函数调用操作符(),来创建类似智能指针的东西。

Fibonacci示例:

class Fib {
  public:
    Fib() : a0_(1), a1_(1) {}
    int operator ();
  private:
    int a0_, a1_;
};
int Fib::operator () {
    int temp = a0_;
    a0_ = a1_;
    a1_ = temp + a0_;
    return temp;
}

//调用
Fib fib;
fib();//或fib.operator()

注意与函数指针的差别:函数对象可以处理需要状态的函数,或指向成员函数的指针。
该部分示例代码见原书P51。

Item19 Command模式与好莱坞法则

当一个函数对象用作回调时,就是一个Command(命令)模式的实例。
回调是一种常见的编程技术,传统上被实现为一个指向函数的简单指针。

用户设置回调函数,交给框架代码。框架知道何时去做一些事情,但具体干什么,框架一无所知。这种责任的分割,通常叫做“好莱坞法则”。

“Don’t call us; we’ll call you.”

通常将一个函数对象代替函数指针,将一个函数对象与“好莱坞法则”结合使用,好处有三:

  1. 面向对象方式,函数对象可以封装数据。
  2. 函数对象可以通过虚拟成员表现出动态行为。
  3. 可以处理类层次结构,而不用去处理较为原始的、缺乏灵活性的结构(函数指针)。

Item20 STL函数对象

一个美国州比较的示例:

class State {
  public:
    //...
    int population() const;
    float aveTempF() const;
    //...
};

比较器:普通函数形式。当一个函数名字作为参数时,会退化为一个函数指针。函数通过指针传递时,无法被内联。

inline bool popLess( const State &a, const State &b )
    { return a.population() < b.population(); }

//排序
State union[50];
//...
std::sort( union, union+50, popLess ); // sort by population

比较器:函数对象。重载()操作符。会被内联处理,编译器知道其类型PopLess。

struct PopLess : public std::binary_function<State,State,bool> {
    bool operator ()( const State &a, const State &b ) const
        { return popLess( a, b ); }
};

//排序
sort( union, union+50, PopLess() );//匿名PopLess对象
//或者具名形式
PopLess comp;
std::sort( union, union+50, comp );

STL中sort这类的泛型算法都是这样编写的:函数指针和函数对象都可以用来实例化它们,只要此二者可以采用典型的函数调用语法即可。

Item21 重载与重写不同

重载(overload)与重写(override)没有任何关系,二者是两个完全不同的概念。

当同一个作用域内的两个或多个函数名字相同但签名不同时,就会发生重载。函数的签名由所声明的参数的数量和类型构成。返回值不作为重载的区分条件。

当派生类函数和基类虚函数具有相同的名字和签名时,就会发生重写。

class B {
  public:
    //...
    virtual int f( int );                                                
    void f( B * );                                                       
    //...
};


class D : public B {
  public:
  	//重写了B::f(int)
    int f( int );
    //没有重写任何函数,因为B::f(B*)不是virtual,但是它却重载了D::f(int)
    int f( B * );
};

函数f在类B中被重载了,但却不是好的习惯。原因有二:
可能并不希望重载一个虚函数,也可能不希望在整型和指针类型之间进行重载。

See C++ Gotchas and Effective C++, respectively, to see why.

Item22 Template Method模式

Template Method是好莱坞设计模式的一个例子。

class App {
  public:
    virtual ~App();
    //...
    void startup() { // Template Method
        initialize();
        if( !validate() )
            altInit();
    }
  protected:
    virtual bool validate() const = 0;
    virtual void altInit();
    //...
  private:
    void initialize();
    //...
};



//The nonvirtual startup Template Method 
//calls down to customizations provided by derived classes:

class MyApp : public App {
  public:
    //...
  private:
    bool validate() const;
    void altInit();
    //...
};

Item23 名称空间

从某些方面来说,名称空间引入了复杂性,但名称空间的大多数用途都很简单。本质上,名称空间是对全局作用域的细分。

连续显示的使用名称空间限定符会很乏味,一种缓解方式是使用using指令。从本质上来说,使用using指令从名称空间中导入名字,使他们在该using指令的作用域内无需进行限定就可以访问。将using指令放在全局作用域内是愚蠢的做法,又回到了起点。

void aFunc() {
    using namespace std; // using directive
    vector<int> a; // OK
    cout << "Hello!" << endl; // OK
    //...
}

如果using作用域内出现同名名字,就会隐藏该名称空间中的相应名字。

void aFunc() {
    using namespace std; // using directive
    //...
    int vector = 12; // a poorly named local variable...
    vector<int> a; // error! std::vector is hidden               
    std::vector<int> b; // OK, can use explicit qualification
    //...
}

另一种方法是使用using声明,它通过一个真正的声明提供对名字空间中名字的访问。

void aFunc() {
    using std::vector; // using declaration
    //...
    int vector = 12; // error! redeclaration of vector          
    vector<int> a; // OK
    //...
}

另一种对付冗长的名称空间的方式是使用别名(alias):

namespace S = org_semantics;
//c++11定义别名
//参考链接 http://c.biancheng.net/view/3730.html
using PA = std::unique_ptr<A>;

另外存在匿名的名称空间,每个匿名的名称空间,编译器会为其生成一个唯一的名称。

namespace {
    int anInt = 12;
    int aFunc() { return anInt; }
}

Item24 成员函数查找

调用一个成员函数的步骤:

  1. 编译器查找函数的名字
  2. 从可用候选者中选择最佳匹配函数
  3. 检查是否具有访问该匹配函数的权限

看以下代码:

class B {
  public:
    void f( double );
};
class D : public B {
    void f( int );
};

D d;
d.f( 12.3 ); 

本例编译失败:

error C2248: “D::f”: 无法访问 private 成员(在“D”类中声明)

本例中虽然B::f(double)是更佳匹配,但是一旦在内层作用域中找到一个名字,编译器就不会到外层作用域中继续查找该名字。内层作用域中的名字会隐藏外层作用域中的名字。 与Java不同,在Java中属于重载关系。

再看这个例子:

class E : public D {
    int f;
};
//...
E e;
e.f( 12 ); // error!

我们同样得到一个编译错误,因为在作用域E中查找名字f,结果找到了一个数据成员,而不是成员函数。

Item25 根据参数查找

根据参数查找(Argument Dependent Lookup, ADL)

ADL背后蕴含的思想非常简单。当查找一个函数调用表达式中的函数名字时,编译器也会到“包含函数调用实参的类型”的名称空间中检查。

namespace org_semantics {
    class X { ... };
    void f( const X & );
    void g( X * );
    X operator +( const X &, const X & );
    class String { ... };
    std::ostream operator <<( std::ostream &, const String & );
}
//...
int g( org_semantics::X * );
void aFunc() {
    org_semantics::X a;
    f( a ); // call org_semantics::f
    g( &a ); // error! ambiguous...                                
    a = a + a; // call org_semantics::operator +
}

普通的查找是不会发现函数org_semantics::f的,因为它被嵌套在一个名称空间org_semantics内,并且对f的使用需要以该名称空间的名字加以限定。 然而,由于实参a的类型被定义于org_semantics名称空间中,因此,编译器也会到该名称空间中查找候选函数。

Item26 operator函数查找

主要讨论编译器在不同的地方查找候选函数,而非重载。

Item27 能力查询

在多继承中,进行横向转型(cross-cast),因为它试图在一个类层次结构中进行横向转换,而非向上或向下转换。

class Circle : public Shape, public Rollable { // circles roll
    //...
    void draw() const;
    void roll();
    //...
};

class Square : public Shape { // squares don't
    //...
    void draw() const;
    //...
};

Shape *s = getSomeShape();
if( Rollable *roller = dynamic_cast<Rollable *>(s) )
{
	roller->roll();
}

在上例中,如果s指向的是一个Square,那么dynamic_cast将会产生一个空指针。

Item28 指针比较的含义

在c++中,一个对象可以有多个有效的地址,因此指针比较不是地址问题,而是对象同一性(是否为同一个对象)问题。

不具有继承关系的对象之间,不可以进行指针比较。

class Shape {  };
class Subject {  };
class ObservedBlob : public Shape, public Subject {  };

int main() {
	ObservedBlob *ob = new ObservedBlob;
	Shape *s = ob; // predefined conversion
	Subject *subj = ob; // predefined conversion

	//possible output: (void * )ob=000002DC727FFC70,(void * )s=000002DC727FFC70,(void * )subj=000002DC727FFC71
	std::cout<< "(void * )ob=" << (void *)ob << "," << "(void * )s=" << (void *)s << "," << "(void * )subj=" << (void *)subj << "\n";

	if (ob == s) {
		std::cout << "ob == s\n";
	}
		
	if (subj == ob) {
		std::cout << "ob == subj\n";
	}
	return 0;
}

在这个例子中,上述两个条件表达式结果均为true,即使ob,s和subj中包含的地址并不相同。

不管是哪种内存布局,ob,s和subj都指向同一个ObservedBlob对象,因此编译器必须确保以上两个条件表达式为true。编译器通过将参与比较的指针值之一调整一定的偏移量来完成这种比较。

经验:一般而言,当处理指向对象的指针或引用时,必须小心避免丢失类型信息(void*)。

Item29 虚构造函数与prototype模式

不存在真正的虚构造函数,但是生成对象的copy通常涉及到通过一个虚函数对其类的构造函数的间接调用,效果上的虚构造函数。

class Meal {
   public:
     virtual ~Meal();
     virtual void eat() = 0;
     virtual Meal *clone() const = 0;
     //...
};

class Spaghetti : public Meal {
   public:
     Spaghetti( const Spaghetti & ); // copy ctor
     void eat();
     //注意这里修改了返回类型,参见item31 协变返回类型
     Spaghetti *clone() const
         { return new Spaghetti( *this ); } // call copy ctor
     //...
};

Item30 Factory Method模式

Factory Method的本质在于,基类提供一个虚函数,用于产生适当的“产品”。每一个派生类可以重写继承的虚函数,为自己产生适当的产品。

在这个案例中,我们具备了使用一个未知类型的对象(某种Employee)来产生另一个未知类型对象(某种HRInfo)的能力。

Factory Method通常是治疗一系列运行期类型查询(RTTI)问题的良方。

Factory Method:

class Employee {
  public:
    //...
    virtual HRInfo *genInfo() const = 0; // Factory Method
    //...
};

class Temp : public Employee {
  public:
    //...
    TempInfo *genInfo() const
        { return new TempInfo( *this ); }
    //...
};

糟糕的设计:

class Employee {                                                 
  public:                                                        
    enum Type { SALARY, HOURLY, TEMP };                          
    Type type() const { return type_; }                          
    //...                                                        
  private:                                                       
    Type type_;                                                  
    //...                                                        
};                                                               
//...                                                            
HRInfo *genInfo( const Employee &e ) {                           
    switch( e.type() ) {                                         
    case SALARY:                                                 
    case HOURLY: return new StdInfo( e );                        
    case TEMP:return new TempInfo( static_cast<const Temp*>(e) );
    default: return 0; // unknown type code!                     
    }                                                            
}    

Item31 协变返回类型

协变返回类型(Covariant Return Types)

一般来说,一个重写的函数必须与被它重写的函数具有相同的返回类型。

然而这个规则对于“协变返回类型”的情形显得不那么严格。

B是一个类类型,其中D共有派生与B(is-a关系)
如果基类虚函数返回B*,那么重写的派生类函数可以返回D*;
如果基类虚函数返回B&,那么重写的派生类函数可以返回一个D&。

Item32 禁止复制

访问修饰符(public,protected和private)可以用于表达和执行高级约束技术,指明怎样去使用一个类。

最常见的一种是不接受对象的复制操作,这是通过将其复制操作声明为private,同时不为之提供定义做到:

class NoCopy{
public:
	NoCopy(int);
	//...
private:
	NoCopy(const NoCopy&);//复制构造函数
	NoCopy& operator=(const NoCopy&);//复制赋值函数
};

如果二者不声明为private,那么编译器将会隐式地将它们声明为公有的,内联的成员。声明为private之后,这两个操作的任何使用,都将产生编译器错误。

示例:

void aFunc( NoCopy );
void anotherFunc( const NoCopy & );
NoCopy a( 12 );
NoCopy b( a ); // error! copy ctor                            
NoCopy c = 12; // error! implicit copy ctor                   
a = b; // error! copy assignment                              
aFunc( a ); // error! pass by value with copy ctor            
aFunc( 12 ); // error! implicit copy ctor                     
anotherFunc( a ); // OK, pass by reference
anotherFunc( 12 ); // OK

Item33 制造抽象基类

对应java中的抽象类,不允许创建该类的实例或对象。

多种实现方式:

  1. 声明至少一个纯虚函数(或者从别的类继承一个纯虚函数且不予实现)
  2. 通过确保类中不存在公有的构造函数
  3. 将析构函数设置为受保护的,非虚的?

Item34 进制或强制使用堆分配

有时候一些类的对象不应该被分配到堆上,通常是为了确保该对象的析构函数得到调用。

如维持body对象引用计数的handle对象

具有自动存储区的类的局部对象,其析构函数会被自动地调用(exit和abort等非正常终止情形除外);具有静态存储区的类的对象同样(abort情形除外)。而堆分配的对象则必须显式地销毁。

实现方式,将堆内存分配定义为不合法:

class NoHeap {
protected:
	//size_t参数将被自动初始化为对象的大小(字节)
	void * operator new(size_t) { return 0; }
	//void *参数被自动设置为被delete对象的地址
	void operator delete(void *){}
private:
	//同时禁用堆上数组分配:array new和array delete
	void * operator new[](size_t);
	void operator delete[](void *);
};

说明:

  1. 给出new和delete的定义,是因为在一些平台上它们可能会被构造函数和析构函数隐式地调用。
  2. 声明为protected,因为它们可能会被派生类的构造函数和析构函数隐式地调用,如果不用做派生类,可声明为private。
  3. 禁用堆上数组分配,array new和array delete,声明为private且不予定义即可。

另外一些场合,我们会鼓励在堆上创建对象,将析构函数声明为私有即可:

class OnHeap {
private:	
	~OnHeap() {}//私有析构函数
public:
	//并提供公有接口
	void destroy() {
		delete this;
	}
};

测试:

//错误!隐式调用OnHeap的私有析构函数
//OnHeap oh1;
int main() {
	//禁止堆上分配
	NoHeap inst;
	//错误!new不可访问
	//NoHeap * p = new NoHeap;
	//错误!delete不可访问
	//delete p;

	//强制堆上分配
	{
		//错误!隐式调用OnHeap的析构函数
		//OnHeap oh2;
		OnHeap *op = new OnHeap;
		//使用公有接口delete堆上对象
		op->destroy();
		//错误!无法访问~OnHeap
		//delete op;
	}
	return 0;
}

Item35 placement new

c++不允许直接调用构造函数,但是可以通过使用placement new(定位new)欺骗编译器调用构造函数。

//placement new的实现
void * operator new(size_t, void * p) throw()
{
	return p;
}

placement new是operator new的标准重载版本,也位于全局名称空间中,但是不同于operator new,语言明令禁止用户替换placement new。

placement new的实现忽略了表示大小的实参,直接返回第二个实参。它允许我们在一个特定的位置“放置”对象,起到调用构造函数的效果。

区分new操作符和命名为operator new的函数很重要。new操作符不可以被重载,所以其行为总是一样的。它调用一个名为operator new的函数,然后初始化返回的存储区。如果希望对内存分配方式进行任何改变,均需要通过operator new的不同重载版本实现,而非通过new操作符实现。同理,delete操作符和operator delete。

placement new是函数operator new的一个版本,它不实际分配任何存储区,仅仅返回一个(可能)指向已经分配好空间的指针。所以,不要对其进行delete操作。然而它又确实创建了一个对象,因此应使用该对象的析构函数销毁这个对象。

placement array new用于在给定的位置创建对象数组。

使用placement new解决缓冲区问题:

//对象数组可能出现的问题:当数组被分配时,必须通过调用一个默认的构造函数而初始化每一个元素。
string *sbuf = new string[BUFSIZ]; // BUFSIZ default ctor calls!  
int size = 0;                                                     
void append( string buf[], int &size, const string &val )         
    { buf[size++] = val; } // wipe out default initialization! 
//如果只使用了数组的一部分元素,或者元素立即被赋值,以上做法效率很低。
//更糟糕的是,如果数组的元素类型没有默认构造函数,将会产生编译器错误。

//使用placement new通常用于解决此类缓冲区问题:采用这种方式,
//缓冲区占用的存储区的分配,可以避免被默认的构造函数初始化。
const size_t n = sizeof(string) * BUFSIZE;
string *sbuf = static_cast<string *>(::operator new( n ));
int size = 0;

//使用placement new通过复制ctor初始化元素
void append( string buf[], int &size, const string &val )
    { new (&buf[size++]) string( val ); } // placement new

//清理工作
void cleanupBuf( string buf[], int size ) {
    while( size )
        buf[--size].~string(); // destroy initialized elements
    ::operator delete( buf ); // free storage
}

以上方式快速灵活,广泛应用于大多数标准库容器的实现。

Item36 特定于类的内存管理

我们无法对new操作符和delete操作符(这里指的是new和delete操作)做什么,它们的行为是固定的。但可以改变它们所调用的operator new和operator delete(为类声明这两个成员函数)。

以Handle类为例,在一个new表达式中分配一个Handle的对象时,编译器首先会在Handle的作用域内查找一个operator new,如果没找到,它将会使用全局作用域中的operator new。operator delete同理,因此此二者最好同时定义。

**成员operator new和operator delete是静态成员函数。**静态成员函数没有this指针,它们只负责获取和释放对象的内存区,也不需要this指针。同其他静态成员函数一样,它们可以被派生类继承。

如果派生类已经声明了自己的operator new和operator delete,编译器将首先采用它们,而不再使用继承来的版本;基类如果定义了operator new和operator delete,要确保基类的析构函数是虚拟的,否则通过基类指针来删除一个派生类对象的结果就是未定义的。

一个常见的误解是以为使用哦new和delete操作符就意味着使用堆(或自由存储区)内存,其实并非如此。使用new操作符唯一能表明的是名为operator new的函数将被调用,且该函数返回一个指向某块内存的指针。

全局的operator new和operator delete的确是从堆上分配内存,但成员operator new和operator delete可以做任何事。对于分配的内存到底从哪里来没有任何限制:可能来自一个特殊的堆,也可能来自一个静态分配的块,也可能来自一个标准容器内部,也可能来自某个函数范围的局部存储区。

不使用堆内存示例:

struct rep {
    enum { max = 1000 };
    static rep *free; // head of freelist
    static int num_used; // number of slots used
    union {
        char store[sizeof(Handle)];
        rep *next;
    };
};
static rep mem[ rep::max ]; // block of static storage
void *Handle::operator new( size_t ) {
    if( rep::free ) { // if something on freelist
        rep *tmp = rep::free; // take from freelist
        rep::free = rep::free->next;
        return tmp;
    }
    else if( rep::num_used < rep::max ) // if slots left
        return &mem[ rep::num_used++ ]; // return unused slot
    else // otherwise, we're...
        throw std::bad_alloc(); // ...out of memory!
}
void Handle::operator delete( void *p ) { // add to freelist
    static_cast<rep *>(p)->next = rep::free;
    rep::free = static_cast<rep *>(p);
}

Item37 数组分配

array new和array delete

两个混乱的点:

  1. 类只定义了operator new和operator delete,却没有定义数组情形:operator new[]和operator delete[],这种情况下,数组情形调用全局的operator new和operator delete。从逻辑上来说,此四者同时出现。而如果目的是想调用全局的数组分配操作,那么可定义“仅仅转发对全局形式的调用”可以让事情变得清晰。而如果不鼓励数组分配,那么可将数组形式的函数声明为private且不提供定义。
class Handle {
  public:
    //...
    void *operator new( size_t );
    void operator delete( void * );
    void *operator new[]( size_t n )
        { return ::operator new( n ); }
    void operator delete[]( void *p )
        { ::operator delete( p ); }
    //...
};
  1. 传递给array new的那个表示大小的参数值,取决于函数是如何被调用的。
  • 当operator new被隐式调用时,编译器会决定需要多少内存:对象的大小
aT = new T;//调用operator new(sizeof(T));
  • 直接调用operator new,需要明确指明希望分配的字节数
aT = static_cast<T*>(operator new( sizeof(T) ));
  • 直接调用array new
aryT = static_cast<T*>( operator new[](n * sizeof(T) ));
  • 隐式调用array new时,编译器常常会略微增加一些内存请求
aryT = new T [n];//请求内存为n * sizeof(T) + delta字节

所请求的额外空间一般由运行期内存管理器(runtime memory manager)来记录数组的一些信息(分配的元素个数,每个元素大小等),这些信息对于回收内存是必不可少的。不过事情远没有这么简单,编译器未必都请求额外的内存空间,额外空间的大小也会发生变化。

请求内存数量的区别通常只在编写非常底层代码时才需要考虑,这种情况下,数组的存储区被直接处理。通常最简单的做法是避免直接调用array new以及编译器所执行的有关干预,取而代之的是使用普通的operator new(参见placement new)。

Item38 异常安全公理

  • 公理1:异常是同步的
    异常是同步的并且只能发生在函数调用的边界。因此,诸如预定义类型的算术操作、预定义类型(尤其指针)的赋值,以及其他低层操作不会导致异常发生(它们可能会导致产生某种信号或中断,但这些东西都不是异常)。

    操作符重载和模板使得情形变得复杂化了,因为通常很难判定一个给定的操作是否会导致一个函数调用并可能抛出异常。 例如,对字符串指针赋值,可以肯定不会抛出异常,但是如果对一个用户自定义的String进行赋值,就有可能发生异常。

    由于存在这种不确定性,因而模板内所有可能的函数调用都必须假定为就是函数调用,包括中缀操作符、隐式转换等。

  • 公理2:对象的销毁是异常安全的
    按照惯例,析构函数、operator delete以及operator delete[]不会抛出异常。

  • 公理3:交换操作不会抛出异常
    这同样是c++社群共识之上的公理。交换(swap)的使用很常见,尤其是在STL的实现中,无论何时只要执行一个sort、reverse、partition以及其他许多操作,都会涉及到交换操作。

Item39 异常安全的函数

Herb Sutter在Exceptional C++中的总结:首先做任何可能会抛出异常的事情(但不会改变对象重要的状态),然后以不会抛出异常的操作作为结束。

举例:

void Button::setAction( const Action *newAction ) {
    Action *temp = newAction->clone(); // off to the side...
    delete action_; // then change state!
    action_ = temp;
}

上述clone是一个虚函数,我们做最坏的假设:若clone抛出一个异常将会导致从Button::setAction中退出,不会对任何人造成伤害。

糟糕的实现:

void Button::setAction( const Action *newAction ) {              
    delete action_; // change state!                             
    action_ = newAction->clone(); // then maybe throw?           
}

这个版本中,先于clone执行delete操作,若clone抛出异常,将会使得Button对象处于不一致的状态。

提醒:编译正确的异常安全代码其实很少使用try语句。

带有多余的try…catch语句的版本:

void Button::setAction( const Action *newAction ) {              
    delete action_;                                              
    try {                                                        
        action_ = newAction->clone();                            
    }                                                            
    catch( ... ) {                                               
        action_ = 0;                                             
        throw;                                                   
    }                                                            
}      

对比之下,原先的版本更短小精悍,更简单,也更异常安全,因为一旦发生异常,Button对象的状态不仅稳固,而且不会发生改变。

因此,只要可能,尽量少用try语句块。而主要在这些地方使用它们:确实希望检查一个传递的异常的类型,为的是对它做一些处理。在实际中,这些地方通常是代码和第三方库之间、以及代码和操作系统之间的模块分界处。

Item40 RAII

RAII(Resource Acquisition Is Initialization)是一项很简单的技术,它利用C++对象生命周期的概念来控制程序的资源,例如内存、文件句柄、网络连接以及审计追踪等

RAII基本技术原理很简单:如果希望对某个重要资源进行跟踪,那么创建一个对象,并将资源的生命周期和对象的生命周期相关联。 最简单的RAII形式:创建一个对象,在其构造函数中获取一份资源,而析构函数释放这份资源。

举例:

void f() {
    ResourceHandle rh( new Resource );
    //...
    if( iFeelLikeIt() ) // no problem!
        return;
    //...
    g(); // exception? no problem!
    // rh destructor performs deletion!
}

注意:

使用RAII时,只有一种情况无法确保析构函数得到调用,就是当ResourceHandle对象被分配到堆上时,这时只有显式地delete该对象。其实还有一些边缘性的情形,包括调用abort或exit,以及抛出的异常从未被捕获而导致不确定的情形。

Trace示例:

class Trace {
  public:
    Trace( const char *msg ) : msg_(msg)
        { std::cout << "Entering " << msg_ << std::endl; }
    ~Trace()
        { std::cout << "Leaving " << msg_ << std::endl; }
  private:
    std::string msg_;
};

//调用trace追踪打印消息
void f() {
    Trace tracer( "f" ); // print "entering" message
    ResourceHandle rh( new Resource ); // seize resource
    //...
    if( iFeelLikeIt() ) // no problem!
        return;
    //...
    g(); // exception? no problem!
    // rh destructor performs deletion!
    // tracer destructor prints exiting message!
}

这个示例还展示了关于构造函数和析构函数结构激活的一个重要定式:这些激活形成了一个栈。
确切地说,先于rh声明并初始化tracer,这样就会保证rh将于tracer之前被销毁(后初始化的先销毁)。 推而广之,无论何时我们声明了一系列的对象,这些对象在运行期将会以特定的顺序被初始化,并最终以相反的顺序被销毁。这个性质对资源的获取和释放尤其重要,因为通常资源必须以特定的顺序进行获取且以相反的顺序进行释放。

这种基于栈的行为甚至延伸到了个体对象的初始化和析构方面。一个对象的构造函数按照其基类子对象(在继承列表中)声明的顺序来初始化各个基类子对象,接着按照数据成员声明的顺序来初始化初始化各数据成员,然后执行构造函数的本体。对于析构行为,就是“回退着执行”。首先执行析构函数本体,接着按与声明顺序相反的顺序销毁对象的数据成员,最后按与声明相反的顺序销毁对象的基类子对象。

Item41 new、构造函数和异常

关于new操作符的使用有一个明显的问题,它实际上执行两个不同的操作:

  1. 调用名为operator new的函数来分配一些存储区
  2. 调用一个构造函数将未被初始化的存储区变成一个对象
String * title = new String("Kicks");

问题在于,如果发生了一个异常,我们说不清到底是operator new抛出来的,还是String构造函数抛出来的。

搞清楚这一点很重要,因为如果operator new成功了,而构造函数抛出异常,我们就应该调用operator delete来归还已分配的存储区;如果抛出异常的函数是operator new,那么就没有任何内存得到分配(将会抛出std::bad_alloc异常),就无需调用operator delete。

糟糕的手动实现:

String *title // allocate raw storage                                   
    = static_cast<String *>(::operator new(sizeof(String));             
try {                                                                   
    new( title ) String( "Kicks" ); // placement new                    
}                                                                       
catch( ... ) {                                                          
    ::operator delete( title ); // clean up if ctor throws              
}   

这种手动实现的代码开始可能正常工作,但是一旦为String增加了operator new/delete成员的话,将无法正确工作。

幸运的是,编译器可以帮我们处理这种情况:

String *title = new String( "Kicks" ); // use members new/delete if present
String *title = ::new String( "Kicks" ); // use global new/delete

编译器总是能正确对应new与delete的版本。

Item42 智能指针

智能指针是一个类类型,它假装成指针,但额外提供了内建指针所无法提供的能力。通常一个智能指针通过使用类的构造函数、析构函数和复制操作符所提供的能力,来控制(或跟踪)它所指向的东西的访问,而内建指针在这方面无能为力。

所有智能指针都重载->和*操作符,从而可以采用标准指针语法来使用它们。智能指针通常采用类模板来实现,从而使它们可以指向不同类型的的对象。

一个简单的智能指针实现:

template <typename T>
class CheckedPtr {
  public:
    explicit CheckedPtr( T *p ) : p_( p ) {}
    ~CheckedPtr() { delete p_; }
    T *operator ->() { return get(); }
    T &operator *() { return *get(); }
  private:
    T *p_; // what we're pointing to
    T *get() { // check ptr before returning it
        if( !p_ )
            throw NullCheckedPointer();
        return p_;
    }
    CheckedPtr( const CheckedPtr & );
    CheckedPtr &operator =( const CheckedPtr & );
};



//Use of a smart pointer should be straightforward, mimicking the use of a built-in pointer:

CheckedPtr<Shape> s( new Circle );
s->draw(); // same as (s.operator ->())->draw()

Item43 auto_ptr非同寻常

资源句柄是c++中广为使用的技术,因此标准库提供了一个资源句柄模板auto_ptr。

auto_ptr有很多好处:

  1. 它非常高效。不可能使用内建指针实现一个性能更好的方案。
  2. 当auto_ptr离开作用域时,其析构函数会释放它所指向的资源。
  3. 类型转换方面,其行为酷似内建指针(基类指向子类)。

示例:

using std::auto_ptr; // see Namespaces [23, 81]
auto_ptr<Shape> aShape( new Circle );
aShape->draw(); // draw a circle
(*aShape).draw(); // draw it again

//auto_ptr赋值。一个auto_ptr可以复制给另一个
auto_ptr<Circle> aCircle( new Circle );
aShape = aCircle;

auto_ptr不同于普通智能指针之处在于其复制操作:对于一般的类而言,复制操作不会改变参与复制的源值。
在上例的赋值中,将aCircle赋值给aShape时,二者的值均受到影响:

  • 如果aShape是非空的,那么不管它指向的是什么东西,都将被delete掉取而代之以aCircle指向的东西;
  • 除此之外,aCircle也被设置为空。

对于auto_ptr而言,赋值和初始化并不是真正的复制操作。它们实际上是将对底层对象的控制权从一个auto_ptr转移到另一个auto_ptr。对于资源句柄的情形来说,这是一个很好的属性。

两种应避免auto_ptr的场合:

  1. 它们永远不应被用作容器元素。

容器中的元素通常在容器内部被拷来拷去,并且容器假定其元素遵从普通的非auto_ptr复制语义。除auto_ptr外的其他智能指针可以用做容器元素。

  1. auto_ptr不应指向一个数组,而应该指向单个元素。

原因在于auto_ptr指向的对象被删除时,它使用operator delete而非array delete来执行删除操作。

vector< auto_ptr<Shape> > shapes; // likely error, bad idea   
auto_ptr<int> ints( new int[32] ); // bad idea, no error (yet)

Item44 指针算术

指针算术很直观,为理解C++中指针算术的性质,最好将指针放在数组的环境中考虑:

const int MAX = 10;
short points[MAX];
short *curPoint = points+4;

如果对curPoint执行递增或递减操作,等于是请求它指向points数组中的下一个或上一个short元素。换句话说,指针算术总是依照所指向对象的大小进行的:即不是增减一个字节,而是增减sizeof(short)个字节,这也是void *不支持指针算术运算的原因,因为无法知道void *所指向的对象类型。

对于多维数组,需要特别注意:

const int ROWS = 2;
const int COLS = 3;
int table[ROWS][COLS]; // array of ROWS arrays of COLS ints
int (*ptable)[COLS] = table; // ptr to array of COLS ints

对ptable进行指针算术时,仍然按照ptable所指向对象的大小进行,即具有COLS个int元素的数组,其大小为:COLS * sizeof(int)

同一类型的指针可以进行减法运算,结果为两个指针之间的元素个数。如果前者大,结果为正;如果后者大,结果为负;若二者指向同一个元素或者均为空,结果为0。

两个指针相减的结果类型为标准typedef ptrdiff_t,它通常是int的一个别名。两个指针之间不可执行加法、乘法、除法,因为没有实际意义。

记住:指针并不是整数。

STL迭代器支持指针风格的算术操作,即利用了和内建指针相同的语法操作。STL迭代器不是内建指针,而是带有重载操作符的智能指针。list迭代器,根据节点之间的链接关系(可能不是连续内存区域)进行移动,而不是像操作内建指针那样。

Item45 模板术语

  • 模板参数(template parameter):用于模板声明
  • 模板实参(template argument):用于模板特化(specialization)
  • 模板名称(template name):简单的标识符
  • 模板id(template id):附带有模板实参列表的模板名称
template <typename T> // T is a template parameter
class Heap { ... };
//...
Heap<double> dHeap; // double is a template argument

//Heap为模板名称,Heap<double>为模板id

template<typename T>
void print(const T & foo);

**实例化(instantiation)特化(specialization)**的差别:

模板特化是指将模板实参提供给一个模板时得到的东西。特化可以显式进行,也可以隐式进行:

Heap<double>//显式特化
print(12.4) //隐式特化

模板特化可能会也可能不会导致模板发生实例化。另见Item46与Item47。

Item46 类模板显式特化

主模板(primary template):template <typename T> class Heap;

仅仅被声明为用于特化,但它通常也提供定义。

template <typename T>
class Heap {
  public:
    void push( const T &val );
    T pop();
    bool empty() const { return h_.empty(); }
  private:
    std::vector<T> h_;
};

template <typename T>
void Heap<T>::push( const T &val ) {
    h_.push_back(val);
    //标准库算法
    std::push_heap( h_.begin(), h_.end() );
}

template <typename T>
T Heap<T>::pop() {
    std::pop_heap( h_.begin(), h_.end() );
    T tmp( h_.back() );
    h_.pop_back();
    return tmp;
}

以上实现对于许多类型的值非常有效,但是在处理字符串(指向字符串的指针)时,会遇到问题。 因为默认情况下,标准堆算法使用<操作符来对堆中的元素进行比较和组织。 那么对于字符串指针,会按照字符串地址大小进行排序,而不是字符串本身的值。 即按照指针的值,而非指针指向的值进行组织。

为解决这个问题,可以提供一个针对指向字符串指针的显式特化版本:

//注意模板参数列表为空
template <>
class Heap<const char *> {

  public:
    void push( const char *pval );
    const char *pop();
    bool empty() const { return h_.empty(); }
  private:
    std::vector<const char *> h_;
};

这个类模板显式特化版本其实并不是一个模板,因为此时没有剩下任何未指定的模板参数了。出于这个原因,类模板显式特化通常被称为完全特化,以便与局部特化分开,后者是一个模板。

模板特化与实例化:

Heap<const char *>
Heap<int>

以上二者都是模板特化,但是前者不会导致Heap模板发生实例化(因为将使用专为const char*)定义的显示特化),后者将导致Heap的主模板发生实例化

特化的const char *的定制push操作,完全特化的模板类,其成员中template关键字和参数列表省略

bool strLess( const char *a, const char *b )
    { return strcmp( a, b ) < 0; }

void Heap<const char *>::push( const char *pval ) {
    h_.push_back(pval);
    std::push_heap( h_.begin(), h_.end(), strLess );
}

Item47 模板局部特化

函数模板不能进行局部特化,只能对他们进行重载,这里讨论的局部特化都是类模板。

局部特化像完全特化一样,首先需要一个通用的主模板进行特化。

给定主模板:

template <typename T> class Heap;

显式特化(俗称完全特化),用一套精确的实参来定制模板。

在上面的例子中,为char *提供了完全特化,对于其他指针类型的Heap排序而言,仍然需要进行完全特化,这种方式太麻烦,难以维护,这时候就需要局部特化。

template <typename T>
class Heap<T *> {
  public:
    void push( const T *val );
    T *pop();

    bool empty() const { return h_.empty(); }
  private:
    std::vector<T *> h_;
};

局部特化的语法类似于显式特化的语法,但它的模板参数列表是非空的。

和类模板的完全特化不同,局部特化是一个模板,在其成员的定义中,template关键字和模板列表是必不可少的。

template <typename T>
class Heap<T *> {
  public:
    void push( const T *val );
    T *pop();

    bool empty() const { return h_.empty(); }
  private:
    std::vector<T *> h_;
};

template <typename T>
struct PtrCmp : public std::binary_function<T *, T *, bool> {
    bool operator ()( const T *a, const T *b ) const
        { return *a < *b; }
};

//注意成员中的template关键字和模板列表
template <typename T>
void Heap<T *>::push( T *pval ) {
    if( pval ) {
        h_.push_back(pval);
        std::push_heap( h_.begin(), h_.end(), PtrCmp<T>() );
    }
}

注意实例化Heap时,几个不同版本的匹配顺序:

Heap<std::string> h1; // primary, T is std::string
Heap<std::string *> h2; // partial spec, T is std::string
Heap<int **> h3; // partial spec, T is int *
Heap<char *> h4; // complete spec for char *
Heap<char **> h5; // partial spec, T is char *
Heap<const int *> h6; // partial spec, T is const int
Heap<int (*)()> h7; // partial spec, T is int ()

关于在各种不同的局部特化之间进行选择的完整规则,是相当复杂的,但是多数情况下非常直观。通常而言,最具体的,限制性最强的候选者将被选择。局部特化机制非常精确,允许高精度地在候选者中进行选择。

Item48 类模板成员特化

考虑主模板:

template <typename T>
class Heap {
  public:
    void push( const T &val );
    T pop();
    bool empty() const { return h_.empty(); }
  private:
    std::vector<T> h_;
};

针对const char *的完全特化Heap替代了主模板的全部实现,其实对于字符指针类型的堆来说,主模板Heap的私有成员和empty成员函数已经足够用了,我们真正要做的全部事情就是特化push和pop成员函数:

template <>
void Heap<const char *>::push( const char * const &pval ) {
    h_.push_back(pval);
    std::push_heap( h_.begin(), h_.end(), strLess );
}

template<>
const char *Heap<const char *>::pop() {
    std::pop_heap( h_.begin(), h_.end(), strLess );
    const char *tmp = h_.back(); h_.pop_back();
    return tmp;
}

这些函数是对Heap主模板相应成员的显式特化。

Item49 利用typename消除歧义

模板类名字嵌套时,使用typename明确告诉编译器,某个嵌套的名字是一个类型名字。

template <typename Etype>
class SCollection {
public:
	//...
	typedef Etype ElemT;
	void insert(const Etype &);
	//...
};

//错误
//warning C4346: “Cont::ElemT”: 依赖名称不是类型;
//note: 用“typename”为前缀来表示类型
//error C2061: 语法错误: 标识符“ElemT”  
template <class Cont>
void fill(Cont &c, Cont::ElemT a[], int len) {
	for (int i = 0; i < len; ++i)
		c.insert(a[i]);
}

嵌套的名字Cont::ElemT并没有被识别为一个类型名字。问题在于,在fill模板的上下文中,编译器没有足够的信息来决定嵌套的名字ElemT是一个类型名字,还是一个非类型名字。按照C++标准约定,在这种情况下,嵌套的名字被假定为一个非类型的名字。

看一个不同的上下文:

class MyContainer {
  public:
    typedef State ElemT;
    //...
};
//...
MyContainer::ElemT *anElemPtr = 0;

模板类同样,一个实例化的类模板就是一个类,因此与MyContainer情形无差别:

template <typename T>
class PtrList {
  public:
    //...
    typedef T *ElemT;
    void insert( ElemT );
    //...
};

typedef PtrList<State> StateList;
//...
StateList::ElemT aState = 0;
PtrList<State>::ElemT anotherState = 0;

对于嵌套名字的访问,以上两种情形下,编译器只需检查类的内容便可确定ElemT是否为一个类型名字。

但是一旦进入模板的上下文,就不同了:

//看如下的模板函数
template <typename T>
void aFuncTemplate( T &arg ) {
    ...T::ElemT...

//case1
PtrList<State> states;
//...
aFuncTemplate( states ); // T::ElemT is PtrList<State>::ElemT 

//case2
struct X {
    enum Types { typeA, typeB, typeC } ElemT;
    //...
};
X anX;
//...
aFuncTemplate( anX ); // T::ElemT is X::ElemT 是数据成员,非类型

为了应对这种情形,有时必须明确地通知编译器,某个嵌套的名字是一个类型名字,使用typename关键字可以明确地告诉编译器,接下来的限定名字是一个类型名字,从而允许编译器正确地解析模板。

typename A::B::C::D::E
//以上是在告诉编译器,嵌套层次最深的那个名字E是一个类型名字。

当然,如果一个类型不能满足被解析模板的要求,就会得到编译错误:

struct Z {
    // no member named ElemT...
};
Z aZ;
//...
aFuncTemplate( aZ ); // error! no member Z::ElemT            
aFuncTemplate( anX ); // error! X::ElemT is not a type name  
aFuncTemplate( states ); // OK. nested ElemT is a type

至此,我们可以改写fill函数模板:

template <class Cont>
void fill( Cont &c, typename Cont::ElemT a[], int len ) { // OK
    for( int i = 0; i < len; ++i )
        c.insert( a[i] );
}

Item50 成员模板

一个成员模板就是一个自身是模板的成员:

template <typename T>
class SList {
  public:
    //...
    template <typename In> SList( In begin, In end );
    //...
};

不同于默认构造函数,这个SList构造函数是一个成员模板,显式地采用类型名字In进行参数化。

如果T和S是同样的类型,那么编译器将不会实例化成员模板,它将会字节编写一个复制操作。在这种情形下,通常最好明确地定义复制操作。

template <typename T>
class SList {
  public:
    //显式定义复制构造和复制赋值
    SList( const SList &that ); // copy ctor
    SList &operator =( const SList &rhs ); // copy assignment
    //成员模板的类似赋值构造和复制赋值
    template <typename S> 
    SList( const SList<S> &that );
    template <typename S>
        SList &operator =( const SList<S> &rhs );
    //...
};

应用:

float rds[] = { ... };
const int size = sizeof(rds)/sizeof(rds[0]);
std::vector<double> rds2( rds, rds+size );
SList<float> data( rds, rds+size ); // In is float *
SList<double> data2( rds2.begin(), rds2.end() ); // In is vector<double>::iterator

SList<double> data3( data ); // T is double, S is float
data = data3; // T is float, S is double

SList<float> data4( data ); // copy ctor
data3 = data2; // copy assignment
data3 = data4; // non-copy assignment from member template

Item51 采用template消除歧义

配置器是一种类类型,用于为STL容器定制内存管理操作,通常作为类模板去实现:

template <class T>
class AnAlloc {
  public:
    //...
    template <class Other>
    class rebind {
      public:
        typedef AnAlloc<Other> other;
    };
    //...
};

下面这个例子中,typedef A::rebind<Node>::other NodeAlloc 会报出语法错误。编译器除了知道A是类型名字之外,别的一无所知。按照C++标准,假定嵌套名字rebind是一个非模板名字,并将后面的尖括号解析为“小于”操作符。

#include <memory>
//https://en.cppreference.com/w/cpp/memory/allocator
template < typename T, class A = std::allocator<T> >
class SList {
	//...
	struct Node {
		//...
	};
	//typedef A::rebind<Node>::other NodeAlloc; // syntax error!
	typedef typename A::template rebind<Node>::other NodeAlloc;
	//...
};

使用关键字template,告诉编译器,rebind是一个模板名字,而使用typename则告诉编译器,整个这一堆东西表示的是一个类型名字。

Item52 针对类型信息的特化

类模板显式特化和局部特化通常用于生成主类模板的一些版本,这些版本根据具体的模板实参或模板实参的类定制而成。

然而,这些语言特性常常也以相反的方式使用,这种方式不是基于类型的属性生成特化版本,而是从一个特化版本中推导出类型的属性。确切地说,就是利用模板实例化机制,在编译期而非运行期执行一部分计算。

简单示例,判断是否为int:

//Item52 针对类型信息的特化
#include <iostream>

template <typename X>
struct IsInt { enum {result = false}; };

//利用此完全特化版本,在编译期询问一个未知类型是否确实为int
template<>
struct IsInt<int> { enum{result = true}; };

template <typename X>
void func(IsInt<X> & x) {
	bool ret = IsInt<X>::result;
	using std::cout;
	cout << ret << "\n";
}

int main() {
	IsInt<double> id;
	func(id);
	IsInt<int> ii;
	func(ii);
	return 0;
}

在编译期向类型询问此类问题的能力,是许多重要的优化机制和查错技术的基础。

判断类型是否为指针的示例:

//判断类型是否为指针
#include <deque>
struct Yes {}; // a type analog to true
struct No {}; // a type analog to false

template <typename T>
struct IsPtr // T is not a ptr...
{
	enum { result = false }; typedef No Result;
};
template <typename T>
struct IsPtr<T *> // unless it's an unqualified ptr,
{
	enum { result = true }; typedef Yes Result;
};


template <typename T>
class Stack {
public:
	~Stack()
	{
		//cleanup(typename IsPtr<T>::Result);//可不带()
		cleanup(typename IsPtr<T>::Result());
	}
	//...
private:
	void cleanup(Yes) {
		for (I i(s_.begin()); i != s_.end(); ++i)
			delete *i;
	}
	void cleanup(No)
	{}
	typedef std::deque<T> C;
	typedef typename C::iterator I;
	C s_;
};

其中两个不同版本的cleanup成员函数,一个带有一个类型为Yes的参数,另一个带有类型为No的参数。析构函数利用T去实例化IsPtr,访问其嵌套的类型名字Result,进而询问“T是否为指针类型”。

另外示例:

  • 判定类型是否为数组,什么类型的数组,边界值是多少
template <typename T>
struct IsArray { // T is not an array...
    enum { result = false };
    typedef No Result;
};
template <typename E, int b>
struct IsArray<E [b]> { // ...unless it's an array!
    enum { result = true };
    typedef Yes Result;
    enum { bound = b }; // array bound
    typedef E Etype; // array element type
};
  • 判定是否为指向数据成员的指针,类的类型是什么,成员类型是什么
template <typename T>
struct IsPCM { // T is not a pointer to data member
    enum { result = false };
    typedef No Result;
};
template <class C, typename T>
struct IsPCM<T C::*> { // ...unless it is!
    enum { result = true };
    typedef Yes Result;
    typedef C ClassType; // the class type
    typedef T MemberType; // the type of class member
};

Item53 嵌入的类型信息

通过将信息嵌入于类型自身,以向外部提供。

//Item53 嵌入的类型信息
#include <iostream>

template <class T>
class Seq {
public:
	typedef T Elem; // element type
	typedef T Temp; // temporary type
	size_t size() const;
	//...
};

//注意3处typename,告知嵌套的名字是类型名
template <class Container>
typename Container::Elem process(Container &c, int size) {
	typename Container::Temp temp = typename Container::Elem();
	for (int i = 0; i < size; ++i)
		temp += c[i];
	return temp;
}

这个process函数可以处理任意符合其规范的container,即含有嵌入的类型信息,如:

template <typename T>
class ReadonlySeq {
  public:
    typedef const T Elem;
    typedef T Temp;
    //...
};

但是当某container不符合其规范时,就不再适用:

class ForeignContainer {
    // no nested type information...
};

这时候就需要引入traits的概念了。

Item54 traits

本条款提到的traits class(特征类,特征萃取类),实际上是traits class template,这种类模板的成员用于描述模板实参的特征。除此外,往往还用于避免(其他)模板参数数目过多。

参考cppreference.com

traits类是一个关于某个类型的信息的集合,然而与嵌套的容器信息不同的是,traits类独立于它所描述的类型。

有关traits类的一个常见应用,是在泛型算法和不遵从算法期望的约定的类型之间,放一个遵从约定的中间层(适配器设计模式?)。

根据类型的traits编写算法,在一般情况下通常假设存在某种约定。本例中,ContainerTraits将对Seq和ReadonlySeq容器所使用的约定做出假设:

template <typename Cont>
struct ContainerTraits
{
	typedef typename Cont::Elem Elem;
	typedef typename Cont::Temp Temp;
	typedef typename Cont::Ptr  Ptr;
};

//采用traits改写process
template <typename Container>
typename ContainerTraits<Container>::Elem process(Container & c, int size)
{
	//typename Container::Temp temp = typename Container::Elem();
   	//这里Elem必须带有(),否则 error C2226: 语法错误: 意外的“ContainerTraits<const char *>::Elem”类型
	typename ContainerTraits<Container>::Temp temp = typename ContainerTraits<Container>::Elem();
	for (int i = 0; i < size; ++i)
		temp += c[i];
	return temp;
}

非兼容容器

相比Item53中,这里做了退一步的考虑,即不是直接从容器类型自身获取信息,而是从中间层traits类获取信息。这样对于特定的非兼容容器来说,可以对traits模板进行特化:

class ForeignContainer {
	// no nested type information...
};

//针对没有嵌入类型信息的ForeignContainer,创建完全特化版本的ContainerTraits
template <>
struct ContainerTraits<ForeignContainer> {
	typedef int  Elem;
	typedef Elem Temp;
	typedef Elem *Ptr;
};

ContainerTraits<ForeignContainer>::Elem y;//使用traits

对traits的索引,发生于编译期,且通过特化模板完成。

通过traits类来访问一个类型的信息的另一个优势在于,可以给那些不是类的类型(因而可以没有嵌套的信息)提供信息。尽管traits是类,但封装了traits的类型未必是类。

处理数组/指针

//针对const char *的特化,会出现temp无法赋值问题
//error C3892: “temp”: 不能给常量赋值 此时temp为const char
/*
template <>
struct ContainerTraits<const char *> {
	typedef const char Elem;
	typedef Elem Temp;
	typedef const char *Ptr;
};
*/

//针对其他类型数组,利用traits模板对指针的局部特化
template <typename T>
struct ContainerTraits<T *> {
	typedef T    Elem;
	typedef Elem Temp;
	typedef T *  Ptr;
};

//指向常量的指针,对应的局部特化版。当模板实参为指向常量的指针时,这个更具针对性的局部特化版本将会优先于上一个而得到选择
//解决上述针对const char *的完全特化版本,无法为temp赋值的问题,
template <typename T>
struct ContainerTraits<const T *> {
	typedef const T Elem;
	typedef T		Temp;
	typedef const T *Ptr;
};

#include <iostream>
#include <cstring>
int main() {

	ContainerTraits<ForeignContainer>::Elem y;//使用traits

	const char * str = "Hello World";
	//temp值初始化为0'\0',但是此处的process中对char类型的累加并无实际意义,char范围:-128~127
	process(str, strlen(str));
	return 0;
}

另一用途

局部特化还可以帮助我们将traits机制扩展至下面这种用途:将一个“外部”的约定转换为符合本地的约定。

STL对约定有很强的依赖。标准容器的概念类似于封装在上述ContainerTraits中的概念,但表现形式不同。

我们尝试将std::vector应用到process算法,参考:

#include <vector>
#include <string>
#include <iostream>

//针对std::vector的局部特化版
/*
//my simple implementation
template <typename T>
struct ContainerTraits<std::vector<T>> {
	//typedef xxx Elem;
	//typedef xxx Temp;
	//typedef xxx Ptr;

	typedef typename std::vector<T>::value_type Elem;
	typedef typename std::vector<T>::value_type Temp;
	typedef typename std::vector<T>::value_type* Ptr;
};
*/

/**/
//implementation in Item54
template <typename T>
struct ContainerTraits<std::vector<T>> {
	typedef typename std::vector<T>::value_type Elem;
	typedef typename
		std::iterator_traits<typename std::vector<T>::iterator>::value_type Temp;
	typedef typename
		std::iterator_traits<typename std::vector<T>::iterator>::pointer Ptr;
};

int main() {
	std::vector<int> vec{ 1,2,3 };
	using std::cout;
	cout << "process vec{ 1,2,3 } ret: " << process(vec, vec.size()) << "\n";

	std::vector<std::string> strings{"Hello",", ","World", "!"};
	cout << "process strings ret: " << process(strings, strings.size()) << "\n";
	return 0;
}

以上并不是我们能想到的最具可读性的实现,但是对用户来说,它是隐藏的,调用即可。

附:基于std::iterator_traits的示例

来源于cppreference.com iterator_traits中的示例:

//Shows a general-purpose std::reverse() implementation for bidirectional iterators.
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

template<class BidirIt>
void my_reverse(BidirIt first, BidirIt last)
{
	typename std::iterator_traits<BidirIt>::difference_type n = std::distance(first, last);
	for (--n; n > 0; n -= 2) {
		typename std::iterator_traits<BidirIt>::value_type tmp = *first;
		*first++ = *--last;
		*last = tmp;
	}
}

int main()
{
	std::vector<int> v{ 1, 2, 3, 4, 5 };
	my_reverse(v.begin(), v.end());
	for (int n : v) {
		std::cout << n << ' ';
	}
	std::cout << '\n';

	std::list<int> l{ 1, 2, 3, 4, 5 };
	my_reverse(l.begin(), l.end());
	for (int n : l) {
		std::cout << n << ' ';
	}
	std::cout << '\n';

	int a[] = { 1, 2, 3, 4, 5 };
	my_reverse(a, a + std::size(a));
	for (int n : a) {
		std::cout << n << ' ';
	}
	std::cout << '\n';

	//    std::istreambuf_iterator<char> i1(std::cin), i2;
	//    my_reverse(i1, i2); // compilation error
}

Item55 模板的模板参数

标准容器实际上至少有两个参数:一个表示元素类型,另一个表示配置器类型。容器使用配置器来分配和释放工作内存,从而使得这种行为可以按需定制。实际上,配置器为容器指定内存管理策略。

例如,实例化一个std::vector<int>时,得到的其实是一个std::vector<int, std::allocator<int>>

如同给函数声明中的形参命名一样,在模板声明中是否赋予模板参数一个名字也是可选的。

//省略模板参数名称
template<typename>
class List;
//等价于上面
template<typename T>
class List;

同样,如同函数定义一样,只有在模板定义中并且仅当参数名字被被模板定义所使用时,模板参数的名字才是必须的。不过,同函数声明中的形参一样,经常在模板声明中给模板参数起个名字,以有助于该模板的文档化(提升可读性)。

template <typename T, class Cont>
class Stack {
  public:
    ~Stack();
    void push( const T & );
    //...
  private:
    Cont s_;
};

Stack<int, List<int> > aStack1; // OK
//注意以下两者的问题
Stack<double, List<int> > aStack2; // 合法,但有问题,从double到int的复制会丢失精度       
Stack<std::string, Deque<char *> > aStack3; //错误! 不能将string复制给char *

可以为Cont提供一个默认实现,当用户乐于接受一个Deque实现或者不关心实现时,这很有帮助:

template <typename T, class Cont = Deque<T> >
class Stack {
    //...
};

Stack<int> aStack1; // container is Deque<int>
Stack<double> aStack2; // container is Deque<double>

提供默认实现的这种方式,是对方便性和灵活性的一个折中。其灵活性是以安全性为代价的。当使用其他容器(非Deque)进行特化时,仍然需要协调元素和容器的类型,例如:

Stack<int, List<int> > aStack3;
Stack<int, List<unsigned> > aStack4; // 哎呀!

让我们看看是否能在提高安全性的同时,尽可能地保持灵活性?

答案就是模板的模板参数

//可读性差
template <typename, template <typename> class>
class Stack;
//或者补全模板参数名称
template <typename T, template <typename ElemType> class Cont>
class Stack;

Stack模板采用其类型名字参数来实例化其模板的模板参数,所得到的容器类型用于实现Stack:

template <typename T, template <typename> class Cont>
class Stack {
    //...
  private:
    Cont<T> s_;
};

以上这种方式可以使元素和容器类型之间的协调问题通过Stack自身的实现来解决。

这种“单点特化”的方式大大降低了元素类型和用于容纳该种元素的容器类型之间不协调的可能性:

Stack<int,List> aStack1;
Stack<std::string,Deque> aStack2;

为了使之更加便利,可以为模板的模板参数提供一个默认值:

template <typename T, template <typename> class Cont = Deque>
class Stack {
    //...
};
//...
Stack<int> aStack1; // use default: Cont is Deque
Stack<std::string,List> aStack2; // Cont is List

易混淆点

//使用class而不是typename,目的是告诉读者,期望一个class或struct而不是任意类型,不过这对于编译器没有任何差别,在这个上下文中,class与typename是完全相同的东西
template <class Cont> class Wrapper1;

Wrapper1< List<int> > w1; // fine, List<int> is a type name
Wrapper1< std::list<int> > w2; // fine, list<int> is a type
Wrapper1<List> w3; // error! List is a template name

一种替代方式:

//需要一个模板名字用作模板实参,所需模板必须带有单个类型参数
template <template <typename> class Cont> class Wrapper2;

Wrapper2<List> w4; // fine, List is a template one type
Wrapper2< List<int> > w5; // error! List<int> isn't a template 
Wrapper2<std::list> w6; // error! std::list takes 2+ arguments

如果希望使用标准容器进行特化,需要这样做:

template <template <typename Element, class Allocator> class Cont>
class Wrapper3;
//或者其等价形式
template <template <typename,typename> class Cont>
class Wrapper3;

//以上二者声明是说,模板必须带有两个类型名字参数
Wrapper3<std::list> w7; // might work...
Wrapper3< std::list<int> > w8; // error! list<int> is a class 
Wrapper3<List> w9; // error! List takes one type argument

值得一提的是,标准容器模板(比如list)可能被声明为带有不止两个参数(这是合法的),因此w7可能并不能在所有平台上起作用。STL并非完美无暇。

Item56 policy

policy:策略。policy是一个类或一个类模板,其成员用于描述泛型组件的可配置的行为。它通常用作其他模板的实参。

回顾Item52中的stack模板,当栈的生命周期结束时,如果栈中的元素类型为指针,它可以delete栈中任何剩余的元素。这种策略并非不合理,只是不够灵活:

  • 可能不希望delete指针所指向的东西:指针所指向的对象不位于堆上、这些对象被其他容器所共享
  • 指针指向的是一个对象数组,而非单个对象

删除策略(deletion policy)将灵活处理:

  • 指针若指向单个对象,使用delete
  • 指针若指向对象数组,使用array delete

如下实现,stack采用模板的模板参数实现deletion policy,其实现可以多种多样:

template <typename T, template <typename> class DeletionPolicy>
class Stack {
  public:
    ~Stack() {
        for( I i( s_.begin() ); i != s_.end(); ++i )
            DeletionPolicy<T>::doDelete( *i ); //执行策略
    }
    //...
  private:
    typedef std::deque<T> C;
    typedef typename C::iterator I;
    C s_;
};

以上使用doDelete的静态成员函数进行删除,可以定义一个delete策略:

template <typename T>
struct PtrDeletePolicy {
    static void doDelete( T ptr )
        { delete ptr; }
};

或者采用函数对象的形式:

template <typename T>
struct PtrDeletePolicy {
    void operator()( T ptr )
        { delete ptr; }
};
//对应函数对象形式,需要调整删除方式
DeletionPolicy<T>()( *i );

其他形式的删除策略:

//数组形式的delete
template <typename T>
struct ArrayDeletePolicy {
    static void doDelete( T ptr )
        { delete [] ptr; }
};

//不执行delete
template <typename T>
struct NoDeletePolicy {
    static void doDelete( const T & ){}
};

这样一来,在实例化Stack时,即可指定不同的删除策略:

Stack<int, NoDeletePolicy> s1; // don't delete ints
Stack<std::string *, PtrDeletePolicy> s2; // delete string *'s
Stack<const char *, ArrayDeletePolicy> s3; // delete [] these
Stack<const char *, NoDeletePolicy> s4; // don't delete!
Stack<int, PtrDeletePolicy> s5; // error! can't delete int!

在模板设计中,通常需要为用户提供一些通过策略进行参数化的机会。如Item55中,让用户能够指定实现一个Stack的方法:

template <
	typename T,
	template <typename> class DeletionPolicy = NoDeletePolicy,
	template <typename> class Cont = Deque
>
class Stack;

这样带来灵活性的同时,也为默认情况下提供了一般行为:

Stack<double *, ArrayDeletePolicy, Vector> dailyReadings;
//默认情况
Stack<double> moreReadings; // no deletion, use a Deque

在泛型程序设计中,常常要做出关于实现和行为的策略决策。通常,这些决策可以作为策略进行抽象和表示。

Item57 模板实参推导

当调用函数模板时,“可以”显式地特化该模板,就像“必须”特化一个类模板那样:

template <typename R, typename E>
R cast( const E &expr ) {
    // ...do some clever checking...
    return R( expr ); // ...and cast.
}
//调用时,特化
int a = cast<int,double>(12.3);

然而,典型的做法(也更方便)是让编译器根据函数调用中的实参类型推导出模板实参,这个过程称为模板实参推导(template argument deduction)。

//编译器推导模板实参
int ii = cast<int>(12.3);//R是int,E是double

与函数重载一样,编译器在模板实参推导期间只检查函数实参的类型,对可能得返回类型不做检查。要想让编译器知道返回类型,除了明确告诉它,别无他法。

注意,如果编译器可以自行推导出它们,那么模板实参列表中靠后的模板实参可以省略。对于模板的可用性而言,模板参数的顺序很重要。上述例子中,如果表达式类型放置于返回类型之前,那么二者都必须显式指定。

模板实参推导是通过检查传递给调用中的实参类型来进行的,这意味着任何无法从实参类型推导出来的模板实参都必须显式地提供。

以上为编译器从单个函数实参中推导出单个模板实参,其实这种机制也能够从单个函数实参的类型推导出多个模板实参:

template <int bound, typename T>
void zeroOut( T (&ary)[bound] ) {
    for( int i = 0; i < bound; ++i )
        ary[i] = T();
}
//...
const int hrsinweek = 7*24;
float readings[hrsinweek];
zeroOut( readings ); // bound == 168, T is float

从函数指针来生成函数对象的类模板:

template <typename A1, typename A2, typename R>
class PFun2 : public std::binary_function<A1,A2,R> {
  public:
    explicit PFun2( R (*fp)(A1,A2) ) : fp_( fp ) {}
    R operator()( A1 a1, A2 a2 ) const
        { return fp_( a1, a2 ); }
  private:
    R (*fp_)(A1,A2);
};

bool isGreater( int, int );
std::sort(b, e, PFun2<int,int,bool>(isGreater)); // painful

template <typename R, typename A1, typename A2>
inline PFun2<A1,A2,R> makePFun( R (*pf)(A1,A2) )
    { return PFun2<A1,A2,R>(pf); }
//...
std::sort(b, e, makePFun(isGreater)); // much better...

Item58 重载函数模板

一个函数模板可以被其他函数模板和非模板函数重载。

函数模板和非函数模板之间的主要差别在于对实参隐式转换的支持度。

  • 非模板函数允许对实参进行广泛的隐式转换,从内建转换(如整型提升)到用户自定义转换(借助未标记为explicit的单参数构造函数和转换操作符)。
  • 对函数模板来说,由于编译器必须基于调用的实参类型来执行模板实参推导,因此只支持一些琐细的隐式转换,包括外层修饰(如T->const T或const T->T)、引用(如T->T&)以及从数组和函数退化的指针(如T[42]->T *)等情形。

这个差别导致的结果是:函数模板需要有比非模板函数更精确的匹配。

看下面例子:

template <typename T>
void g( T a, T b ) { ... } // this g is a template
void g( char a, char b ) { ... } // this g is not
//...
g( 12.3, 45.6 ); // template g 通过实例化模板g(T为double)可以得到更精确的匹配
g( 12.3, 45 ); // non-template g! 不会匹配模板g,编译器不会对第二个实参进行int到double的转换

对Item57中的makePFun重载:

template <typename A, typename R>
class PFun1 : public std::unary_function<A,R> {
  public:
    explicit PFun1( R (*fp)(A) ) : fp_( fp ) {}
    R operator()( A a ) const
        { return fp_( a ); }
  private:
    R (*fp_)(A);
};

//帮助函数
template <typename R, typename A>
inline PFun1<A,R> makePFun( R (*pf)(A) )
    { return PFun1<A,R>(pf); }

Item59 SFINAE

当试图使用函数模板实参推导机制在多个重载函数模板和非模板函数中进行选择时,编译器可能会尝试到一些失败的特化:

template <typename T> void f( T );
template <typename T> void f( T * );
//...
f( 1024 ); // instantiates first f

尽管在第二个函数模板f中拿一个非零整数来替换T*是错误的,然而只要发现了一个正确的替换,这个尝试过的替换就不会导致报错。

**SFINAE(substitution failure is not an error)——替换失败并非错误。**这是一个非常重要的属性,没有它就很难去重载函数模板,因为实参推导和重载机制搅在一起,将会导致很多对重载函数模板的使用变成非法的。

case1

Item52中,利用模板局部特化技术,来判断某种未知类型是否为某种类型的指针。这里使用SFINAE来获得类似效果:

typedef char True;
typedef struct { char buf[2]; } False;

template <typename T> True isPtr(T *);
False isPtr(...);

#define is_ptr(e) (sizeof(isPtr(e)) == sizeof(True))

int main() {
	using std::cout;
	int i = 0, *ip = &i, **ipp = &ip;
	cout << "is_ptr(i): "   << is_ptr(i)   << "\n";//0
	cout << "is_ptr(ip): "  << is_ptr(ip)  << "\n";//1
	cout << "is_ptr(ipp): " << is_ptr(ipp) << "\n";//1
	return 0;
}

在本例中,is_ptr可用来判定一个表达式类型是否为指针。这是通过结合使用函数模板实参推导和SFINAE达成的:

  • 如果e为指针,编译器将会匹配函数模板isPtr
  • 如果e不是指针,编译器将会匹配带有省略号形参的非模板函数isPtr

SFINAE可以确保以非指针类型来匹配模板isPtr的尝试不会导致编译期错误。

第二个奇妙的点在于is_ptr宏使用了sizeof。两个isPtr函数都没有提供定义,这是正确的,以为它们永远都没有被实际调用。sizeof中的函数调用,会导致编译器去执行实参推导和函数匹配工作,但它不会真的去调用该函数。sizeof只对“可能被调用”的函数的返回类型大小感兴趣。因而可以检查其返回类型的大小,以便确定匹配了哪一个函数。

不过,作为函数模板实参推导的一个组成部分,编译器将会忽略“一级”cv修饰符(即const和volatile),也会忽略引用修饰符。

case2

判断某类型是否为一个类:

template <typename T>
struct IsClass {
	template <class C>
	static True isClass(int C::*);

	template <typename K>
	static False isClass(...);

	enum {
		r = sizeof(IsClass<T>::isClass<T>(0)) == sizeof(True)
	};
};

struct Foo{};
int main() {
	using std::cout;
	cout << "IsClass<Foo>::r " << IsClass<Foo>::r << "\n";//1
	cout << "IsClass<int>::r " << IsClass<int>::r << "\n";//0
	return 0;
}

本例中将SFINAE机制封装在一个类模板IsClass内,并且重载两个函数模板作为IsClass的静态成员。

其中一个模板带有一个指向成员的指针的实参(Item15)。由于一个字面值0可被转换为一个指向类成员的指针,因此如果T是类类型,那么第一个isClass将得到匹配。

case3

摘自Vandevoorde 和 Josuttis的《C++ Templates》,判定某个特定的类类型中是否有一个名为“iterator”的嵌套类型:

template <class C>
True hasIterator(typename C::iterator const *);
template <typename T>
False hasIterator(...);

#define has_iterator(C) (sizeof(hasIterator<C>(0)) == sizeof(True))

//template <typename T>
struct Cont {
	//typedef T * iterator;
	typedef int iterator;
};
int main() {
	using std::cout;
	cout << "has_iterator(Cont) " << has_iterator(Cont) << "\n";//1
	cout << "has_iterator(char) " << has_iterator(char) << "\n";//0
	return 0;
}

case4

摘自Andrei Alexandrescu的《Modern C++ Design》

//T1是否能转换为T2
template <typename T1, typename T2>
struct CanConvert {
	static True canConvert(T2);
	static False canConvert(...);
	static T1 makeT1();//提供返回一个T1类型的快捷函数
	enum {
		r = sizeof(canConvert(makeT1())) == sizeof(True)
	};
	static const bool r2 = sizeof(canConvert(makeT1())) == sizeof(True);
};

int main() {
	bool r = CanConvert<int, double>::r;//1
	bool r2 = CanConvert<int, double>::r2;//1
	return 0;
}

参考enum与static member差异,摘自《C++ Templates: The Complete Guide, 2nd Edition

// primary template to compute 3 to the Nth
template<int N>
struct Pow3 {
  enum { value = 3 * Pow3<N-1>::value };
};

// full specialization to end the recursion
template<>
struct Pow3<0> {
  enum { value = 1 };
};

Item60 泛型算法

一个泛型算法就是一个以这种方式设计的函数模板:根据使用的上下文,很容易在编译期高效地进行定制。

函数模板 vs 泛型算法:

//函数模板
template <typename T>                                            
void slowSort( T a[], int len ) {                                
    for( int i = 0; i < len; ++i ) // For each pair              
        for( int j = i; j < len; ++j )                           
            if( a[j] < a[i] ) { // ...if out of order...         
                T tmp( a[j] ); // ...swap them.                  
                a[j] = a[i];                                     
                a[i] = tmp;                                      
            }                                                    
} 

//泛型算法
template <typename For, typename Comp>
void slowSort( For b, For e, Comp less ) {
    for( For i( b ); i != e; ++i ) // For each pair
        for( For j( i ); j != e; ++j )
            if( less( a[j], a[i] ) ) // ...if out of order...
               swap( a[j], a[i] ); // ...swap them.
}
template <typename For>
void slowSort( For b, For e ) {
    for( For i( b ); i != e; ++i ) // For each pair
        for( For j( i ); j != e; ++j )
            if( a[j] < a[i] ) // ...if out of order...
                swap( a[j], a[i] ); // ...swap them.
}

Item61 只实例化要用的东西

template <typename T, int n>
class Array {
public:
	Array() : a_(new T[n]) {}
	~Array() { delete[] a_; }
	Array(const Array &);
	Array &operator =(const Array &);
	void swap(Array &that) { std::swap(a_, that.a_); }
	T &operator [](int i) { return a_[i]; }
	const T &operator [](int i) const { return a_[i]; }
	bool operator ==(const Array &rhs) const;
	bool operator !=(const Array &rhs) const
	{
		return !(*this == rhs);
	}
private:
	T *a_;
};

template <typename T, int n>
bool Array<T, n>::operator ==(const Array &that) const {
	for (int i = 0; i < n; ++i)
		if (!(a_[i] == that.a_[i]))
			return false;
	return true;
}

struct Circle {
	//不定义==操作符
};

int main() {

	//OK
	Array<int, 12> a, b;
	//没问题
	if (a == b) {}// calls a.operator ==(b)

	//OK
	Array<Circle, 6> c, d;
	//编译失败 Circle未定义==
	//error C2676: 二进制“==”:“T”不定义该运算符或到预定义运算符可接收的类型的转换
	if (c == d) {}
	return 0;
}

在本例中,在调用 c == d 之前,一切OK。因为尚未直接或间接地对Array<Circle, 6>对象使用==操作,因此函数operator==就不会被实例化,也就不会发生错误。

Item62 Include Guards

考虑如下头文件hdr1.h

#ifndef HDR1_H
#define HDR1_H
// actual content of the header file...
#endif

在某些情况下,采用冗余的包含哨位可以相当显著地加快编译速度:

#ifndef HDR1_H
#include "hdr1.h"
#endif

不是简单地#include头文件,而是通过测试头文件里相同的哨位符号来守卫这个#include。这当然是冗余的,因为当头文件第一次被包含时,同一个条件将被测试两次。对于后续(非首次)的包含来说,可以防止头文件被不必要地打开和扫描。

这个操作并不常见,在某些大型程序来说,可以成倍加快编译速度。

Item63 可选的关键字

virtual

最常见的是在重写了基类虚成员函数的派生类成员函数中,可选地使用virtual关键字:

class Shape {
  public:
    virtual void draw() const = 0;
    virtual void rotate( double degrees ) = 0;
    virtual void invert() = 0;
    //...
};

class Blob : public Shape {
  public:
    virtual void draw() const;
    void rotate( double );
    void invert() = 0;
    //...
};

//再次重写
class SharpBlob : public Blob {
  public:
    void rotate( double ); // overrides Blob::rotate
    //...
};

成员函数Blob::draw重写了基类虚函数draw,因此它也是虚拟的。其中的virtual是可选的,使用与否对程序的含义没有任何影响。

常见误解:遗漏virtual会阻止下级派生类中进一步重写该函数,事实并非如此。

当重写Blob::invert这样的纯虚函数时,virtual关键字同样可选。

总之,在一个重写的派生类中,virtual关键字是否出现是可选的,没有任何影响。

在重写的派生类函数中无论使用与不使用viatual,要么都使用,要么都不使用。

operator new/delete/array new/array delete

当声明operator new/delete/array new/array delete成员时(参见Item36),static关键字是可选的,因为这些函数是隐式静态的:

class Handle {
  public:
    void *operator new( size_t ); // implicitly static
    static void operator delete( void * );
    static void *operator new[]( size_t );
    void operator delete[]( void * ); // implicitly static
};

同样,一致性原则,要么都使用,要么都不使用。

typename/class

在一个模板的头部,关键字typenameclass可以互换使用,表示一个模板参数是一个类型名字,在这种上下文中,二者含义并无任何差别。

通常使用typename表示该实参可以是任何类型,使用class表示必须是一个类类型。

register

在如今的编译器中,是否使用register关键字没有任何影响。

参考文献

Alexandrescu, Andrei. Modern C++ Design. Addison-Wesley, 2001.

Dewhurst, Stephen C. C++ Gotchas. Addison-Wesley, 2003.

Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns. Addison-Wesley, 1995.

Josuttis, Nicolai M. The C++ Standard Library. Addison-Wesley, 1999.

Meyers, Scott. Effective C++, Third Edition. Addison-Wesley, 2005.

Meyers, Scott. Effective STL. Addison-Wesley, 2001.

Meyers, Scott. More Effective C++. Addison-Wesley, 1996.

Sutter, Herb. Exceptional C++. Addison-Wesley, 2000.

Sutter, Herb. More Exceptional C++. Addison-Wesley, 2002.

Sutter, Herb. Exceptional C++Style. Addison-Wesley, 2005.

Sutter, Herb, and Andrei Alexandrescu. C++ Coding Standards. Addison-Wesley, 2005.

Vandevoorde, David, and Nicolai M. Josuttis. C++ Templates. Addison-Wesley, 2003.

Wilson, Matthew. Imperfect C++. Addison-Wesley, 2005.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值