0%

Effective C++之模板与泛型编程

7章:模板与泛型编程

条款41:了解隐式接口和编译期多态
  • 哪一个重载函数被调用——发生在编译期

  • 哪一个virtual函数被绑定——发生在运行期

  • classtemplate都支持接口和多态

    class而言,接口是显式的,以成员函数的签名为中心,多态则通过virtual函数发生在运行期。对template而言,接口是隐式的,以有效表达式为中心,多态则通过template具现化和函数重载解析发生于编译期。

条款42:了解typename的双重意义
  • 当我们声明模板类型参数,classtypename的意义完全相同

    1
    2
    template<typename T>
    template<class T>
  • typename用来标识嵌套从属类型名称

    template内出现的名称如果相依于某个template参数,称这个名称为从属名称,如果丛属名称在class内呈嵌套状,称为嵌套从属名称。如果嵌套从属名称还指涉某种类型名称为嵌套从属类型名称

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // std::iterator_traits<IterT>::value_type 就是嵌套从属类型名称
    template<typename IterT>
    void workWithIterator(IterT iter)
    {
    // 必须使用 typename 关键字标识
    typename std::iterator_traits<IterT>::value_type temp(*iter);
    ...
    }

    // 使用 typedef 少打几个字
    template<typename IterT>
    void workWithIterator(IterT iter)
    {
    // 必须使用 typename 关键字标识
    typedef typename std::iterator_traits<IterT>::value_type value_type;
    value_type temp(*iter);
    ...
    }
  • 不允许在base class listmember initialization list内使用typename作为base class的标识符

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

假设我们需要撰写一个MsgSender类,它能够传送信息到若干不同的Company去。信息要不进行加密后的密文,要不就是未经加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于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
31
32
33
34
35
36
37
38
39
40
41
class CompanyA
{
public:
...
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};

class CompanyB
{
public:
...
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
};

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

class MsgInfo {...}; // 这个 class 以备将来产生信息

template<typename Company>
class MsgSender
{
public:
... //构造函数、析构函数等等
void sendClear(const MsgInfo& info)
{
string msg;
... // 在这儿, 根据 info 产生信息
Company c;
c.sendCleartext(msg);
}

void sendSecret(const MsgInfo& info)
{
string msg;
... // 在这儿, 根据 info 产生信息
Company c;
c.sendEncrypted(msg);
}
};

现在假设我们想要在每次送出信息时log某些信息。derived class可轻易提供解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
... // 构造函数、析构函数等等
void sendClearMsg(const MsgInfo& info)
{
... // 将“传送前”的信息写至 log
sendClear(info); // 调用 base class 函数, 这段码无法通过编译!
... // 将“传送后”的信息写至 log
}
...
};

编译器会抱怨sendClear函数不存在,明明在那里,为什么?

问题在于,当编译器看见class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender<Company>,但其中的Company是个template参数,不当LoggingMsgSender被具现化之前无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么——更明确地说是没办法知道它是否有个sendClear函数。

具体点说,假设现在有个CompanyZ坚持只使用加密通讯:

1
2
3
4
5
6
7
8
// 不提供 sendCleartext 函数
class CompanyZ
{
public:
...
void sendEncrypted(const string& msg);
...
};

一般性的MsgSender templateCompanyZ并不合适,因为那个template提供了一个sendClearMsg函数,而这对CompanyZ对象并不合理。欲矫正这个问题,我们可以针对CompanyZ产生一个MsgSender特化版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 全特化版本只提供 sendSecret 函数
template<>
class MsgSender<CompanyZ>
{
public:
... //构造函数、析构函数等等
void sendSecret(const MsgInfo& info)
{
string msg;
... // 在这儿, 根据 info 产生信息
Company c;
c.sendEncrypted(msg);
}
...
};

这就解释了前面编译器拒绝那个调用的原因:它知道base class template有可能被特化,而那个特化版本可能不提供和一般性template相同的接口。因此它往往拒绝在templatized base class(本例的MsgSender<Company>)内寻找继承而来的名称(本例的SendClear)。

  • 当我们从Object Oriented C++跨进Template C++(见条款1) ,继承就不像以前那样畅行无阻了

有三个解决令C++编译器“不进入templatized base class观察”的行为失效的办法:

  1. 使用this指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<typename Company>
    class LoggingMsgSender : public MsgSender<Company>
    {
    public:
    ... // 构造函数、析构函数等等
    void sendClearMsg(const MsgInfo& info)
    {
    ... // 将“传送前”的信息写至 log
    this->sendClear(info); // 调用 base class 函数
    ... // 将“传送后”的信息写至 log
    }
    ...
    };
  2. 使用using声明

    虽然using声明式在在条款33或在这里都可有效运作,但两处解决的问题其实不相同。前者是base class名称被derived class名称遮掩,而后者是编译器不进入base class作用域内查找,于是我们通过using告诉它,请它那么做。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template<typename Company>
    class LoggingMsgSender : public MsgSender<Company>
    {
    public:
    using MsgSender<Company>::sendClear;
    ... // 构造函数、析构函数等等
    void sendClearMsg(const MsgInfo& info)
    {
    ... // 将“传送前”的信息写至 log
    sendClear(info); // 调用 base class 函数
    ... // 将“传送后”的信息写至 log
    }
    ...
    };
  3. 使用作用域运算符::明确指定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<typename Company>
    class LoggingMsgSender : public MsgSender<Company>
    {
    public:
    ... // 构造函数、析构函数等等
    void sendClearMsg(const MsgInfo& info)
    {
    ... // 将“传送前”的信息写至 log
    MsgSender<Company>::sendClear(info); // 调用 base class 函数
    ... // 将“传送后”的信息写至 log
    }
    ...
    };

即使如此,如果稍后这样做:

1
2
3
4
LoggingMsgSender<Companyz>zMsgSender;
MsgInfo msgData;
...
zMsgSender.sendClearMsg(msgData); // 仍然无法通过编译

其中对sendClearMsg的调用动作将无法通过编译,因为在那个点上,编译器知道base class是个template特化版本MsgSender<CompanyZ>,而且它知道那个class不提供sendClear函数,而sendClear却是sendClearMsg尝试调用的函数。

条款44:将与基类无关的代码抽离template

当你编写某个函数,其中某些部分的实现码和另一个函数的实现码实质相同,你会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。如果你正在编写某个class,而你明白其中某些部分和另一个class的某些部分相同,你也不会重复这共同的部分。你会把共同部分搬移到新class去,然后使用继承或复合(见条款323839) ,令原先的class取用这共同特性。而原class的互异部分仍然留在原位置不动。

编写template时,也是做相同的分析,以相同的方式避免重复。然而在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。

举个例子,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// template 支持 n x n 矩阵, 元素类型为 T
template<typename T, size_t n>
class squareMatrix
{
public:
...
void invert( ); // 求逆矩阵
};

SquareMatrix<double, 5> sml;
...
sm1.invert(); // 调用 SquareMatrix<double, 5>::invert
sSquareMatrix<double, 10> sm2;
...
sm2.invert(); // 调用 SquareMatrix<double, 10>::invert

这会具现化两份invert。这些函数并非完完全全相同,因为其中一个操作的是5x5矩阵而另一个操作的是10x10矩阵,但除了常量510,两个函数的其他部分完全相同。这是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
// 与尺寸无关的 base class
template<typename T>
class SquareMatrixBase
{
protected:
void invert(size_t matrixSize) // 以给定的尺寸求逆矩阵
{
...
}
};

template<typename T, size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
private:
// 声明为 private 是为了不让客户看见
using SquareMatrixBase<T>::invert; // 避免遮掩 base 版的 invert, 见条款 33
public:
...
void invert()
{
this->invert(n); // inline 调用 base class 版的 invert
// 为什么这儿出现 this-> 见条款 43
// LYL 认为前面已经使用了 using, this-> 应该也不需要了
}
};

这里的base class只是为了帮助derived class实现,不是为了表现SquareMatrixSquareMatrixBase之间的is-a关系(关于private继承,见条款39)。

目前为止一切都好,但还有一些棘手的题目没有解决。SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪儿?想必只有derived class知道。derived class如何联络其base class做逆运算动作?一个可能的做法是为SquareMatrixBase::invert添加另一个参数,可以是个指针,指向一块用来放置矩阵数据的内存地址。

第二次修改:

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
template<typename T>
class SquareMatrixBase
{
protected:
// 存储矩阵大小和一个指针, 指向矩阵数值
squareMatrixBase(size_t n, T* pMem) : size(n), pData(pMem) {}
void invert() // 不用指定尺寸了
{
...
}
...
private:
size_t size; // 矩阵的大小
T* pData; // 指针, 指向矩阵内容
};

template<typename T, size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
private:
using SquareMatrixBase<T>::invert;
public:
SquareMatrix() : SquareMatrixBase<T>(n, data) {}
void invert()
{
this->invert(n);
}
...
private:
T data[n * n]; // 这里直接将数据矩阵存储在 SquareMatrix 对象内部
// 也可以使用动态内存分配的方案
};

这个条款只讨论由non-type template parameter(非类型模板参数)带来的膨胀,其实type parameter(类型参数)也会导致膨胀。例如在许多平台上intlong有相同的二进制表述,所以像vector<int>vector<long>的成员函数有可能完全相同。某些链接器(linker)会合并完全相同的函数实现码,但有些不会,后者意味某些template被具现化为intlong两个版本,并因此造成代码膨胀。类似情况,所有指针类型都有相同的二进制表述,因此凡template持有指针者(例如list<int*>list<const int*>list<SquareMatrix<long, 3>*>等等)往往应该对每一个成员函数使用唯一一份底层实现。这很具代表性地意味,如果你实现某些成员函数而它们操作强型指针(strongly typed pointer,即T*),你应该令它们调用另一个操作无类型指针(untyped pointer,即void*)的函数,由后者完成实际工作。某些C++标准程序库实现版本的确为vectordequelisttemplate做了这件事。

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

内置指针是支持隐式转换的,比如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; // 将 Middle* 转换为 Top*
Top* pt2 = new Bottom; // 将 Bottom* 转换为 Top*
const Top* = pt1; // Top* 转换为 const Top*

但是我们自己实现的智能指针模板类,想做到这样就稍稍有点麻烦了。我们希望下面的代码能够通过编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
temmplate<typename T>
class SmartPtr
{
public:
explicit SmartPtr(T* realPtr); // 智能指针通常以内置指针完成初始化
...
};

// 将 SmartPtr<Middle>转 换为 SmartPtr<Top>
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);

// 将 SmartPtr<Bottom> 转换为 SmartPtr<Top>
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);

// 将 SmartPtr<Top> 转换为 SmartPtr<const Top>
SmartPtr<const Top> pct2 = ptl;

注意,这些赋值表达式需要调用的都是copy构造函数。这里的代码不能通过编译,因为如果以带有base-derived关系的BD两类型分别具现化某个template,产生出来的两个具现体并不带有base-derived关系,所以编译器视SmartPtr<Middle>SmartPtr<Top>为完全不同的class,为了获得我们希望获得的SmartPtr class之间的转换能力,我们必须将它们明确地编写出来。

我们应该为它写一个构造模板。这样的模板是所谓member function template,其作用是为class生成函数:

1
2
3
4
5
6
7
8
9
template<typename T>
class SmartPtr
{
public:
// member template, 为了生成 copy 构造函数
template<typename U>
SmartPtr(const SmartPtr<U>& other);
...
};

我们称之为泛化(generalizedcopy构造函数。它并未被声明为explicit,那是因为内置指针类型之间的转换(例如从derived class指针转为base class指针)是隐式转换,所以让智能指针按照这种形式也属合理。

但是,这个为SmartPtr而写的“泛化copy构造函数”提供的东西比我们需要的更多。我们只希望根据一个SmartPtr<Bottom>创建一个SmartPtr<Top>,却不希望根据一个SmartPtr<Top>创建一个SmartPtr<Bottomr>,因为那对public继承而言(见条款32)是矛盾的。我们也不希望根据一个SmartPtr<double>创建一个SmartPtr<int>,我们必须从某方面对这一member template所创建的成员函数群进行挑拣。

假设SmartPtr遵循std::shared_ptr也提供一个get成员函数,返回智能指针对象所持有的那个原始指针的副本,那么我们可以在“构造模板”实现代码中约束转换行为,使它符合我们的期望:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class SmartPtr
{
public:
// 以 other 的 heldPtr 初始化 this 的 heldPtr
template<typename U>
SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) {...}

T* get() const { return heldPtr; }
...
private:
T* heldPtr; // 这个 SmartPtr 持有的内置指针
};

这个行为只有当“存在某个隐式转换可将一个U*指针转为一个T*指针”时才能通过编译,而这正是我们想要的。

member function template(成员函数模板)的效用不限于构造函数,它常扮演的另一个角色是支持赋值操作。例如std::shared_ptr同时支持所有“来自兼容之内置指针、std::shared_ptrstd::weak_ptr”的构造行为以及赋值操作(std::weak_ptr除外)。赋值操作符和copy构造函数实现类似,故省略。

  • 如果你声明member template用于“泛化copy构造”或“泛化assignment操作”你还是需要声明正常的copy构造函数和copy assignment操作符

条款5说过,编译器可能为我们产生四个成员函数,其中两个是copy构造函数和copy assignment操作符。现在,SmartPtr声明了一个泛化copy构造函数,而显然一旦类型TU相同,泛化copy构造函数会被具现化为“正常的”copy构造函数。那么究竟编译器会暗自为SmartPtr生成一个copy构造函数呢?或当某个SmartPtr对象根据另一个同型的SmartPtr对象展开构造行为时,编译器会将“泛化copy构造函数模板”具现化呢?

member template并不改变语言规则:“如果程序需要一个copy构造函数,你却没有声明它,编译器会为你暗自生成一个”。在class内声明泛化copy构造函数(是个member template)并不会阻止编译器生成它们自己的copy构造函数(一个non-template),所以如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和“正常的”copy构造函数。相同规则也适用于赋值操作。

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

条款24讨论过为什么惟有non-member函数才有能力“在所有实参身上实施隐式类型转换”,该条款并以Rational classoperator*函数为例。本条款将Rationaloperator*模板化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
class Rational
{
public:
// 条款 20 告诉你为什么参数以 passed by reference 方式传递
Rational(const T& numerator = 0, const T& denominator = 1);

// 条款 28 告诉你为啥返回值以 passed by value 方式传递
// 条款 3 告诉你为啥它们是 const
const T numerator() const;
const T denominator() const;
...
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}

就像条款24一样,我们也希望支持混合式算术运算,然而以下代码却不能通过编译:

1
2
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // 错误!无法通过编译

在这里,编译器不知道我们想要调用哪个函数!它们试图找到什么函数被名为operator*template具现化出来。它们知道它们应该可以具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但为完成这一具现化行动,必须先算出T是什么。

为了推导T,它们看了看operator*调用动作中的实参类型。分别是Rational<int>oneHalf的类型)和int2的类型)。

oneHalf进行推导,过程并不困难。第二参数的推导则没有这么顺利。operator*的第二参数被声明为Rational<T>,但传递给operator*的类型是int2)。编译器如何根据这个2推算出T?你或许会期盼编译器使用Rational<int>non-explicit构造函数将转换为Rational<int>,进而将T推导为int,然而template实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换。

template class内的friend声明式可以指涉某个特定函数。这意味class Rational<T>可以声明operator*是它的一个friend函数。class template并不倚赖template实参推导,所以编译器总是能够在class Rational<T>具现化时得知T。因此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class Rational
{
public:
...
friend
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs);
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}

现在当对象oneHalf被声明为一个Rational<int>class Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来。后者身为一个函数而非function template,因此编译器可在调用它时使用隐式转换函数(例如调用Rationalnon-explicit构造函数)。

但是,虽然这段代码通过编译,却无法连接。

因为这个友元函数只被声明于Rational内,并没有被定义出来。我们意图令此class外部的operator* template提供定义式,是行不通的。既然我们没有提供定义式,连接器当然找不到它!

最简单的方法就是,将函数本体放进class内:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class Rational
{
public:
...
friend
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
};

为了让类型转换可能发生于所有实参身上,我们需要一个non-member函数(条款24);为了令这个函数被自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。因此我们就这样做了。

  • 当此friend函数过于复杂时,令其调用类外的辅助函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    template<typename T> class Rational; // 前向声明

    template<typename T>
    const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>&rhs);
    {
    return Rational<T>(lhs.numerator() * rhs.numerator(),
    lhs.denominator() * rhs.denominator());
    }

    template<typename T>
    class Rational
    {
    public:
    ...
    friend
    const Rational<T>operator* (const Rational<T>& lhs,
    const Rational<T>& rhs)
    {
    return doMultiply(lhs, rhs);
    }
    ...
    };

    作为一个templatedoMultiply当然不支持混合式乘法,但它其实也不需要。它只被operator*调用,而operator*支持混合式操作。

条款47traits classes表现类型信息

STL迭代器分类:

  1. input迭代器

    只能向前移动,一次一步,用户只能读取它所指的东西,而且只能读取一次,读完立即自动向前一步。程序库中的输入流迭代器istream_iterator就属于这一类。

  2. output迭代器

    只能向前移动,一次一步,用户只能涂写它所指的东西,而且只能涂写一次,写完立即自动向前一步。程序库中的输出流迭代器ostream_iterator就属于这一类。

  3. forward迭代器

    可以做上述两种迭代器做的事情,而且支持多次读写,读写完由用户自行决定是否前进。单向链表slist提供的迭代器就属于这一类。

  4. bidirectional迭代器

    除了能做forward迭代器做的事情,还可以向后移动。双向链表list以及setmutilsetmapmutilmap提供的迭代器属于这一类。

  5. random access迭代器

    功能类似于内置指针,可以进行算数操作。vectordequestring提供的迭代器属于这一类。

  6. 迭代器适配器

    • reverse迭代器
    • insert迭代器
      • back inserter
      • front inserter
      • general inserter
    • stream迭代器
    • move迭代器
1
2
3
4
5
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};

这些struct之间的继承关系是有效的is-a关系:所有forward迭代器都是input迭代器,依此类推。

关于iterator_trait的实现技术(参考书籍P227-P232

条款48:认识template元编程
  • 如题,以后买本书再专门去学吧!