0%

C++(四) Effective-C++(下)

原图

模板与泛型编程

条款41:了解隐式接口和编译期多态

面向对象编程世界总是以显式接口( explicit interfaces)和运行期多态( runtime polymorphism)解决问题。Templates及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口( implicit interfaces)和编译期多态( compile-time polymorphism)移到前头了。

1
2
3
4
5
6
7
8
9
template<typename T>
void doprocessing(T& w)
{
if (w.size() > 10 && w != somenastywidget) {
T temp (w);
temp.normalize ();
temp.swap(w);
}
}
  • w必须支持哪一种接口,系由 template中执行于w身上的操作来决定。本例看来w的类型好像必须支持size, normalize和swap成员函数、copy构造函数(用以建立temp)、不等比较( inequality comparison,用来比较 somenasty-Widget)。我们很快会看到这并非完全正确,但对目前而言足够真实。重要的是,这一组表达式(对此 template而言必须有效编译)便是必须支持的一组隐式接口 (implicit interface)
  • 凡涉及w的任何函数调用,例如 operator>和 operator!=,有可能造成 template具现化( instantiated),使这些调用得以成功。这样的具现行为发生在编译期。“以不同的 template参数具现化 function templates”会导致调用不同的函数,这便是所谓的编译期多态( compile-time polymorphism)

请记住

  • classes 和 templates都支持接口( interfaces)和多态( polymorphism)对 classes 而言接口是显式的( explicit),以函数签名为中心。多态则是通过 virtual函数发生于运行期
  • 对 template参数而言,接口是隐式的( implicit),奠基于有效表达式。多态则是通过 template具现化和函数重载解析( function overloading resolution)发生于编译期

条款42:了解 typename的双重意义

  • 声明 template参数时,前缀关键字 class和 typename可互换
  • 请使用关键字 typename标识嵌套从属类型名称;但不得在 base class lists(基类列)或 member initialization list(成员初值列)内以它作为 base class 修饰符

条款43:学习处理模板化基类内的名称

假设我们需要撰写一个程序,它能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未经加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于 template的解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Class CompanyA
{
void sendcleartext(const std: string& msg);
void sendencrypted(const std: string& msg);
...
};
Class CompanyB
{
void sendcleartext(const std: string& msg);
void sendencrypted(const std: :string& msg);
...
};

... //针对其他公司设计的 classes

class Msginfo {...}; //这个 class用来保存信息,以备将来产生信息

template<typename Company>
class MsgSender {
public:
... //构造函数、析构函数等等。
void sendclear(const Msginfo& info){
std::string msg;
//在这儿,根据info产生信息;
Company c;
c.sendcleartext(msg);
}
void sendSecret(const Msginfoinfo) //类似 sendclear,唯一不同是
{...} //这里调用c.sendencrypted
};

这个做法行得通。但假设我们有时候想要在每次送出信息时志记(log)某些信息。 derived class可轻易加上这样的生产力,那似乎是个合情合理的解法:

1
2
3
4
5
6
7
8
9
10
11
template<typename Company>
class Loggingmsgsender: public MsgSender<Company>
{
public:
... //构造函数、析构函数等等
void sendclearmsg(const Msginfo& info){
//将“传送前”的信息写至log
sendclear(info); //调用 base class 函数;这段码无法通过编译。编译器会抱怨 sendclear不存在
//将“传送后”的信息写至log;
}
};

问题在于,当编译器遭遇 class template Loggingmsgsender定义式时,并不知道它继承什么样的 class当然它继承的是 MsgSender,但其中的 Company是个 template参数,不到后来(当 Loggingmsg Sender被具现化)无法确切知道它是什么。而如果不知道 Company是什么,就无法知道 class MsgSender看起来像什么一更明确地说是没办法知道它是否有个 sendclear函数。

有三个办法,第一是在 base class函数调用动作之前加上 "this->":

1
2
3
4
5
6
7
8
9
10
template<typename Company>
class Loggingmsgsender: public Msgsender<Company>
{
public:
void sendclearmsg(const Msginfo& info){
//将“传送前”的信息写至log
this->sendclear(info); //成立假设sendClean被继承
//将“传送后”的信息写至log;
}
};

第二是使用 using声明式。如果你已读过条款33,这个解法应该会令你感到熟悉。条款33描述 using声明式如何将“被掩盖的 base class 2名称”带入个 derived class作用域内。我们可以这样写下 sendclearmsg:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename Company>
class Loggingmsgsender: public Msgsender<Company>
{
public:
using Msgsender<Company>::sendclear;//告诉编译器,请它假设
... // sendclear位于 base class内。
void sendclearmsg(const Msginfo& info){
//将“传送前”的信息写至log
this->sendclear(info); //OK,假设 sendclear将被继承下来。
//将“传送后”的信息写至log;
}
};

第三个做法是,明白指出被调用的函数位于 base class内:

1
2
3
4
5
6
7
8
9
10
template<typename Company>
class Loggingmsgsender: public Msgsender<Company>
{
public:
void sendclearmsg(const Msginfo& info){
//将“传送前”的信息写至log
Msgsender<Company>::sendclear(info); //OK, sendclear
//将“传送后”的信息写至log;
}
};

但这往往是最不让人满意的一个解法,因为如果被调用的是 virtual 函数,上述的明确资格修饰( explicit qualification)会关闭“ virtual绑定行为”。

请记住

可在 derived class templates内通过" this->"指涉 base class templates内的成员名称,或藉由一个明白写出的“base class资格修佈符”完成。

条款44:将与参数无关的代码抽离 templates

  • Templates生成多个 classes和多个函数,所以任何 template代码都不该与某个造成膨胀的 template参数产生相依关系
  • 因非类型模板参数(non一type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或 class 成员变量替换 template参数
  • 因类型参数( type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述( binary representations)的具现类型( instantiation types)共享实现码

条款45:运用成员函数模板接受所有兼容类型

  • 请使用 member function templates(成员函数模板)生成“可接受所有兼容类型的函数
  • 如果你声明 member templates用于“泛化copy构造”或“泛化 assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符

条款46:需要类型转换时请为模板定义非成员函数

模板类中的模板函数不支持隐式类型转换,如果你在调用时传了一个其他类型的变量,编译器无法帮你做类型转换,从而报错。 解决方案是将该模板函数定义为模板类内的友元模板函数,从而支持了参数的隐式转换。

条款47:请使用traits classes表现类型信息

对于模板函数,可能对于接收参数的不同类型,有不同的实现。此时,可以提供一个traits class,其中包含了某一系列类型的类型信息(通常以枚举区分具体类型),然后,在该类中实现接收多种traits参数的重载工具函数,用来根据标识的不同类进行不同的具体函数操作。这使得该行为能在编译期就被区分。

条款48:认识模板元编程(TMP)

所谓 template metaprogram(模板元程序)是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从 templates具现出来的若干C++源码,便会一如往常地被编译。

1
2
3
4
5
6
7
8
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> {
enum { value = 1 };
}

有了这个 template metaprogram(其实只是个单一的 template metafuncti acterial),只要你指涉 Factoria: value就可以得到阶乘值。

循环发生在 template具现体 Factorial内部指涉另一个 template具现体Factorial之时。和所有良好递归一样,我们需要一个特殊情况造成递归结束。这里的特殊情况是 template特化体 Factorial<0>。

1
2
3
4
5
int main()
{
std: cout << Factorial<5>::value; //印出120
std: cout << Factorial<10>::value; //印出3628800
}

请记住

  • Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期因而得以实现早期错误侦测和更高的执行效率
  • TMP可被用来生成“基于政策选择组合”( based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码

定制 new 和 delete

条款49:了解 new-handler 的行为

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
  • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常

条款50:了解new和 delete的合理替換时机

让我们暂时回到根本原理。首先,怎么会有人想要替换编译器提供的 operatornew或 operator deletel呢?下面是三个最常见的理由:

  • 用来检测运用上的错误。如果将“new所得内存” delete掉却不幸失败,会导致内存泄漏( memory leaks)。如果在“new所得内存”身上多次 delete则会导致不确定行为。如果 operator new持有一串动态分配所得地址,而 operator delete将地址从中移走,倒是很容易检测出上述错误用法。此外各式各样的编程错误可能导致数据" over runs"(写入点在分配区块尾端之后)或" under runs"(写入点在分配区块起点之前)。如果我们自行定义个 operator news,便可超额分配内存,以额外空间(位于客户所得区块之前或后)放置特定的byte pattems(即签名, signatures)。 operator deletes便得以检查上述签名是否原封不动,若否就表示在分配区的某个生命时间点发生了 over run或 under run,这时候 operator delete可以志记(log)那个事实以及那个惹是生非的指针

  • 为了强化效能。编译器所带的 operator new和 operator delete主要用于一般目的。它们必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题( fragmentation),这最终会导致程序无法满足大区块内存要求即使彼时有总量足够但分散为许多小区块的自由内存

  • 为了收集使用上的统计数据。在一头栽进定制型news和定制型 deletes之前,理当先收集你的软件如何使用其动态内存。分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO(先进先出)次序或LIFO(后进先出)次序或随机次序来分配和归还?它们的运用型态是否随时间改变,也就是说你的软件在不同的执行阶段有不同的分配归还形态吗?任何时刻所使用的最大动态分配量(高水位)是多少?自行定义 operator new和 operator delete使我们得以轻松收集到这些信息

参考文献

《Effective C++》