2019/11/19 effective cpp 第七章 模板与范型编程

C++ template最初是为了让我们建立类型安全的容器,如vector,list,map等等,后来随着越来越多的人用上模板之后,人们发现,template这种代码与其处理对象类型分离,彼此独立的风格很好,于是人们道出了模板元编程,template的作用越来越大。

本章主要解决在使用template上遇到的一些可以避免,优化的问题。

  • 41 条款:了解隐式接口和编译期多态
  • 42 条款:了解typename的双重含义
  • 43 条款:学习处理模板化基类内的名称
  • 44 条款:将与参数无关的代码抽离template
  • 45 条款:运用成员函数模板接受所有兼容类型
  • 46 条款:需要类型转换时请为模板定义非成员函数
  • 47 条款:请使用traits classes表现类型信息
  • 48 条款:认识template元编程

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

面向对象编程总是以显式的接口和运行期多态来解决问题,它具有两个特点:

  • 必须在子类总的各种方法,且他的代码在源码中是明确可见的。
  • 由于widget的某些成员函数是virtual,w对于那些函数的调用将表现出运行期间的多态,根据运行期间w的动态类型来决定调用哪一个函数。

在template的泛型编程中,我们将函数转变为函数模板:

1
2
3
4
5
6
7
8
template<typename T>
void doProcessing(T& w){
if (w.size() > 10 && w!= someWidget){
T temp(w);
temp.normalize();
...
}
}

从上面的代码,我们可以认为T这种类型应该具有size(),normalize()这些函数,允许进行大小的比较。但是实际上,对于模板类来说,他不一定必须要具备这些,这就是和显示接口的一个重大的不同。

对于显式接口来说他由函数的签名式构成,即包含函数的名称,参数类型,返回类型。

1
2
3
4
5
6
class widget{
widget();
virtual ~widget();
virtual void normalize();
...
}

对于隐式接口来说,他并不是基于函数签名式,而是由有效表达式组成的,如上的第一份代码。

由于操作符允许重载,因此在实现上述接口的时候,类型T不必要满足支持size成员函数,operation成员函数等。对于size()可由他的父类来提供。对于operator>来说,只要存在一个隐式转换就能够进行类型的转换,将操作符两边的对象转换为同一种对象即可。

总结

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

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

在template的声明式中,class和typename没有不同。

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

当我们在声明参数的时候,上面的两种表达方式完全相同。

在template中,我们存在着两种类型的变量。

从属名称:template内部出现名称依赖于某个template参数。如果存在嵌套的话,则称为嵌套从属名称,如C::iterator,类型C的从属名称。

非从属名称:对于类似于int那种名称,不依赖于template。

对于从属名称来说,typename有时候表示为一种类型,而有时候则是一个成员白能量,例如:

1
2
3
4
template<typename T>
void func(const C& container){
C::const_iterator*x;
}

当上式C::const_iterator表示一个变量的时候,上面变成一个乘法的表达式,如果他是一个类型的话,那就表示声明了一个local的指针。

C++是如何区分这种情况的呢,C++在默认的情况下,处理从属关系的时候优先认为这是一个变量,而不是一个类型,除非你告诉编译器。

显式告诉编译器这是个类型的方式是通过typename来实现的:

1
2
3
4
template<typename C>
void func(const C& container){
typename C::cosnt_iterator iter(container.begin());
}

typename只被用来确定嵌套从属类型的名称,在其他地方不要去使用它。

1
2
3
template<typename C>
void f(const C& container, // 一定不要使用typename
typename C::iterator iter); // 一定要使用typename

此外,在typename在一个特殊的例子中是不允许使用的,就是 base class list 以及mem init list即父类列表,以及成员初始化的初始化列表中不允许使用。

当我们在使用嵌套类型的时候,有时候类型名非常的长,我们希望通过typedef来给他重命名,可以将typedef typename一起连用:

1
typedef typename std::iterator_traits<iterT>::value_type value_type;

总结

  • 声明template参数时,前缀关键字class和typename可互换。
  • 请使用关键字typename标识嵌套从属类型的名称,但不得在base class lists或mem init list以他作为base class修饰符。

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

template的继承和显式的继承有些不同之处:

1
2
3
4
5
6
7
8
9
template<typename company>
class Loggin:public MsgSender<company>{
public:
void sendClearMSG(const MsgSender<Company>){
// do something
sendClear(info); // 调用父类中的sendClear函数
// do something
}
}

上面的代码如果实在class的继承中,一定是成立的,但是template继承中则会出错,因为在继承MsgSender<company>的时候,编译器并不知道这是个什么样的class,也就自然不知道这个class中是否有一个sendClear函数了,因此上面的调用将会出错。

解决方法:

  1. 在base class函数调用动作之前加上this->
1
2
3
4
5
6
7
8
9
template<typename company>
class Loggin:public MsgSender<company>{
public:
void sendClearMSG(const MsgSender<Company>){
// do something
this->sendClear(info); // 调用父类中的sendClear函数
// do something
}
}
  1. 使用using声明式,是的父类的方法能够在子类中可见
1
2
3
4
5
6
7
8
9
10
template<typename company>
class Loggin:public MsgSender<company>{
public:
void sendClearMSG(const MsgSender<Company>){
// do something
using MsgSender<company>::sendClear;
sendClear(info); // 调用父类中的sendClear函数
// do something
}
}
  1. 明确指出函数在base class内
1
2
3
4
5
6
7
8
9
template<typename company>
class Loggin:public MsgSender<company>{
public:
void sendClearMSG(const MsgSender<Company>){
// do something
MsgSender::sendClear(info); // 调用父类中的sendClear函数
// do something
}
}

在template继承的时候,子类对父类的方法一无所知,因此我们需要通过this,或者明确指出父类方法的方式得到函数的声明。

总结

  • 可在derived class templates内通过this->指涉base class template内的成员名称,或由一个明白写出的base class资格的修饰符,使用using 或直接由类调用。

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

template是一个节约时间与避免代码重复的一个方法。但是有时候我们可能会导致代码膨胀。

一些指针,vector,list等等,位于父类函数中,将会造成代码的膨胀。

总结

  • template生成多个class和多个参数,所以任何template代码都不该与某个造成膨胀的template参数产生相依的关系。
  • 因非类型末班参数而造成的代码膨胀,往往可以消除,做法是用函数参数或class成员变量替代template参数
  • 因类型参数而造成的代码膨胀往往可以降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。

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

智能指针是行为上像是一个指针的对象,它提供了指针的所有机能,在STL容器中,我们总是使用智能指针。此外指针的另一很好的优点在于支持隐式转换,即子类指针可以隐式的转换为父类指针。但是这种关系在template类模板中是不存在的。

用具有base-derived关系的对象去具现化某个template的时候,产生出来的的具现体并不具有base-derived的关系。

一个可行的方法就是实现一个template构造函数,即构造模板:

1
2
3
4
5
6
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other); // 将一个具现化的u转型为t
};

这一构造函数根据对象u创建对象t,而u和t的类型是同一个template的不同具现体,我们称这个函数为泛化copy构造函数。需要注意的是,上面的前提是说,一个U可以被转型为T。

我们同样可以在构造函数中完成我们想要达到的转化:

1
2
3
4
5
6
7
8
9
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other)
:heldPtr(other.get()){...} // 使用u的指针去初始化t变量的指针
private:
T* heldPtr;
};

同样的,上述的做法是在 U指针可以隐式转换为T*的基础上才成立的。

泛化copy构造函数与普通的copy构造函数之间不存在冲突,因此具现化后的类,依旧会为这个类实现一个copy构造函数。

总结

  • 请使用member function templates成员函数模板,生成可接受所有兼容类型的函数。(即上面的u->t)。
  • 如果你声明member templates用于泛化copy构造或泛化assignment操作,你还是需要声明正常的copy构造函数和copy assignment操作符,因为编译器默认生成的函数不会因为生成泛化copy构造函数受到影响。

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

当我们在使用template来定义非成员函数,同时这个成员函数的参数需要隐式的转换的话,我们可能会遇到问题:

1
2
3
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs)
{...}

当我们调用上面代码:

1
Rational<int> result = onehalf*2;

将会出现编译错误,因为template类型Rational<T>在具现化的时候需要确定T的类型,当遇到2的时候,C++无法推断出T为int,因此无法通过编译。

此路不通,我们曲线救国,通过将Rational<T>申请为class Rational的friend函数的方式来实现。

1
2
3
4
5
template<typename T>
class Rational{
public:
friend Rational operator*(const rational& lhs,const& rhs);
}

通过友元的方式来制定一个具体的函数,从而避免template进行参数的推导。

但是上面方法同样会引发一个问题,就是我们通过友元的方式,使得我们可以通过友元来确定函数,但是仅仅是个声明,没有函数的实现,一个最直接的方法就是我们直接将函数的本体定义在Rational乘法里头。

总结

  • 当我们需要编写一个class template,而他所提供的与此template相关的函数支持所有参数隐式类型转换时,请将那些函数定义为class template内部的friend函数。

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

在一些状况下,我们需要知道一个类的某些信息。traits构件就是做这件事情的,他是一种技术,也是C++程序员所遵守的一种协议。我们将trait放入一个template中去:

1
2
template<typename TT>
struct iterator_traits;

接下来我们确认一个traits中应该包含哪些信息:

  • 确认若干你希望将来可取得的类型的相关信息,例如对于迭代器,我们希望将来可取的他的分类。
  • 为该信息选择一个名称
  • 提供一个template与一组特化版本,内含你希望看到的相关信息

接下来是如何使用traits:

  • 建立一个重载函数或函数模板,彼此的差异在于各自的trait参数;
  • 建立一个控制函数或函数模板,用于调用上述的函数,并传递trait信息。

总结

  • traits class使得类型相关信息在编译期可用,它以template和templates特化完成实现。
  • 整合重载技术后,trait class有可能在编译期对类型执行if … else 操作。

48 条款:认识template元编程

template metaprogramming元编程是编写template-based c++程序并执行与编译期的过程。

总结

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

it is a little difficult for me,but never mind !