Effective C++

Effective学习笔记

让自己习惯C++

条款01

C++高效编程时情况而变,取决于你使用C++的那一部分


条款02:尽量以const,enum,inline替换#define

Perfer consts enum and inlines to #define

总结:

✦For simple constants, prefer const objects or enums to #defines.

✦For function-like macros, prefer inline functions to #defines.


条款03:尽可能使用const

Use const whenever possible

const成员函数

优点

  1. 他们使class接口比较容易被理解,可以得知那个函数可以改动对象成员,那个不能

  2. 他们使用“操作const对象”成为可能。

constness

当两个成员函数如果只是常量性不同,就可以用const来重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TextBlock {
public:
...
const char& operator[ ](std::size_t position) const
// operator[ ] for{ return text[position]; }
// const objects
char& operator[ ](std::size_t position)
// operator[ ] for{ return text[position]; }
// non-const objects
private:
std::string text;
};
===============================================
//TextBlock’s operator[]s can be used like this:
TextBlock tb("Hello");
std::cout << tb[0]; // calls non-const TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0]; // calls const TextBlock::operator[]

在const和non-const成员函数中避免重复

总结

✦Declaring something const helps compilers detect usage errors. constcan be applied to objects at any scope, to function parameters andreturn types, and to member functions as a whole.

✦Compilers enforce bitwise constness, but you should program usinglogical constness.

✦When const and non-const member functions have essentially identi-cal implementations, code duplication can be avoided by having thenon-const version call the const version.


条款04:确定对象被使用前已经先被初始化

Make sure that object are initialized before they're used

初始化与赋值

规则

永远在使用对象之前将它初始化。对于内置内省必须要手工完成初始化

对于自定义类型来说,初始化职责放在构造函数上

赋值与初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PhoneNumber { ... };
class ABEntry {// ABEntry = “Address Book Entry”
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
    theName = name;// these are all assignments,
    theAddress = address;// not initializations
    thePhones = phones;
    numTimesConsulted = 0;
}

初始化

成员变量初始化动作发生在进入构造函数本体之前,相对于赋值来说发生时间更早。

初始化列表

1
2
3
4
5
6
7
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones):
theName(name),
theAddress(address),// these are now all initializations
thePhones(phones),
        numTimesConsulted(0)
{}// the ctor body is now empty

初始化优点

通常效率较高;

赋值版本:先调用构造函数为成员变量赋予初值,然后立刻在对他们赋予新值(传入的实参)。因此会浪费一部分时间

初始化版本:成员初始列中针对各个成员变量而设的实参,被拿去作为各成员之构造函数的实参。

必须初始化的情况

例如:成员变量为const或references,必须要进行成员初始列表

综述: 一般情况总是使用成员初始列表,可以针对普遍的变量,而且比赋值更高效

成员初始化次序

  • base class -> derived class

  • 成员变量以被声明的次序进行初始化因此对于示例ABEntry 中theName 最先被初始化,然后是theAddress,再是thePhone,最后是numTimesConsulted.

static对象

生命周期

从被构造出来直到程序结束,因此不是堆栈对象。

定义

一般这种对象被定义为global对象、定于于namspace作用域、classes、函数内、以及file作用内声明为static的对象

local static与non-local static对象

long static对象:定义域函数内,因为它对于函数而言是local

non-local static对象:其他剩余的static对象

多源文件跨编译单元编译

一般至少应该non-local static对象

问题

某编译单元内的某个non-local static对象初始化动作使用了另一个编译单元内的某个编译单元内的某个non-local static对象,它所用到的这个对象可能没有被初始化,因为c++对于 ”定义在不同编译单元的non-local对象那个“初始化次序没有明确的规定

non-local static 对象初始化次序问题

示例

1
2
3
4
5
6
7
class FileSystem {// from your library’s header file
public:
...
std::size_t numDisks( ) const;// one of many member functions
...
};
extern FileSystem tfs;// declare object for clients to use// (“tfs” = “the file system” ); definition// is in some .cpp file in your library
1
2
3
4
5
6
7
8
9
class Directory {// created by library client
public:
Directory( params );...};
Directory::Directory( params )
{
    ...
    std::size_t disks = tfs.numDisks( );// use the tfs object
    ...
}

假设:用户决定创建应该Directory对象,用来放置临时文件

Directory tempDir{params};

初始化次序的重要性

除非tfs在tempDir先被初始化,否则tempDir会用到尚未初始化的tfs。(但是tfs和tempDir是被不同人在不同时间创建的),他们是不同编译单元的non-local对象

解决方案

将每个non-local对象搬到总结专属的函数内(函数也要声明为static)、

这些函数返回应该reference指向它所含的对象。然后用户调用这些函数

C++保证

函数内的local static对象会在”该函数调用期间“”首次遇到该对象的定义式”时被初始化。

因此如果用”函数调用“(返回应该reference指向local static对象)替换”直接访问non-local static对象“,就获得了保证,保证你所获得reference将指向应该历经初始化的对象。

优点

从未调用non-local static对象的”仿真函数“,就绝不会引发构造和析构成本

优化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FileSystem { ... }; // as before
FileSystem& tfs( )// this replaces the tfs object; it could be
{
    // static in the FileSystem class
    static FileSystem fs;// define and initialize a local static object
    return fs;// return a reference to it
}
class Directory { ... };// as before
Directory::Directory( params )// as before, except references to tfs are
{// now to tfs( )
    ...
    std::size_t disks = tfs().numDisks( );
    ...
}
Directory& tempDir( )// this replaces the tempDir object; it
{
// could be static in the Directory class
    static Directory td( params );// define/initialize local static object

    return td;// return reference to it
}

总结:

✦Manually initialize objects of built-in type, because C++ only some-times initializes them itself.

✦In a constructor, prefer use of the member initialization list to assignment inside the body of the constructor. List data members in the initialization list in the same order they’re declared in the class.

✦Avoid initialization order problems across translation units by re-placing non-local static objects with local static objects.

构造/析构/赋值运算

条款05:了解C++默认编写和调用了哪些函数

Know what functions C++silently writes and calls.

总结:

✦Compilers may implicitly generate a class’s default constructor, copyconstructor, copy assignment operator, and destructor


条款06:若不想使用编译器自动生成的函数,就因该明确的拒绝

Explicitly disallow the use of compiler-generated functions you do not want.

总结:

✦To disallow functionality automatically provided by compilers, declare the corresponding member functions private and give no imple-mentations. Using a base class like private is one way to do this.


条款07:为多态基类声明virtual析构函数

1
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
#include <iostream>

struct B{
B(){std::cout<<"B constrctor"<<std::endl;}
~B(){std::cout<<"B destrutor"<<std::endl;}
};

struct D:public B{
D(){std::cout<<"D constrctor"<<std::endl;}
~D(){std::cout<<"D destrutor"<<std::endl;}
};

int main(){
{
D d;
}
std::cout<<std::endl;
//多态
{
B* pb = new B();
delete pb;
}
std::cout<<std::endl;
{
B* pd = new D();
delete pd;
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
B constrctor
D constrctor
D destrutor
B destrutor

B constrctor
B destrutor

B constrctor
D constrctor
B destrutor

总结:

✦Polymorphic base classes should declare virtual destructors. If aclass has any virtual functions, it should have a virtual destructor.

✦Classes not designed to be base classes or not designed to be usedpolymorphically should not declare virtual destructors.


条款08:别让异常逃离析构函数

prevent exception from leaving destruction

总结

✦Destructors should never emit exceptions. If functions called in adestructor may throw, the destructor should catch any exceptions,then swallow them or terminate the program.

✦If class clients need to be able to re act to exceptions thrown duringan operation, the class should provide a regular (i.e., non-destruc-tor) function that performs the operation.


条款11:在operator=中“自我赋值”

Handle assignment to self in operator=

以widget窗口类为示例

1
2
3
4
5
6
7
8
9
class Bitmap{...}
class Widget{
...
private
Bit* pb;
};

Widget w;
w = w; 自我赋值

方法一:证同测试

1
2
3
4
5
6
7
8
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs)                //证同测试
return *this;
delete pb;
pb = new Bitmap(*rhs.bp);    //防止浅拷贝问题
return *this
}

异常问题:

new Bitmap出现异常(分配内存错误导致错误)this->pb会指向一块被删除的内存

方法二:异常安全性

1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb; //创建*this一个副本
pb = new Bitmap(*rhs.pb); //pb指向*pb的一个副本
delete pOrig //删除副本
return *this;
}

证同测试效率问题

应用证同测试会导致代码效率变低,if分支导致执行速度下降

副本:

不是指向一个对象只是一个值容器,如函数传参

方法三:copy-swap(异常安全性)—-条款29

1
2
3
4
5
6
7
8
9
10
11
12
class Widget{
...
void swap(Widget& rhs)
{...}
...
};
Widget& Widget::operator=(const Widget& rhs)//一个副本修改它不会修改其本身
{
Widget temp(rhs); //rhs数据副本
swap(temp); //将*this的属于和副本的数据互换
return *this;
}

A variation on this theme takes advantage of the facts that

(1) a class’scopy assignment operator may be declared to take its argument byvalue and

(2) passing something by value makes a copy of it (seeItem20)

1
2
3
4
5
Widget& Widget::operator=( Widget rhs)//一个副本修改它不会修改其对象本身
{
swap(rhs); //pass by value 将*this的属于和副本的数据互换
return *this;
}

总结

✦Make sure operator= is well-behaved when an object is assigned to itself. Techniques include comparing addresses of source and target objects, careful statement ordering, and copy-and-swap.

✦Make sure that any function operating on more than one object be-haves correctly if two or more of the objects are the same.


资源管理

条款14:在资源管理类中小心copying行为

think carefully about copying behavior in resource-managing classes

1
2
3
4
5
6
7
8
9
10
class Lock 
{
public:
    explicit Lock(Mutex *pm): mutexPtr(pm)
        { lock(mutexPtr); }// acquire resource
    ~Lock( )
       { unlock(mutexPtr); }// release resource
private:
    Mutex *mutexPtr;
};

Clients use Lock in the conventional RAII fashion:

1
2
3
4
5
6
7
Mutex m;// define the mutex you need to use
...
{ // create block to define critical sectionLock
ml(&m); // lock the mutex
... // perform critical section operations
} // automatically unlock mutex at end
// of block

RAII对象copied的选择

  • 禁止复制(prohibit copying)

  • 对底层资源使用“引用计数法”

总结

✦ 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为

✦普遍而常见的RAII classes copying 行为时:一直copying、实行引用计数法。

条款15:在资源类中提供对原始资源的访问

provide access to raw resources in resource-managing classes

原因: 编译器无法将RAII类(shared_ptr)作为指针,会将其作为一个对象,无法发生隐式转换而报错

示例

1
2
3
4
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment *pi);// return number of days
// investment has been held
int days = daysHeld(pInv);// error!

解决方案:

convert an object of the RAII class (in this case,tr1::shared_ptr) into the raw resource

(1)explicit conversion:

smart_ptr offer a get number function to perform explicit conversion to return a raw pointer inside this smart_ptr

1
2
3
4
5
6
7
8
9
10
11
int days = daysHeld(pInv.get( ));// fine, passes the raw pointer
                                // in pInv to daysHeld

void test_string(const string* str){}

void test(){
//explicit
auto ptr = std::make_shared<string>("hello");
test_string(ptr);//error can`t convert RAII to string
test_string(ptr.get());//offer raw resource
}

(2)implicit conversion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Str{
public:
explicit Str(const string& str):m_str(str){}
operator string() const { return m_str;}//implicit convertion
string get(){return m_str;}
~Str(){}
private:
string m_str;
};

void test_string(const string& str){}

void test_implicit_(){
Str a("hello");
test_string(a);
}

总结

✦APIs often require access to raw resources, so each RAII classshould offer a way to get at the resource it manages.

✦Access may be via explicit conversion or implicit conversion. In gen-eral, explicit conversion is safer, but implicit conversion is more con-venient for clients.


设计与声明

条款20:宁以pass-by-reference-to-const 替换pass-by-value

1
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
class Person{
public:
Person()=default;
Person(const string& a):p_name(a){}
Person(const Person& other){}
virtual string printN()const{std::cout<<"P"<<st::endl;return p_name;}
virtual ~Person(){}
private:
string p_name;
string p_address;
};
class Student : public Person{
public:
Student()=default;
Student(const string& a):s_name(a){std::cout<<"S construct\n";}
//Student(const Student& other){std::cout<<"S copy construct\n";}
virtual string printN()const{std::cout<<"S"<<std::endl;return s_name;}
virtual ~Student(){std::cout << "S destruct\n";}
private:
string s_name;
string s_address;
};
void test1(const Person& p)
{
std::cout<<p.printN()<<std::endl;
}

用pass-by-reference-to-const的好处

  1. 高效,不会创建临时对象,因此不会再次调用construt和deconstruct。

  2. 准确且正确的多态,pass-by-value无法识别准确的object,如test1中,real object 都是person无法调用student的多态,pass-by-reference-to-const是可以准确调用student对象

总结

✦Prefer pass-by-reference-to-const over pass-by-value. It’s typicallymore efficient and it avoids the slicing problem.

✦The rule doesn’t apply to built-in types and STL iterator and func-tion object types. For them, pass-by-value is usually appropriat


条款21:必须返回对象时,别妄想返回其reference

Don't try return a reference when you must return an object

1
2
3
4
5
6
7
8
9
10
11
class Rational{
public:
Rational ()= default;
Rational(double x, double y):x_(x),y_(y)
{cout<<"construt"<<endl; }
.........
friend const Rational operator*(const Rational &lhs,
const Rational& rhs)
private:
double x_, y_;
};

该段代码描述有理数的相乘的类的设计

当以reference作为返回值时的几大误区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
==================on the stack===================
const Rational& operator*(const Rational &lhs,
const Rational& rhs)
{
Rational result(lhs.x_*lhs.y_,rhs.x_*rhs.y_);
return result;
}

=================on the heap======================
const Rational& operator*(const Rational &lhs,
const Rational& rhs)
{
Rational *result = new Rational(lhs.x_*lhs.y_,rhs.x_*rhs.y_);
return *result;
}
=================static local====================
const Rational& operator*(const Rational &lhs,
const Rational& rhs)
{
static Rational result
result = Result(lhs.x_*lhs.y_,rhs.x_*rhs.y_);
return result;
}
  1. on the stack :reference作为返回值时,result对象是一个stack值,当其返回时离开了作用域,result对象会被销毁,reference返回时会导致其指向了一个被销毁的值

  2. on the heap :heap分配的内存,new出来的对象,无法得知合适对该对象进行delete,因此会导致内存泄漏

  3. static local :会引起多线程问题,例如数据共享问题

正确做法:

1
2
3
4
5
6
inline const Rational operator*(const Rational &lhs,
const Rational& rhs)
{
Rational result(lhs.x_*lhs.y_,rhs.x_*rhs.y_);
return result;
}

说明:虽然会付出一点点的成本代价(construct和deconstruct),但从长远的角度和综合对比可知,此方法是最合适的做法

总结:

✦Never return a pointer or reference to a local stack object, a refer-ence to a heap-allocated object, or a pointer or reference to a localstatic object if there is a chance that more than one such object willbe needed. (Item4 provides an example of a design where returninga reference to a local static is reasonable, at least in single-threadedenvironments.)


条款22:将成员变量声明为private

Declare data member private

private(提供封装)和其他(提供封装)

对于protect声明的成员变量,当其出现问题后或是被取消后,将会影响整个派生类

总结:

✦Declare data members private. It gives clients syntactically uniformaccess to data, affords fine-grained access control, allows invariantsto be enforced, and offers class authors implementation flexibility.

✦protected is no more encapsulated than public(protect 与 public 差不多)

条款23:宁以non-member、non-friend替换member函数

Perfer non-member、non-friend to member function

问题描述

示例 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WebBrower
{
...
void clearCache();
void clearHistory();
void removeCookies();
...
};

==================member================
class WebBrower{
...
void clearEverything();//调用clearCache、clearHistory、
//removeCookies
}
=================non-member============
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies()
}

我们更愿意使用non-member代替member函数

原因

1.non-member的封装性比member函数的更强

2.non-member对类的相关机能有较大的包裹弹性

封装:它使我们能够改变事物而只影响有限客户

愈多东西被封装,愈少人能看见它,因此能够有愈大的弹性去改变它

愈多函数可以访问封装的函数,数据的封装性越低

条款22:成员变量应该是private,以为如果他们不是,就有无数的函数可以去访问他们,没有封装可言

访问限制

能够访问private成员变量的函数只有class和member函数与friend函数可以访问

程序设计问题

1
2
3
4
namespace WebBrowserStuff{
class WebBrowser{...};
void clearBrowser(WebBrowser& wb);
}

设计特点

  • namespace 与classes不同,namespace可以支持跨多个源码文件而class不能

  • non-member、non-friend函数为用户提供较好的程序机能以及封装性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//头文件webbrowsers。
namespace WebBrowserStuff{
//核心机能,提供所有的non-member函数
...
}
//webbrowserbookmarks。
namespace WebBrowserStuff
{
... //声明与书签相关的non-member、non-friend便利函数
}
//webbrowsercookies
namespace WebBrowerStuff
{
...//声明与cookies相关的non-member、non-friend便利函数
}

上述案例运用了namespace的跨越多个源码文件的特点

标准库组织方式

在STL头文件中std命名空间内的每一个东西,每个头文件声明std的某些机能

例如只想用vector,那就只用vector头文件,这允许客户支队他们所用的小部分系统形成编译依赖,但该方法不能适用于member函数,以为class必须整体定义,不可分割

将所有便利函数放在多个头文件内但隶属同一命名空间,意味用户可以轻松的扩展。需要只做到则是,在该命名空间添加non-member、non-friend函数

总结:

✦Prefer non-member non-friend functions to member functions. Do-ing so increases encapsulation, packaging flexibility, and functionalextensibility.


条款24: 若所有参数皆需类型转换,请为此采用non-member函数

Declare non-member functions when type conversions should apply to all parameters

示例:Rational

运用条款3,20,21书写正确的operator*

1
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
31
32
33
34
35
#include <iostream>

class Rational{
public:
Rational(float numerator = 0,// ctor is deliberately not explicit;
float denominator = 1):n(numerator),d(denominator){};// allows implicit int-to-Rational
// conversions
int numerator( ) const;// accessors for numerator and
int denominator( ) const;// denominator — see Item22private:
friend std::ostream& operator<<(std::ostream& out,const Rational& rhs)
{
out<< rhs.n/rhs.d;
return out;
}
const Rational operator*(const Rational& rhs)const
{
Rational result((n*rhs.n),(d*rhs.d));
return result;
}
private:
float n;
float d;
};

int main(){
Rational r1(1,2);
Rational r2(3,2);

Rational r3 = r1 * r2;//right

r3 = r1 * 2;//right
r3 = 2 * r1;//error

std::cout<<r3<<std::endl;
}

注意事项

  • 在隐式转换时,构造函数需要写出默认参数,不然不能进行隐式转换,因为Rational类构造函数有两个参数;

  • 在构造函数加上explicit就可以防止隐式转换

  • 2 * r1 不能成功运行,是不是不满足交换律?

重写上述例子

1
2
3
4
result = r1.operator*(2);
result = 2.opeator*(r1);//错误
//等同于
result = opeator*(2,r1);

因为r1内含一个operator函数的class的对象,并且将2进行隐式转换r1.operator (Rational(2))

对于2.operator(r1)显然时错误的,没有这个重载函数

将operator写为non-member函数

1
2
3
4
5
const Rational operator*(const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.numerator()*rhs.numerator(),
rhs.denominator()*rhs.denominator());
}

优点

  • non-member函数如条款23所述封装性更强

  • 支持所有参数的隐式类型转换

总结

✦If you need type conversions on all parameters to a function (includ-ing the one that would otherwise be pointed to by the this pointer),the function must be a non-member.


条款25:考虑写出一个不抛出异常的swap函数

consider support a no-throwing swap

前置知识

无法交换值、无法交换指针

普通swap函数实现细节

1
2
3
4
5
6
7
8
9
10
void swap(int* a,int* b){
int* temp = a;
a = b;
b = temp;
}
int *p1 = new int(1);
int *p2 = new int(2);
swap(p1,p2);
========================
地址不变,值不变

陷阱

对于c/c++的初学者来说,犯下一个很大的陷阱,那就是我们只传过去一个地址对于者个地址值只是一个副本信息,并不是原对象,导致无法交换地址值更无法交换指针值。我们只是通过传参过来的一个 (地址)值(副本),这是我们只是对原对象的指针副本做了交换

无法交换地址、可以交换指针

1
2
3
4
5
6
7
8
void swap(int* a,int* b){
int* temp = *a;
*a = *b;
*b = *temp;
}
int *p1 = new int(1);
int *p2 = new int(2);
swap(p1,p2);

解析

解引用后,此时a、b是a、b地址对应内存的值,也就是原对象值,改操作进行的是对(原对象)内存的进行置换,但此时并不会改变地址值,地址值仍是一个副本

可以交换值和地址

1
2
3
4
5
6
7
8
void swap(int** a,int** b){
int* temp = *a;
*a = *b;
*b = temp;
}
int *p1 = new int(1);
int *p2 = new int(2);
swap(p1,p2);

解析

p1和p2是一个地址

我们用双层指针,写出一个swap的置换函数,我们传入地址进去,此时a、b的值是指向p1和p2的地址。对a、b进行解引用,则对应的a 、b是对应p1、p2的地址原对象

进行置换则交换的是p1和p2的值,那么地址交换,相应改变的是p1和p2的对象名所对应的地址而已,而实际的内存并没有置换而已。

引用方法

1
2
3
4
5
6
7
std::swap;
void swap( T& a, T& b)// swaps a’s and b’s values
{
T temp(a);
a = b;
b = temp;
}

这种方法既可以改值也可以改地址,因为引用的本身就是绑定一个原对象,并不是副本

标准库swap

std::swap

1
2
3
4
5
6
7
8
9
10
namespace std {
template<typename T>// typical implementation of
std::swap;
void swap( T& a, T& b)// swaps a’s and b’s values
{
    T temp(a);
    a = b;
    b = temp;
}
}

只要T支持copying(通过copy构造函数和assignment操作符完成),默认的swap会实现代码就会将类型为T的对象进行置换

示例:swap

实践Widget类

  • 用pimpl手法将Widget的数据成员封装到WidgetImpl中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class WidgetImpl {// class for Widget data;
public:// details are unimportant...
private:
    int a, b, c;// possibly lots of data —
    std::vector<double> v;// expensive to copy!...
};

class Widget {// class using the pimpl idiom
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs)// to copy a Widget, copy its
    {        // WidgetImpl object. For
        ...// details on implementing
        *pImpl = *(rhs.pImpl);// operator= in general,
        ...// see Items 10, 11, and 12.
    }
    ...
private:
    WidgetImpl *pImpl;// ptr to object with this
};// Widget’s data

设计问题

  • 置换两个Widget对象过于复杂,浪费空间和效率(对于置换Widget对象值,我们只需要做的是置换impl指针,但默认的swap要交换Widget类更需要交换WidgetImpl)

  • 可以直接交换指针的地址,改变指针指向的内存

置换其impl指针

1
2
3
4
5
6
7
8
namespace std{
template<>
void swap<Widget>(Widget& a,Widget& b)//std::swap的全特化版本只能对
//<Widget>表示这一特例化版本只是针对指针交换而设计
{
swap(a.pImpl,b.pImpl);
}
}

问题

pImpl是属于Widget的private成员因此此函数肯定是无法编译通过的,我们可以将其声明为friend函数但其封装性较弱,可以将swap声明为member函数如下:

声明public成员函数置换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Widget {
// same as above, except for the
public:// addition of the swap mem func
...
void swap( Widget& other){
using std::swap;// the need for this declaration
// is explained later in this Item
swap(pImpl, other.pImpl);// to swap Widgets, swap their
}
// pImpl pointers
...
};
namespace std {
template<>// revised specialization of
void swap<Widget>(Widget& a,// std::swap
                        Widget& b)
{
a.swap(b);// to swap Widgets, call their
}// swap member function
}

优点

能够通过编译,并且具有STL容器的一致性,以为std::swap也提供了有pulic swap成员函数的和std::swap的特化版本

缺点

对于Widget class templates而非classes 将数据类型加以参数化

1
2
3
4
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
1
2
3
4
5
6
7
8
namespace std {
template<typename T>
void swap<Widget<T> >(Widget<T>& a,// error! illegal code!
        Widget<T>& b)
{
    a.swap(b);
}
}

错误分析

企图偏特化一个function template(std::swap),但C++只能对class templates偏特化,在function templates身上时不能偏特化的。因此无法编译。

偏特化function template

1
2
3
4
5
6
namespace std {template<typename T>// an overloading of std::swap 
void swap(Widget<T>& a,// (note the lack of “<...>” after
Widget<T>& b)// “swap”), but see below for
{
a.swap(b);
}// why this isn’t valid code}

问题

重载function templates是没问题的,但std是一个特殊的命名空间:

  • 可以全特化std内的templates

  • 不可以添加新的templates(class或function)到std里面

标准做法

高效正确的做法:non-member的swap、member的swap函数相结合

1
2
3
4
5
6
7
8
9
10
11
12
namespace WidgetStuff {
...// templatized WidgetImpl, etc.
template<typename T>// as before, including the swapclass
Widget { ... };// member function
...
template<typename T>// non-member swap function;
void swap(Widget<T>& a,// not part of the std namespace
                    Widget<T>& b)
{
        a.swap(b);
}
}

swap实现效率不足的解决(class或template运用了pimpl手法)

  1. 提供一个public的swap函数,让它高效地置换你的类型的两个对象值,而其不能抛出异常

  2. 在你的class或template所在的命名空间提供一个non-member swap,并令他调用上述swap成员函数

  3. 如果编写一个class(而非class template),为你的class特化一个std::swap,并令他调用你的swap的成员函数

  4. 必须使用using std::swap,以便其能够在函数类曝光可见,然后报价namspace修饰符

总结

✦Provide a swap member function when std::swap would be inefficientfor your type. Make sure your swap doesn’t throw exceptions.

✦If you offer a member swap, also offer a non-member swap that callsthe member. For classes (not templates), specialize std::swap, too.

✦When calling swap, employ a using declaration for std::swap, then callswap without namespace qualification.

✦It’s fine to totally specialize std templates for user-defined types, butnever try to add something completely new to std.

实现

条款26:尽量将变量的声明置后

Postpone variable definitions as long as possible

变量声明前置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// this function defines the variable "encrypted" too soon
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length( ) < MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
... // do whatever is necessary to place an
// encrypted version of password in encrypted
return encrypted;
}

问题

使用:正常执行的时候需要用到encrypted

未使用:当异常抛出时,encrypted对象的构造成本已经造成,而析构要离开作用域后才会启用。

变量置后

1
2
3
4
5
6
7
8
9
10
11
// this function postpones encrypted’s definition until it’s truly necessary
std::string encryptPassword(const std::string& password){
using namespace std;
if (password.length( ) < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
string encrypted;
... // do whatever is necessary to place an
// encrypted version of password in encrypted
return encrypted;
}

循环中的变量声明

1
2
3
4
5
6
7
8
9
10
11
12
13
// Approach A: define outside loop
Widget w;
for (int i = 0; i < n; ++i)
{
w = some value dependent on i;
...
}

// Approach B: define inside loop
for (int i = 0; i < n; ++i)
{
Widget w(some value dependent on i);...
}

两种方法的成本对比

■Approach A: 1 constructor + 1 destructor + n assignments.

■Approach B: n constructors + n destructors.

效率分析

■Approach A:可见性强,当n较大时效率更好

■Approach B:n小时效率高

(1) assignment相比constructor-destruction(对)更加便宜

(2) 如果您正在处理代码中对性能敏感的部分,则应默认使用方法B。

总结:

✦Postpone variable definitions as long as possible. It increases pro-gram clarity and improves program efficiency.


条款27:尽量少做转型操作

Minimize casting

C语言转型语法

1
2
3
4
//C风格的转型
(T) expression// cast expression to be of type T
//函数风格的转型
T(expression)// cast expression to be of type T

C++新式转型(new-style)

1
2
3
4
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
  • const_cast通常被用来将对象的常量性去除

  • dynamic_cast 主要用来执行“向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。

  • reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,不可以移植。例如将pointer to int 转型为一个int。

  • static_cast用来强制隐式转换,例如将non-const对象转为const对象,或将int转为double等等。它可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derivered。但它无法将cosnt转为non-const——这个只有const_cast才能办到。

新式转换的优点

  • 在代码中容易被识别出来(grep工具和人工识别),找出类型系统在那个地点被破坏

  • 各转型动作的目标越窄化,编译器越可能诊断错误地点,例如将constness去除,除非使用新式转型中的const_cast否之无法通过编译

旧式类型转换的使用场景:

1
2
3
4
5
6
7
8
9
10
11
class Widget 
{
public:
    explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15));// create Widget from int
                        // with function-style
castdoSomeWork(static_cast<Widget>(15));// create Widget from int
                                        // with C++-style cast

在function-style看起来更像类型转换。可进行显式构造函数中类型转换。

RTII(Run Time Type Identification)

含义

令编译器编译出运行期间执行的代码

1
2
3
int x,y;
...
double d = static_cast<double>(x)/y;

将int 转型为double会产生一些代码,因为int与double的底层描述不相同

1
2
3
4
class Base{...};
class Drive:public Base{...};
Derived d;
Base* pb = &d;// implicitly convert Derived*⇒ Base*

上述两个指针的值并不相同(&d,pb),这个时候会又一个偏移量运行期施加在Derived*指针上取得正确的指针值。

转型容易写出似是而非的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Window {// base class
public:
virtual void onResize( )
{
...
}// base onResize impl
...
};
class SpecialWindow: public Window {
// derived class
public:
virtual void onResize( ) {
// derived onResize impl;
static_cast<Window>(*this).onResize();// cast *this to Window,
// then call its onResize;
// this doesn’t work!
...// do SpecialWindow-
}// specific stuff
    ...
};

问题所在

static_cast(this).onResize(),调用的onResize并不是当前对象上的函数,而是稍早转型动作所建立的一个 “this对象的base class成分” 的暂时副本身上的onResize。

(函数就是函数,它只是一个成员仅此一份,关键在于成员函数都含有个隐藏的this指针,因此会影响成员操作数据)

换句话来说就是会丢失在当前对象base-class中对数据的操作

it does not invoke that function on the current object! Instead, the cast creates a new, temporary copy of the base class part of *this, then invokes onResize on the copy!

实际示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
public:
A():size(0){};
virtual int get_size(){
++size;
return size;
}
int size;
};

class B:public A{
public:
B():A(){};

virtual int get_size(){
static_cast<A>(*this).get_size(); //error
size++;
return size;
}
};

如上述例子,我们的操作仅仅只是在*this指针强转得到一个副本上调用了函数,因此在当前对象上并没有调用base-class的成员函数,所有A::get_size中的size++不会在当前对象上作用,所以得到的结果为1.

解决办法:去除类型转换

1
2
3
4
5
6
7
8
class SpecialWindow: public Window {
public:
virtual void onResize( ) {
    Window::onResize( );// call Window::onResize
    ...// on *this
}
    ...
};

dynamic__cast

使用场景

只有指向base-class的一个pointer或reference时,想要去操作认定为derived-class对象身上执行derived-class操作函数,依靠该dynamic__cast转型方法实现

缺陷

dynamic_cast的执行速度相当的慢,而且向下转型本就是一个不安全的行为,因此有两个办法用来取代dynamic__cast

做法一

使用容器并在请汇总存储直接指向derived-class对象的指针(通常为只能指针),消除了”通过base-class接口处理对象函数“的需要。

示例

假设先前的window/specialwindow继承体系只有specialwindows才支持闪烁效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Window { ... };
class SpecialWindow: public Window {
public:
void blink( );
...
};
typedef// see Item13 for info
std::vector<std::tr1::shared_ptr<Window> > VPW;
// on tr1::shared_ptr
VPW winPtrs;
...
for ( VPW::iterator iter = winPtrs.begin();// undesirable code:
iter != winPtrs.end();// uses dynamic_cast++iter)
{
if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>
(iter->get()))
psw->blink();
}

修改

1
2
3
4
5
6
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin();// better code: uses
iter != winPtrs.end();// no dynamic_cast++iter)
(*iter)->blink();

缺陷在于无法在同一容器内存储指向window的任何派生类。处理多窗口需要多个容器,他们都具备类型安全性

做法二

通过base-class接口处理所有window的所有派生类,就是在base-class内提供virtual函数做任何想多window派生类做的时

示例

虽然specialwindow可以闪烁,但或许将闪烁函数声明在base-class中并提供一份空的默认函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Window {
public:
virtual void blink() { }// default impl is no-op;
... // see Item34 for why
};// a default impl may be
// a bad idea
class SpecialWindow: public Window {
public:virtual void blink() { ... }// in this class, blink
    ...
// does something
};
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;// container holds// (ptrs to) all possible
...
// Window types
for ( VPW::iterator iter = winPtrs.begin( );iter != winPtrs.end( );
++iter)// note lack of
(*iter)->blink( );// dynamic_cast

无论是那种做法——”类安全容器“还是”virtual函数往继承体系上方移动“,都只是一个可行方案,需要靠自己判断

总结:

✦Avoid casts whenever practical, especially dynamic_casts in perfor-mance-sensitive code. If a design requires casting, try to develop acast-free alternative.

✦When casting is necessary, try to hide it inside a function. Clientscan then call the function instead of putting casts in their own code.

✦Prefer C++-style casts to old-style casts. They are easier to see, andthey are more specific about what they do.


条款28:避免返回handles(reference,pointer,iterators)指向对象内部成分

Avoid returning “handles” to object internals

示例

假设您正在处理一个涉及矩形的应用程序,每个矩形可以由其左上角和右下角表示。要使矩形对象保持较小,可以决定定义其范围的点不应存储在矩形本身中,而应存储在矩形指向的辅助结构中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Point { // class for representing points
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData {// Point data for a Rectangl
ePoint ulhc;// ulhc = “ upper left-hand corner”
Point lrhc;// lrhc = “ lower right-hand corner”
};
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
private:
std::tr1::shared_ptr<RectData> pData;// see Item13 for info on
};// tr1::shared_ptr

对于Rectangle类我们添加两个const-reference member function,为何使用const-reference在条款20中有说明。但因此会导致以下问题。

破坏封装性

1
2
3
4
5
6
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);// rec is a const rectangle from
                                    // (0, 0) to (100, 100)
rec.upperLeft().setX(50);// now rec goes from
// (50, 0) to (100, 100)!

由上述代码可以得知,我们不仅仅能对矩形的点进行读,并且能够进行修改,那么我们定义的数据成员与public就没有什么两样。(虽然我们在upperLeft()函数添加了const定义,但我们只是不能对指向Rectdata的智能指针进行修改,可以对该对象内部的值进行修改)

修改

1
2
3
4
5
6
7
8
9
class Rectangle {
public:
    ...
const Point& upperLeft( ) const {
    return pData->ulhc; }
const Point& lowerRight( ) const {
    return pData->lrhc; }
    ...
};

经过将返回值加上const我们可以让const成员限定符不在是个fake,我们只能对数据进行读写。

但是这种方式仍然会引起下述问题

dangling handles 所指对象不存在

1
2
3
4
5
6
7
8
9
10
11
class GUIObject { ... };
const Rectangle // returns a rectangle by
boundingBox(const GUIObject& obj);// value; see Item3 for why
// return type is const
=====================================================
//Now consider how a client might use this function:
GUIObject *pgo;// make pgo point to
... // some GUIObject
const Point *pUpperLeft =            // get a ptr to the upper
&(boundingBox(*pgo).upperLeft( ));// left point of its
// bounding box

问题分析

上述问题中会调用boundingBox(*pgo)函数对象,它所返回的值是一个临时的Rectangle副本(temp),当我们用upperLeft去作用与temp身上,返回一个reference指向temp的一个内部成分

错误

当我们结束这段语句是会产生一个问题,就是我们的boundBox的返回值(temp),将会被析构,也就是这个temp对象的内部成员都不复存在,那么我们的pUpperLeft就指向了一个不复存在的值

例外:operator[]

在vector和string容器中有个成员函数operator[]可以选择个别的元素,这个函数就是返回reference指向“容器内的数据”,但那些数据会随着容器的销毁而销毁。这仅仅只是有个例外。

总结

✦Avoid returning handles (references, pointers, or iterators) to object internals. Not returning handles increases encapsulation, helpsconst member functions act const, and minimizes the creation of dangling handles.


条款29:为”异常安全“而努力是值得的

Strive for exception-safe code

异常问题

以class用来表现夹带背景图案的GUI菜单,运用于多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class PrettyMenu{
public:
...
void changebackground(std::istream& imgSrc);
...
private:
Mutex mutex;
Image* bgImage;
int imagechages;
};


void changebackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++ imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}

”异常安全“的条件

  1. 不泄漏任何资源:new Image(imgSrc)导致异常,对unlock的调用就绝不会执行。

  2. 不允许数据败坏:new Iamge(imgSrc)抛出异常,bgImage就是指向一个被删除的对象,imageChanges以及被累加,而其实并没有新的图像被成功安装起来

RAII解决方案

1
2
3
4
5
6
7
void prettMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
delete bgImage;
++imageChages++;
bgImage = new Image(imgSrc);
}

不在需要调用unlock

”异常安全函数“保证

  • 基本承诺:异常抛出时,不会使对象或数据结构会因此而破坏,就数据保持异常抛出钱的状态

  • 强烈保证:异常抛出使,程序状态不会改变。(函数失败恢复到”调用函数之前“的状态)

  • 不抛掷保证:它们总能完成总能的原先承诺的功能

异常安全函数解决问题

智能指针解决问题:

1.引用智能指针类管理内存

2.将计数器的次序交换

1
2
3
4
5
6
7
8
9
10
11
12
class PrettMenu{
...
std::shared_ptr<Image> bgImage;
...
};
void PrettMenu::changBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc));//以”new Image“;
//设定bgImage内部指针
++imageChagnes;
}

优点

不需手动delete旧图像,而且删除操作是在对象被成功创建的之后,因此new成功后才会成功调用reset函数,Image(imgSrc)的临时对象也会在reset中释放掉(delete)

问题

Image构造函数会抛出异常(输出流的读取记号已经被移走)

Copy and Swap

原则介绍

为你打造修改的对象(原件)做出一个副本,然后再那副本身上做一切的修改。若修改发生错误,源对象仍能保存原始状态。修改成功,则原件和副本做置换操作。

修改对象数据副本,一个不会抛出异常的函数(swap)中将修改后的数据和原件置换

”隶属对象数据“ pimpi idiom

从原对象放进一个另一个对象内,然后赋予原对象一个指针,指向那个实现对象(副本)、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct PMImpl{//pImpl是一个private成员具有封装性
std::shared_ptr<Image> bgImage;
int imageChage;
};
class PrettMenu{
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
};

void PrettMenu::changeBackground(std::istream pImpl)
{
using std::swap;
Lock ml(&mutex);
//设计copy副本(值对象),保存原始数据
std::shared_ptr<PMImpl> ptemp(new PMImpl(*pImpl));
//修改副本
ptemp->bgImage.reset(new Image(imgSrc);

ptemp->imageChanges++;
//原始对象与副本交换
swap(pImpl,pNew);//置换数据,释放mutex·
}

总结:

✦Exception-safe functions leak no resources and allow no data struc-tures to become corrupted, even when exceptions are thrown. Suchfunctions offer the basic, strong, or nothrow guarantees.

✦The strong guarantee can often be implemented via copy-and-swap,but the strong guarantee is not practical for all functions.

✦A function can usually offer a guarantee no stronger than the weak-est guarantee of the functions it calls.


条款30:透彻了解inlining的里里外外

Understand the ins and outs of inlining

inline的细节

  1. inline函数代码量不易过大会导致程序体积过大,导致代码膨胀以至于额外的换页行为
  2. inline只是对编译器的申请并不是强制命令,class内的函数被隐喻的称为inline
  3. inline通常被置于头文件,在编译过程中进行inlining,而为将一个“函数调用”替换为“被调用函数的本体”

inline的声明

隐式声明为inline的函数

  • member函数
  • friend函数

不应被声明为inline的函数

  • 构造以及析构函数

总结

✦Limit most inlining to small, frequently called functions. This facili-tates debugging and binary upgradability, minimizes potential codebloat, and maximizes the chances of greater program speed.

✦Don’t declare function templates inline just because they appear inheader files.


条款31:将文件间的编译依存关系降至最低

Minimize compilation dependdencies between files

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
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;// implementation detail
Date theBirthDate;// implementation detail
Address theAddress;// implementation detail
};

要想让Person class编译需要加入以下头文件的类或函数声明式

include < string >

include “date.h”

include “address.h”

但这样会导致这些文件中形成一种编译的依存关系,所依赖的头文件发生改变都会让Person class的头文件进行重新文件

将class的实现细目至于class的定义式中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace std {
class string;// forward declaration (an incorrect
}
// one — see below)
class Date;// forward declaration
class Address;// forward declaration
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;
...
};

问题

  1. string前置声明错误,正确的也复杂

  2. 前置声明每一个东西困难的是,编译器必须知道对象的大小

1
2
3
4
5
int main(){
int x;
Person p(params);
...
}

编译器清楚的知道int需要多大,而Person需要询问class的定义式。

针对于Person类可以用以下方法:将Person分割为两个classes,一个只提供接口,一个只负责实现该接口。将负责实现的Implementation class取名为PersonImpl,Person将定义如下

pimpl idiom(pointer to implementation)

pimpl 惯例是一种新式 C++ 技术,用于隐藏实现、最小化耦合和分离接口。 Pimpl 对于”指向实现的指针”是短的。你可能已熟悉概念,但通过其他名称(如 Che一 cat 或编译器防火墙惯例)了解它。

下面是 pimpl 惯例如何改进软件开发生命周期:

  • 最大程度地减少编译依赖项。

  • 接口和实现分离。

  • 可移植性。

优点

有较好的封装性以及减少客户端的文件依赖性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 使用Pimpl
// 在头文件person.hpp中
#include <memory>
class Person {
public:
Person();
private:
// Person类的实现细节放置在该前向声明的实现类中。
struct Impl;
// 指向实现类Impl的私有指针
std::unique_ptr<Impl> pimpl_;
};

// 在源文件person.cpp中
#include "person.hpp"
#include "basic_info.hpp"
#include <string>
#include <memory>
struct Person::Impl {
std::string name;
std::string id;
BasicInfo basic_info;
};
Person::Person() : pimpl_(std::make_unique<Impl>()) {}

1.减少了需要包含的头文件;2.当内部实现发生变化时,客户端的代码不需要重新编译。例如:客户端在gcc编译中只需要连接上其动态连接库或者静态库文件,这时候服务端已经将所需的文件的编译完了,可以减少客户端编译的时间

由此修改以上Person代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <string>// standard library components
// shouldn’t be forward-declared
#include <memory> // for tr1::shared_ptr; see belowclass PersonImpl;
// forward decl of Person impl.
class PersonImpl;
class Date;// forward decls of classes used in
class Address;// Person interface
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:// ptr to implementation;
std::tr1::shared_ptr<PersonImpl> pImpl;// see Item13 for info on
};// std::tr1::shared_ptr

上述代码main class中内涵一个

这样的设计下,Person的客户就完全与Dataes,Address以及Persons的实现的细目分离。因此哪些classes的任何实现修改都不需要Person客户断重新编译。“接口与实现分离”

关键

这个分离在于以“声明的依存性”替换为“定义的依存性”,编译最小化的本质:现实中让头文件尽可能的自我满足,万一做不到,则让他与其他文件内的声明式(并非定义式)相依

设计策略

■Avoid using objects when object references and pointers will do

■Depend on class declarations instead of class definitions whenever you can.

声明函数而它用到某个class式,你并不需要改class的定义:纵使函数以by value方式传递改类型的参数:

1
2
3
class Date;// class declaration
Date today( );// fine — no definition
void clearAppointments(Date d);// of Date is needed

声明这两个函数的无需Date的定义式,但是当有人调用哪些函数式,调用之前需要让Date的定义式曝光

如果将”提供class定义式“(通过#include完成)的义务冲”函数声明所在“之头文件转移到”内涵函数调用“之客户文件,便可将”并非真正必要的类型定义“与客户端之间的编译依存去除掉

■Provide separate header files for declarations and definitions

需要两个头文件,一个用于声明式,一个用于定义式。文件必须保持一致性,如果声明式被改变,两个文件都需要改变。#include一个声明文件而非前置声明若干函数

1
2
3
4
#include "datefwd.h"// header file declaring (but not
                    // defining)
class DateDate today( );// as before
void clearAppointments(Date d);

C++中提供关键字export,允许将template声明式和template定义式分割与不同的文件内,但式这个关键字在有些编译器里不支持

Handle classses

像Person这样使用pimpl idiom的classes,被称为Handle classes。

方法一

将他们的所有函数转交给一个相应的实验类并由后者完成实际工作。例如卖弄Person的两个成员函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Person.h"
#include "PersonImpl.h"// we must also #include PersonImpl’s class
// definition, otherwise we couldn’t call
// its member functions; note that
// PersonImpl has exactly the same public
// member functions as Person — their
// interfaces are identical
Person::Person(const std::string& name, const Date& birthday,
const Address& addr):
pImpl(new
PersonImpl(name, birthday, addr)){}
std::string Person::name( ) const{
return pImpl->name( );
}

Person构造函数以new调用PersonImpl构造函数,以及Person::name函数内调PersonImpl::name,让Person百年城一个Handle class但不会改变他做的事,只会改变它做事的方法

Interface classes

令Person称为一个特殊的抽象基类,称为interface class。这汇总class的目的事猫叔derived的接口,因此他通常不带有成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtal函数。

1
2
3
4
5
6
7
8
class Person {
public:
virtual ~Person( );
    virtual std::string name( ) const = 0;
    virtual std::string birthDate( ) const = 0;
    virtual std::string address( ) const = 0;
    ...
};

class的用户必须以Person的pointer和reference来写应用程序,因为他不可能针对”内含pure virtual函数“的person classes具体出实体。

interface class的客户必须有办法为这种class创建新的对象。

如下

1
2
3
4
5
6
7
8
9
10
class Person {
public:
    ...
    static std::tr1::shared_ptr<Person>
                    // return a tr1::shared_ptr to a new
    create(const std::string& name,// Person initialized with the
            const Date& birthday,// given params; see Item18 for
            const Address& addr);// why a tr1::shared_ptr is returned
...
};

客户将会这样使用这些接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::string name;
Date dateOfBirth;Address address;
...// create an object supporting the Person interface
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth,
address));
...
std::cout << pp->name( )
// use the object via the
<< " was born on "
// Person interface
    << pp->birthDate( )
    << " and now lives at "
    << pp->address( );
... // the object is automatically
// deleted when pp goes out of
// scope

支持interface class接口的那个concrete class 必须被定义出来,而其真正的构造函数必须被调用。一切都在virtual构造函数实现所在的文件内放生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class RealPerson: public Person 
{
public:
RealPerson(const std::string& name, const Date& birthday,
const Address& addr): theName(name),
theBirthDate(birthday),
theAddress(addr){}
virtual ~RealPerson( ) { }
std::string name( ) const;// implementations of these
std::string birthDate( ) const;// functions are not shown, but
std::string address( ) const;// they are easy to imagineprivate:
std::string theName;Date theBirthDate;Address theAddress;};
======================================================
//Given RealPerson, it is truly trivial to write Person::create:
std::tr1::shared_ptr<Person> Person::
                        create(const std::string& name,
                                    const Date& birthday,
                                        const Address& addr)
{
return std::tr1::shared_ptr<Person>(new
RealPerson( name, birthday,addr));
}

一个更现实的Person::create实现代码会创建不同类型的derived class对象。取决于额外参数值、读自文件或数据库的数据、环境变量。

RealPerson示范实现了Interface class 的两个最常见的机制之一:从Interface class继承接口规格,然后实现出接口所覆盖的函数。第二点则是多重继承

总结:

✦The general idea behind minimizing compilation dependencies is todepend on declarations instead of definitions. Two approachesbased on this idea are Handle classes and Interface classes.

✦Library header files should exist in full and declaration-only forms.This applies regardless of whether templates are involved.


继承与面向对象设计

条款32:确定你的public继承塑造出的is-a关系

Make sure public inheritance models "is-a"

通过public继承出的关系为“is-a”关系,如下所示

1
2
3
4
5
6
7
8
9
10
class Person { ... };
class Student: public Person { ... };
void eat(const Person& p);// anyone can eat
void study(const Student& s);// only students study
Person p;// p is a PersonStudent s;// s is a Student
eat(p);// fine, p is a Person
eat(s);// fine, s is a Student,
        // and a Student is-a Person
study(s);// fine
study(p);// error! p isn’t a Student

如上述关系可以表述出学生是人,但人这个抽象类却不一定是人

is-a的误区

1
2
3
4
5
6
7
8
9
10
class Bird {
public:
    virtual void fly( );
        // birds can fly
    ...
};
class Penguin: public Bird {
    // penguins are birds
    ...
};

错误

这个继承体系中说明企鹅是鸟的派生类,那么它应该含有鸟类的所有行为,但是企鹅却不会飞,这点显得不是特别的严谨。我们应该让is-a有较佳的真实性

方法一:双class继承体系

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
class Bird {
    ...
    // no fly function is declared
};
class FlyingBird: public Bird {
public:
    virtual void fly( );
    ...
};
class Penguin: public Bird {
    ...
    // no fly function is declared
};

方法二:运行期错误

1
2
3
4
5
6
7
void error(const std::string& msg);// defined elsewhere
class Penguin: public Bird {
public:virtual void fly( )
{
    error("Attempt to make a penguin fly!"); }
    ...
};

此处声明出企鹅是不会飞的,那么说企鹅会飞则是一种错误的认知,在运行期的时候会被检测出来

总结:

✦Public inheritance means “is-a.” Everything that applies to baseclasses must also apply to derived classes, because every derivedclass object is a base class object.


条款33:避免遮掩继承而来的名称

Avoid hiding inherited names

命名查找规则(作用域)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
private:
int x;
public:
virtual void mf1( ) = 0;
virtual void mf2( );
void mf3( );
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf4(){mf2();};
...
};

Base的作用域大于Derived的作用域,根据命名查找法,当我们在Derived类中查找mf2时,选择方向Derived->Base->global。小一级的作用域会将其覆盖。

名称可视性(name visibility)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
private:
int x;
public:
virtual void mf1( ) = 0;
virtual void mf1(int);
virtual void mf2( );
virtual void mf3( );
virtual void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1( );
void mf3( );
void mf4( );
...
};

Base内名为mf1和mf3的重载函数都被Derived内的mf1和mf3函数所遮掩。从名称查找观点来看Base::mf1和Base::mf3不在被Derived继承。

1
2
3
4
5
6
7
8
Derived d;
int x;
...
d.mf1( );// fine, calls Derived::mf1d.mf1(x);
// error! Derived::mf1 hides Base::mf1
d.mf2( );// fine, calls Base::mf2
d.mf3( );// fine, calls Derived::mf3
d.mf3(x);// error! Derived::mf3 hides Base::mf3

更具以上代码可知,当我们在重载函数时,在子类中就只能对Derived作用域的函数名可见,但是对于重载函数是不可见的。不论是virtual还是non-virtual都是一样。

解决继承来的名称的遮掩行为

违反is-a关系

当public继承而又不继承哪些重载函数就是违反base和deriver class之间的is-a关系

using声明表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
private:
    int x;
public:
    virtual void mf1( ) = 0;
    virtual void mf1(int);
    virtual void mf2( );
    virtual void mf3( );
virtual void mf3(double);
    ...
};
class Derived: public Base {
public:
using Base::mf1;// make all things in Base named mf1 and mf3
using Base::mf3;// visible (and public) in Derived’s scope
virtual void mf1( );
    void mf3( );
    void mf4( );
    ...
};

1
2
3
4
5
6
7
8
9
10
11
Derived d;
int x;
...
d.mf1( );// still fine, still calls Derived::mf1d.mf1(x);
// now okay, calls Base::mf1
d.mf2( );
// still fine, still calls Base::mf2
d.mf3( );// fine, calls Derived::mf3
d.mf3(x);// now okay, calls Base::mf3 (The int x is
        // implicitly converted to a double so that
        // the call to Base::mf3 is valid.

用using声明,derived类继承了base并加上了重载函数,此时也可以重写一部分重载函数将base的函数给覆盖。

forward function转交函数

使用场景

不想继承base的所有函数,在“is-a”中会违背其含义

private继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
virtual void mf1( ) = 0;
virtual void mf1(int);
...// as before
};
class Derived: private Base {
public:
virtual void mf1( ) // forwarding function; implicitly
{ Base::mf1( ); } // inline — see Item30. (For info...
// on calling a pure virtual
//-------继承private----------
virtual void mf1(int);
    virtual void mf1( )
//---------------------------
};// function, see Item34.)
...
Derived d;
int x;
d.mf1( );// fine, calls Derived::mf1
d.mf1(x);// error! Base::mf1( ) is hidden

inline转交函数的用途

哪些不支持using声明式,将继承而得的名称汇入derived作用域

总结

✦Names in derived classes hide names in base classes. Under publicinheritance, this is never desirable.

✦To make hidden names visible again, employ using declarations orforwarding functions.


条款34:区分接口继承和实现继承

Difference between inheritance of interface and inheritance of implementation

public继承概念

函数接口继承函数实现继承

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
class Shape {
public:
    virtual void draw( ) const = 0;
    virtual void error(const std::string& msg);
    int objectID( ) const;
...
};
class Rectangle: public Shape {
...
};
class Ellipse: public Shape {
    ...
};

成员函数的接口总是会被继承

public为is-a关系继承,所有对Base class为真的事件对于Derived class也为真。

接口与实现

  • 接口:是(对外或者对继承)可视的,定义一个的对象实体可以通过(对外可视的)接口去访问该对象
  • 实现:是一个实体,可以看作是接口所要做到事,对外不一定可见,对内一定可见

pure virtual函数

1
2
3
4
5
class Shape {
public:
    virtual void draw( ) const = 0;
    ...
};

pure virtual函数的特性:

  • 必须被他们所继承的具象类所重新声明

  • 抽象类中通常没有对该函数的定义

pure virtual函数(子类必须重写)

让derived class只继承接口

shape class无法对shape::draw函数提供合理的默认实现,比较其模棱两可(椭圆和矩形的画法),因此在具象derived class 中必须提供一个draw函数,并且不干涉如何实现

1
2
3
4
5
6
7
Shape *ps = new Shape;// error! Shape is abstract
Shape *ps1 = new Rectangle;// fine 多态
ps1->draw( );// calls Rectangle::draw
Shape *ps2 = new Ellipse;// fine
ps2->draw( );// calls Ellipse::draw
ps1->Shape::draw();// calls base class pure virtual函数也可以有实现
Shape::drawps2->Shape::draw();// calls Shape::draw

impure virtual函数(可选是否重写)

让derived class继承函数的接口和默认实现

1
2
3
4
5
class Shape {
public:
virtual void error(const std::string& msg);
...
};

Shaped::error的声明式要求derived classes必须支持一个error函数,但如果不想自己写一个,那么就可以使用Shaped class提供的默认版本

non-virtual函数(不能重写)

让derived class继承函数的接口及一份强制性实现

1
2
3
4
5
class Shape {
public:
    int objectID( ) const;
    ...
};

non-virtual函数:并不打算在derived classes中有不同的行为。实际上non-virtual函数表现的不变性(invariant)大于其特异性(specialization)。

例如:Shape::objectID是有特定计算ID的一个函数,该方法是由其定义式决定的,任何derived class都不应该修改其行为不应该在derived class中被重新定义。(破坏多态性)

总结

✦Inheritance of interface is different from inheritance of implementa-tion. Under public inheritance, derived classes always inherit baseclass interfaces.

✦Pure virtual functions specify inheritance of interface only.

✦Simple (impure) virtual functions specify inheritance of interfaceplus inheritance of a default implementation.

✦Non-virtual functions specify inheritance of interface plus inherit-ance of a mandatory implementation.

条款35:考虑virtual函数以外的其他选择

Consider alternatives to virtual function

设计一个计算人物生命值的函数healthValue()

1
2
3
4
5
6
class GameCharacter {
public:
    virtual int healthValue( ) const;// return character’s health rating;
    ...
    // derived classes may redefine this
};

我们一impure virtual函数去声明函数,那么当子类不提供函数重写那么人物将采用默认的声明值的计算方法

Template Method模式

Non-virtual interface实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class GameCharacter {
public:
int healthValue( ) const// derived classes do not redefine
{// this — see Item36
...
// do “before” stuff — see below
int retVal = doHealthValue( );// do the real work
...
// do “after” stuff — see below
        return retVal;
}
...
private:
virtual int doHealthValue( ) const
    // derived classes may redefine this
{
...
// default algorithm for calculating}
// character’s health
};

基本设计

令客户通过public non-virtual成员函数去间接的调用private virtual函数,因此称为non-virtual(NVI)手法。

优点

NVI手法的优点在于“do ‘before’ stuff”和“do ‘after’ stuff”在上述的注释代码中,在Wrapper(healthValue)中设定好virtual函数的应用场景

  • “do ‘before’ stuff”:locking a mutex, making a log entry(日志记录项), verifying that class invariants and function preconditions aresatisfied, etc

  • “do ‘after’ stuff”:unlocking a mutex, veri-fying function postconditions, reverifying class invariants(再次验证class的约束条件), etc.

疑问

NVI手法中涉及到derived class时base class private virtual 函数我们无法调用,但是我们需要redefining这些我们不会调用的private virtual 函数,看起来十分的矛盾。但时调用virtual函数表示它在”何时“被完成,但重定义virtual函数表示”如何“完成,这两者并不冲突。

NVI允许derived重新定义virtual函数,从而赋予了它如何具体实现的机能,但base class仍然保留函数合适被调用的权力

特别的

NVI手法中的virtual函数并不是非得是private。某些继承体要求在derived class中对应的实现必须调用器base class的对应兄弟,为了合法,那么就必须的设置为protect。

Strategy Pattern via Function Pointers

设计主张

”人物的健康指数的计算与每个人物的类型无关“,这样的计算不需要人物这个成分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GameCharacter; 
// forward declaration
// function for the default health calculation algorithm
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;
};

优点

相比virtual函数继承,这种设计模式提供了更好的弹性

实例

  • 同一类型的不同的实体中应用不同的计算函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc):
GameCharacter(hcf )
{
...
}
...
};
int loseHealthQuickly(const GameCharacter&);// health calculationint lose
int HealthSlowly(const GameCharacter&);// funcs with different
// behavior
EvilBadGuy ebg1(loseHealthQuickly);// same-type charac-
EvilBadGuy ebg2(loseHealthSlowly);// ters with different// health-related
// behavior
  • 已知人物的健康指数计算函数可在运行期变更。例如:base类可以提供一个成员函数setHealthCalculator,用来替换当前的健康计算函数

Strategy Pattern via tr1::function

函数指针的限制

对template以及他们的的隐式接口的使用,基于函数的指针的做法就十分的死板。不够灵活,例如返回类型只能是int,函数对象不能是member function

tr1::function

改用tr1::function的对象替代函数指针,这样的对象可持有任何可调用物(callable entity 函数指针、函数对象、成员函数指针)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class GameCharacter; // as before
int defaultHealthCalc(const GameCharacter& gc);// as before

class GameCharacter {
public:
// HealthCalcFunc is any callable entity that can be called with
// anything compatible with a GameCharacter and that returns anything
// compatible with an int; see below for details
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):
                                            healthFunc(hcf ){}
    int healthValue( ) const { return healthFunc(*this); }
    ...
private:
    HealthCalcFunc healthFunc;
};

在这个实例中我们用tr1::function instantiation来代替目标签名式。那个签名代表的函数时”接受一个reference 指向const GamCharacter“,并返回int。这个tr1::function类型产生的对象可持有任何与此签名式兼容的可调用物。例如可调用物的参数可以被隐式的转换为const GameCharacters&,其返回类型可以被隐式转换为int

tr1::function对象相当于指向函数的泛化指针。

更具与弹性

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
31
32
33
34
35
36
37
38
39
short calcHealth(const GameCharacter&);// health calculation
// function; note
// non-int return type
struct HealthCalculator
{
// class for health
int operator()(const GameCharacter&) const// calculation function
{
...
}// objects
};
class GameLevel {
public:
float health(const GameCharacter&) const;
// health calculation
...
// mem function; note
};// non-int return type
class EvilBadGuy: public GameCharacter {
// as before
...
};
class EyeCandyCharacter: public GameCharacter {
// another character
...
// type; assume same
};
// constructor as // EvilBadGuy
EvilBadGuy ebg1(calcHealth);// character using a
                // health calculation// function
EyeCandyCharacter ecc1(HealthCalculator( ));// character using a
                // health calculation// function object
GameLevel currentLevel;
...
EvilBadGuy ebg2( // character using a
    std::tr1::bind(&GameLevel::health,// health calculation
currentLevel, // member function;
_1)
// see below for details);

解析ebg2 -> bind

1
2
3
4
EvilBadGuy ebg2( // character using a
std::tr1::bind(&GameLevel::health,// health calculation
currentLevel, // member function;
_1)

GameLevel::health接受两个参数,一个是隐式参数currentLevel,也就是this指向的那个、另一个是reference指向GameCharactor。

GameCharacters的健康计算函数值接受单一参数:GameCharacters。

使用GameLevel::health作为ebg2的健康计算函数,我们需要以特殊方式转换,取出GameLevel其中的健康计算函数。

本例中用currentLevel作为ebg2的健康函数所需的GameLevel的对象。_1意味着用currenLevel作为GamLevel的对象

the “Classic” Strategy Pattern

在该图中指示了GameCharacte是某个继承体系的根类,EviBadGuy与EyeCandyCharacter都是derived classes:HealthCalcFunc是另一个继承体系的根类。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class GameCharacter; // forward declaration
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;
};

总结

✦Alternatives to virtual functions include the NVI idiom and variousforms of the Strategy design pattern. The NVI idiom is itself an ex-ample of the Template Method design pattern.

✦A disadvantage of moving functionality from a member function to afunction outside the class is that the non-member function lacks ac-cess to the class’s non-public members.

✦tr1::function objects act like generalized function pointers. Such ob-jects support all callable entities compatible with a given target sig-nature.


条款36:绝不重新定义继承而来的non-virtual函数

Never redefine an inherited non-virtual function

non-virtual性质

在条款34中描述了non-virtual函数会给class建立一个不变性(invariant),凌驾其特异性(specialization)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class B {
public:
void mf( );
...
};
class D: public B {
...
};
//Even without knowing anything about B, D, or mf, given an object x of
//type D,
D x; // x is an object of type D
//you would probably be quite surprised if this,
B *pB = &x; // get pointer to xp
B->mf( ); // call mf through pointerbehaved differently from this:
D *pD = &x; // get pointer to xp
D->mf( ); // call mf through pointer

上述示例中我们都会调用B::mf() 版函数,但是如果我们在D class中重写mf()那么我们会发现以下问题

1
2
3
4
5
6
7
class D: public B {
public:
void mf( ); // hides B::mf; see Item33
...
};
pB->mf( ); // calls B::mf
pD->mf( ); // calls D::mf should call B::mf

non-virtual函数是一种静态绑定(statically bound)

    在子类中重写non-virutal函数,它会根据其**声明式** (也就是D* ,B*)来选取函数执行,但实际上pB与pD指向都是**同一对象**,按理来说应该调用同一对象的函数,因此重写non-virtual函数会**导致破坏多态性**。

public继承关系

public继承关系”is-a”关系,那么non-vitual函数的作用(不变性凌驾于特异性):

  1. 适用于B对象的每一件事,也适用与D对象

  2. B的derived classes一定会继承mf的接口与实现,因为mf是B的一个non-virutal函数

多态性的虚构问题

条款7: virtual析构函数,对于B pd = new D();由于声明的是non-virtual的析构函数,那么执行的时候会根据*声明式来定义 静态绑定调用函数,因此在多态中只会使用B的析构函数,对于D的析构则不会调用,这时候有些D类的成员不能被析构,会导致内存泄漏问题

总结

✦Never redefine an inherited non-virtual function.


条款37:绝不重新定义继承而来的默认参数值

Never redefine a function's inherited default parameter value

virtual函数系动态绑定,然而默认的却是静态绑定

静态绑定

静态绑定义

    在程序中被**声明时**所采用的类型,静态绑定容易造成的问题如:继承类重写non-virtual函数

简单来说就是调用对象是采用声明对象的一部分行为

1
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
31
32
33
34
35
36
37
38
39
40
#include <iostream>
using namespace std;
// a class for geometric shapes
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// all shapes must offer a function to draw themselves
virtual void draw(ShapeColor ) const = 0;
};
void Shape::draw(ShapeColor color = Red)const{
std::cout<<"Shape::darw "<<color<<std::endl;
}

class Rectangle: public Shape {
public:
// notice the different default parameter value — bad!
virtual void draw(ShapeColor color = Green) const{
std::cout<<"Rectangle::darw "<<color<<std::endl;
}
//...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const{
std::cout<<"Circle::darw "<<color<<std::endl;
}
};

int main(){
Shape *ps;                    // static type = Shape*
Shape *pc = new Circle();    // static type = Shape*
Shape *pr = new Rectangle();// static type = Shape*
ps = pc;
ps = pr;
// ps->draw();
pc->draw(Shape::Red);// calls Circle::draw(Shape::Red)
pr->draw(Shape::Red);// calls Rectangle::draw(Shape::Red)

pr->draw();// calls Rectangle::draw(Shape::Red)
}

继承关系

指针(静态类型)

1
2
3
Shape *ps;                // static type = Shape*
Shape *pc = new Circle;    // static type = Shape*
Shape *pr = new Rectangle;// static type = Shape*

动态绑定

动态绑定定义

   普遍的来说是多态性,由一个**静态类型**的对象指针(引用)指向一个子类对象,在运行其就会将行为于其指向的对象进行绑定,调用子类对象的行为。

    简单来说就是调用指向对象的行为

动态类型

1
2
ps = pc;// ps’s dynamic type is// now Circle*
ps = pr;// ps’s dynamic type is// now Rectangle*

调用

1
2
3
4
5
6
7
8
pc->draw(Shape::Red);// calls Circle::draw(Shape::Red)
pr->draw(Shape::Red);// calls Rectangle::draw(Shape::Red)
pr->draw();
====================
//运行结果
Circle::darw 0
Rectangle::darw 0
Rectangle::darw 0

问题分析:动态绑定与静态绑定冲突

    在pr->draw();中出现了问题,pr的动态类型为Rectangle调用为virtual函数,但Rectangle::draw默认参数为应该时GREEN,但由于pr的静态类型为Shape*,所以此一调用的默认阐述时来自于Shape class,而不是来之于Rectangle class。这个函数时两个类共同完成的

NVI解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const// now non-virtual
{
doDraw(color);// calls a virtual
}
    ...
private:
virtual void doDraw(ShapeColor color) const = 0;
// the actual work is
};// done in this func
class Rectangle: public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;// note lack of a
...// default param val.
};
    non-virtual 函数一个不会被derived classes重写(条款36),所以这个设计很清楚地使用了color的默认值为Red,相当于强制性不让动态绑定选择静态绑定的参数,NVI手法将动态绑定和静态绑定通过**private在继承中可见性**,进行了巧妙的结合

总结

✦Never redefine an inherited default parameter value, because defaultparameter values are statically bound, while virtual functions — theonly functions you should be redefining — are dynamically bound.


条款38:通过复合塑造出has-a或“根据某物实现出”

Model "has-a"or"is-implement-in-terms-of" throught composition

复合类型

在一个类中的数据成员是一个或者多个自定义数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
class Address { 
...
};// where someone lives
class PhoneNumber { ... };
class Person {
public:
...
private:
std::string name;// composed object
Address address;// ditto
PhoneNumber voiceNumber;// ditto
PhoneNumber faxNumber;// ditto
};

has-a与is-a

在上述Person中定义了name、address、voiceNumber、faxNumber,我们都很容易说,这个人有一个名字、地址、号码,但我们不能说这个人是一个名字,另外is-a是一种继承关系

复合类型中的has-a和“根据某物实现出”区分

  • has-a是应用域

  • is-implement-in-terms-of是实现域

用Set< T >继承list< T >声明如下

1
2
template <typename T>//将list应用于Set
class Set:public std::list<T>{...};

根据上述继承关系很容易区分出错误,list可以插入相同元素,Set不能含有相同元素,因此在逻辑上,Set不适用于list的逻辑,因此也不是is-a关系,所以对于这两种关系不能用public来实现.

正确做法

1
2
3
4
5
6
7
8
9
10
template<class T>// the right way to use list for Set
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;// representation for Set data
};

通过复合类型可以很明显的看出关系,Set只是依赖list来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 =// see Item42 for info on
    std::find(rep.begin( ), rep.end( ), item);// “typename” here
                if (it != rep.end( )) rep.erase(it);
}
template<typename T>
std::size_t Set<T>::size() const{
    return rep.size();
}

总结

✦Composition has meanings completely different from that of publicinheritance.

✦In the application domain, composition means has-a. In the imple-mentation domain, it means is-implemented-in-terms-of.


条款39:谨慎的使用“private”继承

use private inheritance judiciously

Private 继承

示例

1
2
3
4
5
6
7
class Person { ... };
class Student: private Person { ... }; // inheritance is now private
void eat(const Person& p);// anyone can eat
void study(const Student& s);// only students study
Person p;// p is a PersonStudent s;// s is a Student
eat(p);// fine, p is a Person
eat(s);// error! a Student isn’t a Person

private继承

根据以上代码的展示,我们会发现private继承与public继承不是同一个含义,同样的public继承会产生一些逻辑上的错误,例如学生不是人

  • private继承不会有隐式转换,同样不能多态
  • private的语义为根据某物实现( is-implemented-in-terms-of),不会对外呈现父类的接口
  • private继承中base的成员都会变为private无论是protected还是public

因此,private只在软件的实现中会有意义,在软件的设计中毫无意义

复合与private继承的抉择

private继承与复合都有根据某物实现( is-implemented-in-terms-of)的概念。

取舍

尽可能的选择复合,必要时才会使用private(当protected成员或virtual函数被牵扯进来)

使用Private继承

1
2
3
4
5
6
7
8
9
10
11
12
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick( ) const;// automatically called for each tick
...
};

class Widget: private Timer {
private:
virtual void onTick( ) const;// look at Widget usage data, etc
....
};

对于virtual函数,我们需要用private继承

上述代码当我们需要对一个Widget类进行计时,在运行期中周期性的检查Widget类。对于Timer这一个计时器,Widget中可以重新定义Timer内的virtual函数,但用public检查就说明Widget是一个Timer那肯定是不符合实际的。对于private继承确实是完美的选择:

  • Widget会拥有Timer的一些实现,因此也是根据某物实现。
  • 用户也不会造成接口的滥用,该此Timer实现也是对Widget对象内可见的。

复合实现

1
2
3
4
5
6
7
8
9
10
class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick( ) const;
...
};
WidgetTimer timer;
...
};

用复合类的方法实现private继承同样也是可以的,但是略显复杂

总结

✦Private inheritance means is-implemented-in-terms of. It’s usually inferior to composition, but it makes sense when a derived classneeds access to protected base class members or needs to redefineinherited virtual functions.

✦Unlike composition, private inheritance can enable the empty baseoptimization. This can be important for library developers who strive to minimize object sizes


条款40:明智而谨慎地使用多重继承

多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BorrowableItem {// something a library lets you borrow
public:
void checkOut();// check the item out from the library
...
};
class ElectronicGadget {
private:
bool checkOut() const;// perform self-test, return whether
...// test succeeds
};
class MP3Player:// note MI here
public BorrowableItem,// (some libraries loan MP3 players)
public ElectronicGadget
{ ... };                    // class definition is unimportant
MP3Player mp;
mp.checkOut();// ambiguous! which checkOut?

解决方案

1
mp.ElectronicGadget::checkOut();

因为在MP3player中又两个相同的接口,因此在调用是会无法识别到底调用那个,因此只能指定数据成员

但是会得到一个尝试调用private成员的错误

菱形继承

1
2
3
4
class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile,public OutputFile{ ... };

image-20220511205507673

以上的继承路线有两个条那么,假设File有一个filename数据成员分别继承到InputFile和OutputFile,当IOFile进行多重继承,那么我们会得到两份filename(InputFile::filename,Output::filename)。

得到的IOFile中如果要操作filename这数据成员要指定来自于那个父类同上。但再逻辑上这是不符合逻辑,一个文件不可能拥有两个名字。这时候就需要虚拟继承

虚拟继承

含义

解决多继承时的命名冲突和冗余数据问题,使得在派生类中只保留一份间接基类的成员。

防止二义性问题,共享Top-Base类数据。

虚拟继承与普通继承的区别

时间在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。(虚拟就是运行期进行选择)

空间由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之 多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证 这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。

示例

1
2
3
4
class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile,public OutputFile{ ... };

这样的做法就会使得再最终的outputFile中仅有一个一份filename,且不会产生二义性

虚拟继承的成本

空间

virtual继承的class产生的non-virtual的继承体积大

时间

访问virtual继承的成员变量时,比访问non-virtual base classe的速度慢

虚拟继承的初始化规则更复杂

初始化职责有继承中的最底层承担

  1. classes若派生自virtual bases而需要初始化,必须要知道其virtual bases
  2. 当一个新的derived class加入继承体系中,它必须承当其virtual bases的初始化职责

虚拟继承的抉择

  1. 非必要不用虚拟继承,就用non-virtual 继承
  2. 必须使用virtual base classes,尽可能避免再其中放置数据

总结

✦Multiple inheritance is more complex than single inheritance. It canlead to new ambiguity issues and to the need for virtual inheritance.

✦Virtual inheritance imposes costs in size, speed, and complexity ofinitialization and assignment. It’s most practical when virtual baseclasses have no data.

✦Multiple inheritance does have legitimate uses. One scenario in-volves combining public inheritance from an Interface class withprivate inheritance from a class that helps with implementation.CPersonIPersonPersonInfo{private}


模板与泛型编程

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

Understand implicit interfaces and compile-time polymorphism

面对对象

  • 提供显示接口
  • 运行期多态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
public:
Widget( );
virtual ~Widget( );
virtual std::size_t size( ) const;
virtual void normalize( );
void swap( Widget& other);// see Item25
...
};
//and this (equally meaningless) function,
void doProcessing( Widget& w){
if (w.size( ) > 10 && w != someNastyWidget) {
Widget temp(w);
temp.normalize( );
temp.swap(w);
}
}

分析

  • 显示接口:在本例中我们在doProcessing中调用w的接口,我们可以在源文件中找到这些接口(.h文件),必须知道接口的实现。
  • 运行期多态:在条款37中有动态类型绑定,widget中virtual函数表现出来的为运行期多态

Template以及泛型编程( generic programming )

面对对象的规则依然存在但是被弱化了

  • 隐式接口
  • 编译期多态
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需要提供一系列的隐式接口
  • w的任何函数的调用,入operator> and operator!=,有可能造成template的据具现化(instantiated),这些函数的调用都是具现化在编译期。通俗来讲就是,通过模板类型T去判断T中是否有以下行为(接口),如果没有则会编译失败,如果存在则进行选择

运行期多态与编译期多态

区别

  • 运行期多态:那个virtual函数被选择
  • 编译期多态:那个重载函数被调用

隐式接口与显式接口

显式接口

通常是由函数的签名式(函数名称、参数类型、返回类型)构成

1
2
3
4
5
6
7
8
class Widget {
public:
Widget( );
virtual ~Widget( );
virtual std::size_t size( ) const;
virtual void normalize( );
void swap( Widget& other);
};

如上public接口有一个构造函数、析构函数、各个成员函数及其参数类型、返回类型、常量性构成。

隐式接口

它不基于函数签名式,是由有效表达式(valid expression)组成。

1
2
3
4
5
6
template<typename T>
void doProcessing(T& w)
{
if (w.size( ) > 10 && w != someNastyWidget)
...
}

如上:

T的隐式接口有一系列的约束

  • 必须提供size函数
  • 必须提供operator !=的类型比较函数(假设someNastyWidget的类型为T)

隐式接口同样需要支持T类型

总结

✦Both classes and templates support interfaces and polymorphism.

✦For classes, interfaces are explicit and centered on function signatures. Polymorphism occurs at runtime through virtual functions.

✦For template parameters, interfaces are implicit and based on valid expressions. Polymorphism occurs during compilation through template instantiation and function overloading resolution.


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

Understand the two meaning of typename

class and typename 声明

1
2
template <class T> class widget;
template <typename T> class Widget;

class 与 typename没有任何不同。当声明template类型参数,class和typename的意义完全相同

typename的另一重意义

声明“类型”

当有static成员变量与T::~定义的类型命名冲突时会造成编译器的警告,因此需要typename的显式的声明这是命名为一个类型,而不是变量

示例

1
2
3
template<typename C>// typename allowed (as is “class”)
void f( const C& container,// typename not allowed
typename C::iterator iter);// typename required

上述的C不是嵌套从属类型名称,所以声明container时并不需要typename为签到,但C::iterator是个嵌套从属类型因此需要typename作为前置声明

例外

typename不能出现在base classes list内的嵌套从属类型名称之前,也不可以在member initalization list中作为base class修饰符

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class Derived: public Base<T>::Nested {// base class list: typename not
public:// allowed
explicit Derived(int x)
: Base<T>::Nested(x)// base class identifier in mem.{
// init. list: typename not allowed
typename Base<T>::Nested temp; // use of nested dependent type
...// name not in a base class list or
}// as a base class identifier in a
...// mem. init. list: typename required
};

typename 与 typedef连用

1
2
3
4
5
6
template<typename IterT>
void workWithIterator(IterT iter){
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
...
}

总结

✦When declaring template parameters, class and typename are inter-changeable.

✦Use typename to identify nested dependent type names, except inbase class lists or as a base class identifier in a member initializa-tion list


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

Know how to access names in templatized base classes

示例

假设一个程序可以发送消息给不同公司,信息可以以密文或则明文的方式发送,用类模板这可以在编译期对公司进行选择

1
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
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);
...
};
...// classes for other companies
class MsgInfo { ... };// class for holding information
// used to create a message
template<typename Company>
class MsgSender {
public:
...// ctors, dtor, etc.
void sendClear(const MsgInfo& info){
std::string msg;//create msg from info;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)// similar to sendClear, except
{ ... }// calls c.sendEncrypted
};

MsgSender类的调用不会用任何的问题

添加派生类LoggingMsgSender

1
2
3
4
5
6
7
8
9
10
11
12
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...// ctors, dtor, etc.
void sendClearMsg(const MsgInfo& info){
//write "before sending" info to the log;
sendClear(info);// call base class function;
// this code will not compile!
//write "after sending" info to the log;
}
...
};

:ice_cream: 这个派生类在新的一个non-virtual member函数中调用了父类的non-virtual member函数,这个函数解决了(条款33和条款36)non-virtual函数在派生类中出现的一系列问题,但是这个代码在不同的编译器是不能编译的

image-20220518220901314

问题所在

编译器遇到LoggingMsgSender的模板定义,不知道它是继承的那个类。

因为Company是一个模板参数,它是不确定的一个参数,只有当LoggingMsgSender被实例化后Company才会确定,因此在MsgSender< Company >的派生类中会出现无法确定继承的父类的问题。

更加明确的说是不知道Company中是否有sendClear这个函数

模板全特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CompanyZ {// this class offers no
public:// sendCleartext function
...
void sendEncrypted(const std::string& msg);
...
};
template<>// a total specialization of
class MsgSender<CompanyZ> {// MsgSender; the same as the
public:// general template, except
...// sendClear is omitted
void sendSecret(const MsgInfo& info){
...
}
};

通用的MsgSender模板是不适用于CompanyZ的,因为模板提供的sendClear函数是对CompanyZ没有意义的。因此在仍然会出现上述问题,在Company中找不到sendClear的声明(因为CompanyZ的特例化没有定义sendClear函数)

解决方案:

方案一:在base class函数调用动作之前加上this->

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){
write "before sending" info to the log;
this->sendClear(info);// okay, assumes that// sendClear will be inherited
write "after sending" info to the log;
}
...
};

方案二:使用using声明

在条款33中找不到基类重载继承下来的隐藏的函数,是因为被派生类所隐藏

在本例中是编译器不搜索基类作用域

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;// tell compilers to assume
...// that sendClear is in the// base class
void sendClearMsg(const MsgInfo& info){
...
sendClear(info);// okay, assumes that
...// sendClear will be inherited
}
...
};

方案三:显式的指定你的函数位于base 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){
...
MsgSender<Company>::sendClear(info);// okay, assumes that
...// sendClear will be
}// inherited
...
};

这是一个不太好的方法,因为被调用的是virtual函数会导致virtual函数的绑定行为被关闭

总结

✦In derived class templates, refer to names in base class templatesvia a “this->” prefix, via using declarations, or via an explicit base class qualification.


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

Factor parameter -independent code out of templates

不恰当的使用template可能会导致代码膨胀(code bloat):其二进制带着重复的代码、数据

解决方法:当两个函数实现的实质相同

抽离两个函数中共同的部分,将他放入第三个函数中,然后将他们调用这个新函数。class也是同样的道理,使用继承或则复合

示例

用固定尺寸的方阵编写一个template。

1
2
3
4
5
6
7
template<typename T,// template for n x n matrices of
std::size_t n>// objects of type T; see below for info
class SquareMatrix {// on the size_t parameter
public:
...
void invert( );// invert the matrix in place
};

其中的类型为size_t的参数是一个非类型参数(non-type parameter)。

对上述代码进行调用

1
2
3
4
5
6
SquareMatrix<double, 5> sm1;
...
sm1.invert();// call SquareMatrix<double, 5>::invert
SquareMatrix<double, 10> sm2;
...
sm2.invert();// call SquareMatrix<double, 10>::invert

分析

上述代码中,将会具现化两份invert函数,但这两份函数是完全相同,因为其中一个操作的55矩阵而另一个是10 10的矩阵,除了常量5和10其他部分完全相同,这将是一个典型的代码膨胀示例

解决方案一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>// size-independent base class for
class SquareMatrixBase {// square matrices
protected:
...
void invert(std::size_t matrixSize);// invert matrix of the given size
...
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert;// make base class version of invert
// visible in this class; see Items 33
// and 43
public:
...
void invert( ) {invert(n); }// make inline call to base class
};// version of invert

上述解决方案中

在所有方阵实体中只会共享一个父类的invert实现,这样就有效的防止的代码膨胀

  • 避免derived class代码重复:父类使用了protect代替了public。注在调用时会如果时public(实体对象仍然可以调用该接口)的话也同样会产生不同版本的代码
  • 调用其代码的成本为0,因为derived classes的inverts调用base clas的版本是inline调用
  • this->调用表示模板化基类反之函数名称被隐盖
  • 使用private的继承关系表现的是一种is-a关系

问题:在该方案中没有解决父类与子类之间联系的问题,因为在子类中需要带入矩阵的相关数据,因此需要加入一个指针或者引用。但是反复的传参,这样也会影响效率

解决方案二:父类中存储一个指针,指向所在的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(std::size_t n, T *pMem)// store matrix size and a
: size(n), pData(pMem) { }// ptr to matrix values
void setDataPtr( T *ptr) { pData = ptr; }// reassign pData
...
private:
std::size_t size;// size of matrix
T *pData;// pointer to matrix values
}
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:SquareMatrix( )// send matrix size and
: SquareMatrixBase<T>(n, data) { }// data ptr to base class
...
private:
T data[n*n];
};

这种方法在子类中调用了父类的构造函数,用来初始父类中的数据成员

当数据成员特别大的时候可以使用动态内存分配

1
2
3
4
5
6
7
8
9
10
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix( )// set base class data ptr to null,
: SquareMatrixBase<T>(n, 0),// allocate memory for matrixpData(new T[n*n])
// values, save a ptr to the
{ this->setDataPtr(pData.get( )); }// memory, and give a copy of it
...// to the base classprivate:boost::scoped_array<T> pData;
// see Item13 for info on
};

总结

✦Templates generate multiple classes and multiple functions, so anytemplate code not dependent on a template parameter causes bloat.

✦Bloat due to non-type template parameters can often be eliminatedby replacing template parameters with function parameters or classdata members.

✦Bloat due to type parameters can be reduced by sharing implemen-tations for instantiation types with identical binary representations.


条款45:使用成员函数template接受所有可以兼容的类型

Use member function template to accept “all compatible type”

智能指针

行为像指针的对象,并提供指针没有的技能。STL容器几乎都是用到智能指针,但是我们不会使用“++”的运算符将一个内置的指针从link list的节点移动到另一个节点,所以需要用到迭代器

真实指针(raw pointer)

支持隐式转换。例如:Derived class 指针可以隐式的转换为base class指针(提供多态的选择),“指向non-const对象”的指针可以转为”const对象”

1
2
3
4
5
6
class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top *pt1 = new Middle;// convert Middle*⇒ Top*
Top *pt2 = new Bottom;// convert Bottom*⇒ Top*
const Top *pct2 = pt1;// convert Top*⇒ const Top*

自定义智能指针

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class SmartPtr {
public:// smart pointers are typically
explicit SmartPtr( T *realPtr);// initialized by built-in pointers
...
};
SmartPtr<Top> pt1 =// convert SmartPtr<Middle> ⇒
SmartPtr<Middle>(new Middle);// SmartPtr<Top>
SmartPtr<Top> pt2 =// convert SmartPtr<Bottom> ⇒
SmartPtr<Bottom>(new Bottom);// SmartPtr<Top>
SmartPtr<const Top> pct2 = pt1;// convert SmartPtr<Top> ⇒
//SmartPtr<const Top>

上述同一个template的不同具现体(instantiation)之间不存在继承关系,所以SmartPtr< Top >与SmartPtr< Middle >是完全不同的class

Templates和泛型编程(Generic Programming)

生产需求:自定义指针构造函数的编写

当我们添加一个新继承关系的对象时,那没有添加转型的构造函数的情况下,就会反复的在SmartPtr中添加构造函数

class BelowBottom: public Bottom { ... };

member function tempaltes——泛化copy构造函数

1
2
3
4
5
6
7
template<typename T>
class SmartPtr {
public:
template<typename U>// member template
SmartPtr(const SmartPtr<U>& other);// for a ”generalized
...// copy constructor”
};

上述代码中对于任意类型T与任意类型U,可以根据SmartPtr< U >生成一个SmartPtr< T >——因为SmartPtr< T >有个构造函数接受有个SmartPtr< U >参数。

泛化的copy构造函数并未被声明为explicit,因为原始指针之间(base class与derived class之间)的转换是隐式的转换,无需明白的写出转型动作(cast)

提供原始资源的成员函数

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other)// initialize this held ptr:
heldPtr(other.get( )) { ... }// with other’s held ptr
T* get( ) const { return heldPtr; }
...
private:// built-in pointer held
T*heldPtr;// by the SmartPtr
};

使用成员初始化列表来初始化SmartPtr< T >之内类型为T的成员变量,并以类型为U的指针作为初值。

member initialization templates成员初始化列表的作用不限于构造函数,另一个作用是支持赋值操作

摘录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T> 
class shared_ptr {
public:
template<class Y>// construct from
explicit shared_ptr(Y * p);// any compatible
template<class Y> // built-in pointer,
shared_ptr(shared_ptr<Y> const& r);// shared_ptr,
template<class Y>// weak_ptr, or
explicit shared_ptr(weak_ptr<Y> const& r);// auto_ptr
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r);
template<class Y>// assign from
shared_ptr& operator=(shared_ptr<Y> const& r);// any compatible
template<class Y>// shared_ptr or
shared_ptr& operator=(auto_ptr<Y>& r);// auto_ptr...};

上述所有构造函数都是explicit,唯有”泛化copy构造函数”除外,那么从某个shared_ptr类型隐式转为另一个shared_ptr是允许的,但是从某个内置指针或从其他智能指针进行隐式转换则是不允许的(显示的转换倒是可以)。

总结:

✦Use member function templates to generate functions that accept allcompatible types.

✦If you declare member templates for generalized copy constructionor generalized assignment, you’ll still need to declare the normalcopy constructor and copy assignment operator, too


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

Define non-member function inside templates when type conversion are desired

这个条款换个说法是:当我们需要进行模板函数参数类型需要隐式转换时,将模板函数定义为friend函数

示例:条款24的例子转为模板

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>class Rational {
public:
Rational(const T& numerator = 0,// see Item20 for why params
const T& denominator = 1);// are now passed by referenceconst
T numerator( ) const;// see Item28 for why returnconst
T denominator( ) const;// values are still passed by value,
...// Item3 for why they’re const
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs)
{ ... }

进行以下混合式(mixed-mode)运算

1
2
3
Rational<int> oneHalf(1, 2);// this example is from Item24,
// except Rational is now a template
Rational<int> result = oneHalf * 2;// error! won’t compile

出现以下问题:

分析

以上问题是因为没有找对应的operator * 操作符对应的函数,也就是推导失败。

以上代码中operator*这个non-member函数的两个参数进行隐式类型推导时出现了问题:

  • 第一个实参是onehalf,所以T一定是int,能够顺利的推导出
  • 第二个实参是2,编译器无法将其推导为Rational< int >

经过以上考虑,因该是将隐式转换类型转换函数出现调用失败的问题,无法将non-member函数指定为operator*

friend函数声明

template class中friend声明式可以指定特定的函数,Rational< T >可以声明operator*class的一个friend函数

class template并不依赖template的实参推导,所以能够在class Rational< T >具现化时得知T,换句话说就是T的类型被确定后,就已经指定调用operator*函数

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>class Rational {
public:
Rational(const T& numerator = 0,// see Item20 for why params
const T& denominator = 1);// are now passed by referenceconst
...// Item3 for why they’re const
friend// declare operator*
const Rational operator*(const Rational& lhs,// function (see
const Rational& rhs);// below for details)
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs)
{ ... }

此时混合式调用可以通过编译,当对象onehalf被声明为一个Rational< int >,class Rational< int >被具现化出来了。而作为过程的一部分,friend函数operator(接受Rational< int > 参数)也就被自动声明出来。后者身为*一个函而非函数模板,因此编译器可以调用它时使用隐式转换函数。

上述friend函数同样可以声明为

1
2
friend
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs);

链接错误

image-20220603221112025

虽然经过修改我们能过通过编译,但是在链接时会出现上述问题了,因为friend函数只有一个声明式存在,并没有被定义,因此会导致连接器无法找到对应的实现

函数本体与声明式结合(简单版)

1
2
3
4
5
friend
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){
return Rational<T>(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs.denominator());
}

friend特殊意义

在本条款中虽然使用了friend却和它的传统意义不同(访问non-public成分),但是在此的意义却是让类型转换发生于所有实参身上,我们需要一个non-member函数(条款24);为了使这个函数自动具现化(隐式转换构造函数的指定),我们需要将它声明在class内部;而在class内部声明non-member函数的唯一方法就是将其声明为friend函数

non-member与friend member合作

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
31
32
33
template <typename T>
class Rational;
template <typename T>
const Rational<T> doMultiply(const Rational<T>& lhs,const Rational<T>& rhs);

template <typename T>
class Rational{
public:
Rational(T numerator = 0,
T denominator = 1):n(numerator),d(denominator){};
T numerator( ) const{return n;}
T denominator( ) const{return d;}
friend std::ostream& operator<<(std::ostream& out,const Rational& rhs)
{
out<< rhs.n/rhs.d;
return out;
}

friend
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){
return doMultiply(lhs,rhs);
}
private:
T n;
T d;
};

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());
}

总结

✦When writing a class template that offers functions related to thetemplate that support implicit type conversions on all parameters,define those functions as friends inside the class template.