C++ 模板与泛型编程

《C++ Primer 4th》读书笔记

所谓泛型编程就是以独立于任何特定类型的方式编写代码。泛型编程与面向对象编程一样,都依赖于某种形式的多态性。

面向对象编程中的多态性在运行时应用于存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。

在泛型编程中,我们所编写的类和函数能够多态地用于跨越编译时不相关的类型。一个类或一个函数可以用来操纵多种类型的对象。

面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。

 

模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。

 

函数模板

模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。

template <typename T>

int compare(const T &v1, const T &v2)

{

if (v1 < v2) return -1;

if (v2 < v1) return 1;

return 0;

}

 

模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。类型形参跟在关键字 class 或 typename 之后定义.在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用:

// ok: no distinction between typename and class in template parameter list

template <typename T, class U> calc (const T&, const U&);

 

模板形参表示可以在类或函数的定义中使用的类型或值。使用函数模板时,编译器会推断哪个(或哪些)模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。

实质上,编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该版本的函数。编译器承担了为我们使用的每种类型而编写函数的单调工作。

int main ()

{

// T is int;

// compiler instantiates int compare(const int&, const int&)

cout << compare(1, 0) << endl;

// T is string;

// compiler instantiates int compare(const string&, const string&)

string s1 = "hi", s2 = "world";

cout << compare(s1, s2) << endl;

return 0;

}

 

 函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前。

// ok: inline specifier follows template parameter list

template <typename T> inline T min(const T&, const T&);

// error: incorrect placement of inline specifier

inline template <typename T> T min(const T&, const T&);

 

类模板

类模板也是模板,因此必须以关键字 template 开头,后接模板形参表。Queue 模板接受一个名为 Type 的模板类型形参。

除了模板形参表外,类模板的定义看起来与任意其他类问相似。类模板可以定义数据成员、函数成员和类型成员,也可以使用访问标号控制对成员的访问,还可以定义构造函数和析构函数等等。在类和类成员的定义中,可以使用模板形参作为类型或值的占位符,在使用类时再提供那些类型或值。

template <class Type> class Queue {

public:

Queue (); // default constructor

Type &front (); // return element from head of Queue

const Type &front () const;

void push (const Type &); // add element to back of Queue

void pop(); // remove element from head of Queue

bool empty() const; // true if no elements in the Queue

private:

// ...

};

 

与调用函数模板形成对比,使用类模板时,必须为模板形参显式指定实参:

Queue<int> qi; // Queue that holds ints

Queue< vector<double> > qc; // Queue that holds vectors of doubles

Queue<string> qs; // Queue that holds strings

 

除了定义数据成员或函数成员之外,类还可以定义类型成员。如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型。必须显式地这样做,因为编译器(以及程序的读者)不能通过检查得知,由类型形参定义的名字何时是一个类型何时是一个值。如果希望编译器将 size_type 当作类型,则必须显式告诉编译器这样做:

template <class Parm, class U>

Parm fcn(Parm* array, U value)

{

typename Parm::size_type * p; // ok: declares p to be a pointer

}

通过在成员名前加上关键字 typename 作为前缀,可以告诉编译器将成员当作类型。

如果拿不准是否需要以 typename 指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定 typename 没有害处,因此,即使 typename 是不必要的,也没有关系。

 

非类型模板形参

模板形参不必都是类型。模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参(例如,像这里所做的一样)指定数组的长度。

// initialize elements of an array to zero

template <class T, size_t N> void array_init(T (&parm)[N])

{

for (size_t i = 0; i != N; ++i) {

parm[i] = 0;

}

}

int x[42];

double y[10];

array_init(x); // instantiates array_init(int(&)[42]

array_init(y); // instantiates array_init(double(&)[10]

 

类型等价性与非类型形参: 对模板的非类型形参而言,求值结果相同的表达式将认为是等价的。array_init 调用引用的是相同的实例—— array_init<int, 42>:

int x[42];

const int sz = 40;

int y[sz + 2];

array_init(x); // instantiates array_init(int(&)[42])

array_init(y); // equivalent instantiation

 

在函数模板内部完成的操作限制了可用于实例化该函数的类型。程序员的责任是,保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用哪些操作的环境中那些操作运行正常。

 

编写独立于类型的代码的一般原则:编写模板代码时,对实参类型的要求尽可能少是很有益的。说明了编写泛型代码的两个重要原则:

• 模板的形参是 const 引用。

• 函数体中的测试只用 < 比较。

通过将形参设为 const 引用,就可以允许使用不允许复制的类型。大多数类型(包括内置类型和我们已使用过的除 IO 类型之外的所有标准库的类型)都允许复制。但是,也有不允许复制的类类型。将形参设为 const 引用,保证这种类型可以用于 compare 函数,而且,如果有比较大的对象调用 compare,则这个设计还可以使函数运行得更快。

 

实例化

模板是一个蓝图,它本身不是类或函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程称为实例化。模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。

 

类模板的每次实例化都会产生一个独立的类类型。为 int 类型实例化的 Queue 与任意其他 Queue 类型没有关系,对其他Queue 类型成员也没有特殊的访问权。

 

从函数实参确定模板实参的类型和值的过程叫做模板实参推断。

 

类型形参的实参的受限转换

一般而论,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换:

• const 转换:接受 const 引用或 const 指针的函数可以分别用非 const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略 const,即,无论传递 const 或非 const 对象给接受非引用类型的函数,都使用相同的实例化。

• 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。

template <typename T> T fobj(T, T); // arguments are copied

template <typename T>

T fref(const T&, const T&); // reference arguments

string s1("a value");

const string s2("another value");

fobj(s1, s2); // ok: calls f(string, string), const is ignored

fref(s1, s2); // ok: non const object s1 converted to const reference int a[10], b[42];

fobj(a, b); // ok: calls f(int*, int*)

fref(a, b); // error: array types don't match; arguments aren't converted to pointers

 

模板实参推断与函数指针

可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。

template <typename T> int compare(const T&, const T&);

// pf1 points to the instantiation int compare (const int&, constint&)

int (*pf1) (const int&, const int&) = compare;

获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。如果不能从函数指针类型确定模板实参,就会出错。

// overloaded versions of func; each take a different function pointer type

void func(int(*) (const string&, const string&));

void func(int(*) (const int&, const int&));

func(compare); // error: which instantiation of compare?

 

在返回类型中使用类型形参

指定返回类型的一种方式是引入第三个模板形参,它必须由调用者显式指定:

// T1 cannot be deduced: it doesn't appear in the function parameterlist

template <class T1, class T2, class T3>

T1 sum(T2, T3);

// ok T1 explicitly specified; T2 and T3 inferred from argument types

long val3 = sum<long>(i, lng); // ok: calls long sum(int, long)

 

显式模板实参从左至右对应模板形参相匹配,第一个模板实参与第一个模板形参匹配,第二个实参与第二个形参匹配,以此类推。

// poor design: Users must explicitly specify all three template parameters

template <class T1, class T2, class T3>

T3 alternative_sum(T2, T1);

// error: can't infer initial template parameters

long val3 = alternative_sum<long>(i, lng);

// ok: All three parameters explicitly specified

long val2 = alternative_sum<long, int, long>(i, lng);

 

模板编译模型

编译器实例化特定类型的模板的时候,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码。标准 C++ 为编译模板代码定义了两种模型。

在包含编译模型中,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条 #include 指示使定义可用,该#include 引入了包含相关定义的源文件:

// header file utlities.h

#ifndef UTLITIES_H // header gaurd (Section 2.9.2, p. 69)

#define UTLITIES_H

template <class T> int compare(const T&, const T&);

// other declarations

#include "utilities.cc" // get the definitions for compare etc.

#endif

// implemenatation file utlities.cc

template <class T> int compare(const T &v1, const T &v2)

{

if (v1 < v2) return -1;

if (v2 < v1) return 1;

return 0;

}

// other definitions

 

在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用 export 关键字来做这件事。对类模板使用 export 更复杂一些。通常,类声明必须放在头文件中

// class template header goes in shared header file

template <class Type> class Queue { ... };

// Queue.ccimplementation file declares Queue as exported

export template <class Type> class Queue;

#include "Queue.h"

// Queue member definitions

导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字 export 不在类模板本身指定,而是只在被导出的特定成员定义上指定。

 

类模板的 static 成员

template <class T> class Foo {

public:

static std::size_t count() { return ctr; }

// other interface members

private:

static std::size_t ctr;

// other implementation members

};

每个实例化表示截然不同的类型,所以给定实例外星人所有对象都共享一个static 成员。因此,Foo<int> 类型的任意对象共享同一 static 成员 ctr,Foo<string> 类型的对象共享另一个不同的 ctr 成员。

 

通常,可以通过类类型的对象访问类模板的 static 成员,或者通过使用作用域操作符直接访问成员。当然,当试图通过类使用 static 成员的时候,必须引用实际的实例化:

Foo<int> fi, fi2; // instantiates Foo<int> class

size_t ct = Foo<int>::count(); // instantiates Foo<int>::count

ct = fi.count(); // ok: uses Foo<int>::count

ct = fi2.count(); // ok: uses Foo<int>::count

ct = Foo::count(); // error: which template instantiation?

与任意其他成员函数一样,static 成员函数只有在程序中使用时才进行实例化。

 

像使用任意其他 static 数据成员一样,必须在类外部出现数据成员的定义。在类模板含有 static 成员的情况下,成员定义必须指出它是类模板的成员:

template <class T> size_t Foo<T>::ctr = 0; // define and initialize ctr

 

一个泛型句柄类

/* generic handle class: Provides pointerlike behavior. Although access through

* an unbound Handle is checked and throws a runtime_error exception.

* The object to which the Handle points is deleted when the last Handle goes away.

* Users should allocate new objects of type T and bind them to a Handle.

* Once an object is bound to a Handle,, the user must not delete that object.

*/

template <class T> class Handle {

public:

// unbound handle

Handle(T *p = 0): ptr(p), use(new size_t(1)) { }

// overloaded operators to support pointer behavior

T& operator*();

T* operator->();

const T& operator*() const;

const T* operator->() const;

// copy control: normal pointer behavior, but last Handle deletes the object

Handle(const Handle& h): ptr(h.ptr), use(h.use)

{ ++*use; }

Handle& operator=(const Handle&);

~Handle() { rem_ref(); }

private:

T* ptr; // shared object

size_t *use; // count of how many Handle spointto *ptr

void rem_ref()

{ if (--*use == 0) { delete ptr; delete use; } }

};

 

template <class T>

inline Handle<T>& Handle<T>::operator=(const Handle &rhs)

{

++*rhs.use; // protect against self-assignment

rem_ref(); // decrement use count and delete pointers if

needed

ptr = rhs.ptr;

use = rhs.use;

return *this;

}

 

template <class T> inline T& Handle<T>::operator*()

{

if (ptr) return *ptr;

throw std::runtime_error("dereference of unbound Handle");

}

template <class T> inline T* Handle<T>::operator->()

{

if (ptr) return ptr;

throw std::runtime_error("access through unbound Handle");

}

 

template <class T> inline const T* Handle<T>::operator->() const

{

           if (ptr) return ptr;

else throw std::logic_error("unbound Sales_item");

 }

 

template <class T> inline const T& Handle<T>:: const

{

           if (ptr) return *ptr;

else throw std::logic_error("unbound Sales_item");

}

 

class Sales_item {

public:

// default constructor: unbound handle

Sales_item(): h() { }

// copy item and attach handle to the copy

Sales_item(const Item_base &item): h(item.clone()) { }

// no copy control members: synthesized versions work

// member access operators: forward their work to the Handle class

const Item_base& operator*() const { return *h; }

const Item_base* operator->() const

{ return h.operator->(); }

private:

Handle<Item_base> h; // use-counted handle

};

 

 

模板特化

模板特化(template specialization)是这样的一个定义,该定义中一个或多个模板形参的实际类型或实际值是指定的。特化的形式如下:

• 关键字 template 后面接一对空的尖括号(<>);

• 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;

• 函数形参表;

• 函数体。

template <typename T>

int compare(const T &v1, const T &v2)

{

if (v1 < v2) return -1;

if (v2 < v1) return 1;

return 0;

}

 

// special version of compare to handle C-style character strings

template <>

int compare<const char*>(const char* const &v1,

const char* const &v2)

{

return strcmp(v1, v2);

}

 

模板特化必须总是包含空模板形参说明符,即 template<>,而且,还必须包含函数形参表。如果可以从函数形参表推断模板实参,则不必显式指定模板实参:

// error: invalid specialization declarations

// missing template<>

int compare<const char*>(const char* const&,

const char* const&);

// error: function parameter list missing

template<> int compare<const char*>;

// ok: explicit template argument const char* deduced from parameter types

template<> int compare(const char* const&, const char* const&);

 

当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。

 

与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件。

 

普通作用域规则适用于特化

在能够声明或定义特化之前,它所特化的模板的声明必须在作用域中。类似地,在调用模板的这个版本之前,特化的声明必须在作用域中:

// define the general compare template

template <class T>

int compare(const T& t1, const T& t2) { /* ... */ }

int main() {

// uses the generic template definition

int i = compare("hello", "world");

// ...

}

// invalid program: explicit specialization after call

template<>

int compare<const char*>(const char* const& s1,

const char* const& s2)

{ /* ... */ }

 

这个程序有错误,因为在声明特化之前,进行了可以与特化相匹配的一个调用。当编译器看到一个函数调用时,它必须知道这个版本需要特化,否则,编译器将可能从模板定义实例化该函数。

 

重载与函数模板

函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。

 

如果重载函数中既有普通函数又有函数模板,确定函数调用的步骤如下:

1. 为这个函数名建立候选函数集合,包括:

a. 与被调用函数名字相同的任意普通函数。

b. 任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参。

2. 确定哪些普通函数是可行的(第 7.8.2 节)(如果有可行函数的话)。候选集合中的每个模板实例都 可行的,因为模板实参推断保证函数可以被调用。

3. 如果需要转换来进行调用,根据转换的种类排列可靠函数,记住,调用模板函数实例所允许的转换是有限的。

a. 如果只有一个函数可选,就调用这个函数。

b. 如果调用有二义性,从可行函数集合中去掉所有函数模板实例。

4. 重新排列去掉函数模板实例的可行函数。

• 如果只有一个函数可选,就调用这个函数。

• 否则,调用有二义性。

 

设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义

函数模板特化几乎总是比使用非模板版本更好。

你可能感兴趣的:(C++ 模板与泛型编程)