C++进阶_Effective_C++第三版(七) 模板与泛型编程 Templates and Generic Programming

模板与泛型编程 Templates and Generic Programming

C++模板的最初设计目的是建立类型安全的容器如vector,list和map。后来发现模板有能力完成越来越多可能的变化。后面就出现了泛型编程,写出的代码和其所处理的对象类型彼此独立。STL算法如for_each,find和merge就是这类编程的成果。后面发现它可以被用来计算任何可计算的值。于是导出了模板元编程。创造出在C++编译器内执行并于编译完成时停止执行的程序。

41、了解隐式接口和编译期多态

Understand implicit interfaces and compile-time polymorphism.
面向对象编程世界总是以显式接口和运行期多态解决问题。举个例子,给定这样的class:

class Widget{
public:
	Widget();
	virtual ~Widget();
	virtual std::size_t size() const;
	virtual void normalize();
	void swap(Widget& other);};
// 和这样的函数:
void doProcessing(Widget& w)
{
	if(w.size() >10 && w != someNastyWidget)
	{
		Widget temp(w);
		temp.normalize();
		temp.swap(w);
	}
}

对于doProcessing内的w,由于w的类型被声明为Widget,所以w必须支持Widget接口,可以在源码中找到这个接口,这就为一个显式接口,由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态,也就是说将于运行期根据w的动态类型决定究竟调用哪个函数。
Templates及泛型编程中,和面向对象有根本上的不同。显式接口和运行期多态仍然存在,但重要性降低。隐式接口和编译期多态移到前面。如将上述的doProcessing从函数变成函数模板:

template<typename T>
void doProcessing(T& w)
{
	if(w.size() >10 && w != someNastyWidget)
	{
		T temp(w);
		temp.normalize();
		temp.swap(w);
	}
}

现在对于doProcessing内的w,w必须支持的接口由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数、copy构造函数(用来建立temp)、不等比较。重要的是这一组表达式便是T必须支持的一组隐式接口。凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功。这样这样的具现化发生在编译期。“以不同的template参数具现化”会导致调用不同的函数,这便是所谓的编译期多态。
通常显式接口由函数的签名式(也就是函数名称、参数名称、返回类型)构成。隐式接口则完全不同,它并不基于函数签名式,而是由有效表达式组成。如上面模板函数doProcessing,T的隐式接口有这些约束:必须提供一个名为size的成员函数,该函数返回一个整数值。必须支持一个operator!=函数,用来比较两个T对象等等。加诸于template参数身上的隐式接口,就像加诸于class对象身上的显式接口一样,而且两者都在编译期完成检查。就和无法以一种“与class提供的显式接口矛盾”的方式来使用对象(代码将通不过编译),也无法在template中使用“不支持template所要求之隐式接口”的对象(代码将通不过编译)。

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

42、了解typename的双重意义

Understand the two meanings of typename.
很多时候typename和class是等价的,意义完全相同。但是也有例外。假设有个模板函数,接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints,然后打印其第二个元素值:

template<typename C>
void print2nd(const C& container)	//打印容器内的第二元素
{							//这不是有效的C++代码
	if(container.size() >= 2)
	{
		C::const_iterator iter(container.begin());	//取第一个元素的迭代器
		++iter;			
		int value = *iter;		//将该元素复制到某个int
		std::cout << value;
	}
}

template内出现的名称如果相依于某个template参数,称之为从属名称。如果从属名称在class内呈嵌套状,称为嵌套从属名称如C::const_iterator。实际它还是个嵌套从属类型名称,也就是个嵌套从属名称并且指涉某类型。而变量value不依赖任何template参数,称为非从属名称。嵌套从属名称有可能导致解析困难如:

template<typename C>
void print2nd(const C& container)	
{
	C::const_iterator* x;}

看起来好像声明x为一个local变量,是个指针,指向一个C::const_iterator。但是如果C::const_iterator不是一个类型且x是个全局变量名称,上述代码成了一个相乘动作。为了解除歧义,必须显式的声明其为类型,只需要给其前面加上typename即可:

template<typename C>
void print2nd(const C& container)
{
	if(container.size() >= 2)
	{
		typename C::const_iterator iter(container.begin());}
}

Typename只被用来验明嵌套从属类型名称,其他名称不该有其存在。如下函数模板接受一个容器和一个指向该容器的迭代器:

template<typename C>				//允许使用typename或class
void f(const C& container,			//不允许使用typename
typename C::iterator iter);	//一定要使用typename

上述的C并不是嵌套从属类型名称,所以声明container时并不需要以typename为前导,但C::iterator是个嵌套从属名称类型,所以必须以typename为前导。typename必须作为嵌套从属类型名称的前缀词的例外时typename不可以出现在基类list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为基类修饰符。例如:

template<typename T>
class Derived: public Base<T>::Nested{	//基类list中不允许typename
public:
	explicit Derived(int x):Base<T>::Nested(x)	//成员初值列不允许typename
	{
		typename Base<T>::Nested temp;	//作为一个基类修饰符需加上typename}};

假设写一个函数模板,它接受一个迭代器,然后为该迭代器指涉的对象做一份local复件temp:

template<typename IterT>
void workWithIterator(IterT iter)
{
	typename std::iterator_traits<IterT>::value_type tewmp(*iter);}

std::iterator_traits::value_type是标准traits类的一种运用,相当于类型为IterT的对象所指之物的类型。这个语句声明一个local变量(temp),使用IterT对象所指物的相同类型,并将temp初始化为iter所指物。如果IterT是vector::iterator,temp的类型就是int。

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

43、学习处理模板化基类内的名称

Know how to access names in templatized base classes.
假设有个程序,能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未加工的文字。如果编译期间我们有足够信息来决定哪个信息传递哪个公司,用基于template实现:

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);};class MsgInfo	{};	//保存信息,以备将来产生信息
template<typename Company>
class MsgSender{
public:void sendClear(const MsgInfo& info)
	{
		std::string msg;
		//根据info产生信息
		Company c;
		c.sendCleartext(msg);
	}
	void sendSecret(const MsgInfo& info)	//类似sendClear,唯一不同调用c.sendEncrypted
	{}
};

假设想在每次送出信息时记录日志。derived class实现如下:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:void sendClearMsg(const MsgInfo& info)
	{
		//将传送前的信息写至log
		sendClear(info);		//调用基类函数,代码是不能通过编译的。
		//将传送后的信息写至log
	}
};

这个派生类的信息传递函数有一个不同的名称sendClearMsg,与其基类内的名称sendClear不同,可以避免遮掩继承而得的名称,也避免重新定义一个继承而得的非虚函数。但上述代码不能通过编译,是因为编译器在处理class template LoggingMsgSender定义式时,并不知道它继承什么样的class。虽然看起来继承MsgSender,但其中的Company是个template参数,不到LoggingMsgSender被具现化无法确切知道它是什么。而且如果不知道Company是什么,就无法知道class MsgSender看起来像什么,更明确地说是没办法知道它是否有个sendClear函数。
为解决此问题,有三个办法:第一在基类函数调用动作之前加上this->:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:void sendClearMsg(const MsgInfo& info)
	{
		//将传送前的信息写至log
		this->sendClear(info);		//假设sendClear将被继承下来。
		//将传送后的信息写至log
	}
};

第二是使用using声明式:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
	using MsgSender<Company>::sendClear;void sendClearMsg(const MsgInfo& info)
	{
		//将传送前的信息写至log
		sendClear(info);		//假设sendClear将被继承下来。
		//将传送后的信息写至log
	}
};

第三是明确指出被调用的函数位于基类内:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:void sendClearMsg(const MsgInfo& info)
	{
		//将传送前的信息写至log
		MsgSender<Company>::sendClear(info); //假设sendClear将被继承下来。
		//将传送后的信息写至log
	}
};

第三个做法是最不可取的,因为如果被调用的是个virtual函数,上述的明确资格修饰会关闭virtual绑定行为。

  • 可在派生类模板内通过this->指涉基类模板内的成员名称,或由一个明白写出的基类资格修饰符完成。

44、将与参数无关的代码抽离template

Factor parameter-independent code out of template.
Templates是节省时间和避免代码重复的一个奇方妙法。但是使用template可能会导致代码膨胀,其二进制码带着重复的代码、数据。一般编写某个class,然后发现和另一个class的某些部分相同,把共同的部分搬移到新类中,然后使用继承或复合令原先的类取用这些共同特性。编写template时,也是做相同的分析,以相同的方式避免重复,但在template代码中,代码重复时隐晦的,因为只存在一份template源码,需要自己感受当template被具现化多次时可能发生的重复。
假设为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算。
template<typename T, std::size_t n> //template支持n*n矩阵,元素类型为T的对象。

class SquareMatrix{
public:void invert();	//求逆矩阵
};

这个template接受一个类型参数T,除此之外还接受一个类型为size_t的参数,那是个非类型参数。这种参数和类型参数比起来较不常见,但完全合法。如下使用:

SquareMatrix<double, 5> sm1;
…
sm1.invert();
SquareMatrix<double, 10> sm2;
…
sm2.invert();

上述代码会具现化两份invert。这些函数并非完全相同,但是除了常量5和10,其它部分完全相同。这种现象是template造成代码膨胀。
对于此可以采用下述实现:

template<typename T >	//与尺寸无关的基类
class SquareMatrixBase{
protected:void invert(std::size_t matrixSize);	//求逆矩阵
};
template<typename T, std::size_t n>	
class SquareMatrix: private SquareMatrixBase<T> {
private:
	using SquareMatrixBase<T>:: invert
public:void invert(){	this->invert(n);	 };	//求逆矩阵
};

这种做法利用SquareMatrixBase:: invert使用protected避免代码重复。调用它而造成的额外成本是0,因为派生类的invert调用基类版本时用的是inline调用。但是这个需要每次告诉基类矩阵尺寸,需要额外添加一个额外参数,不够友好。所以可以让SquareMatrixBase储存一个指针,指向矩阵数值所在的内存,只有它存储了那些东西,也就可能存储矩阵尺寸:

template<typename T, std::size_t n>	
class SquareMatrix: private SquareMatrixBase<T> {
public:
	SquareMatrix() : SquareMatrixBase<T>(n,data) {	}private:
	T data[n*n]; 
};

这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一个做饭是把每一个矩阵的数据放进heap:

template<typename T, std::size_t n>	
class SquareMatrix: private SquareMatrixBase {
public:
	SquareMatrix() : SquareMatrixBase<T>(n,0),pData(new T[n*n])
	{	this->setDataPtr(pData.get());	}private:
	boost::scoped_array<T> pData; 
};
  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
  • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
  • 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现化类型共享实现码。

上一篇: C++进阶_Effective_C++第三版(六) 继承与面向对象设计 Inheritance and Object-Oriented Design
下一篇: C++进阶_Effective_C++第三版(八) 定制new和delete Customizing new and delete

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章