effective C++笔记--模板与泛型编程(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/rest_in_peace/article/details/84344543

了解隐式接口和编译器多态

. 面向对象编程世界总是以显式接口和运行期多态解决问题。比如一个函数中有一个参数是一个类的指针或引用,由于该参数的类型确定,所以它必定支持这一类的接口,可以在源码中找到相关的接口(比如头文件中),我们称此为一个显示接口;假如这个类的某些成员函数时virtual的,那么该参数的调用将显现出运行期多态,也就是根据该参数的动态类型来决定究竟调用哪个函数。
  template及泛型编程的世界,与面向对象有根本上的不同。在此世界中显示接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口和编译期多态移到前面。在函数模板中会发生什么事呢:

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的类型T必须支持size、normalize和swap成员函数、copy构造函数、不等比较。这一组表达式便是T必须支持的一组隐式接口;凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功,这样的行为发生在编译期,这就是所谓的编译器多态。
  运行期多态和编译器多态之间的差异就像是“哪一个重载函数应该被调用”和"哪一个virtual函数该被绑定"之间的差异。显式接口和隐式接口的差异就比较新颖了。
  通常显式接口由函数的签名式(函数名称、参数类型、返回类型)组成;隐式接口就完全不同了,它并不基于函数签名式,而是由有效表达式组成,表达式可能看起来很复杂,但是他们要求的约束条件一般而言相当直接和明确,例如:
  if(w.size() > 10 && w != someNastyWidget)…
  if语句必须是个布尔表达式,所以无论w是什么类型,无论括号类的内容将导致什么,它都必须与bool兼容。
  在template中使用不支持template所要求的隐式接口的对象,将通不过编译。

了解typename的双重意义

. 首先说明一下在template声明式中,class和typename没有什么不同,不过typename还有特别的用处,那就是指定这个名称是个类型,比如有一下函数:

//作用是输出容器中的第二个元素
template<typename C>
void f(const C& container){
	if(container.size() > 2){
		C::const_iterator iter(container.begin());
		++iter;
		int value = *iter;
		std::cout<<value;
	}
}

. 其中变量iter的类型实际是什么取决于template参数C,这类名称被叫做从属名称,如果从属名称在class中呈嵌套状,可称之为嵌套从属名称,比如C::iterator就是这样一个名称;另一个变量value类型是int,其不依赖与template参数,这类名称称为非从属名称。
  嵌套从属名称可能导致解析困难,比如将上面的代码前部分改写为:

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

. 看起来好像是在声明一个指针,指向C::const_iterator,但是你这么想是因为你已经知道C::const_iterator是个类型,如果它不是呢?比如C::const_iterator是一个static成员变量,x是一个全局变量,这句话是不是就变成两者相乘的意思了。所以要告诉编译器它是一个类型的办法就是在紧邻它之前加上关键字typename:

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

. “typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename不可以出现在继承列表和初始化列表里。
. 在写一个编程时常见的例子:

template<typename T>
void f(T it){
	typename std::iterator_traits<T>::value_type temp(*it);
}

. iterator_traits接受一个原始指针,value_type表示这个指针所指的类型,假如T是vector<int>::iterator的话,temp的类型就是int的。所以前面那么长一串是类型,需要typename来指出。另外这么长的一句话多写几遍应该会很难受,所以常常将它与typedef一起使用:

template<typename T>
void f(T it){
	typedef typename std::iterator_traits<T>::value_type value_type;
	value_type temp(*it);
}

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

. 假设要编写一个程序,功能是传送信息到不同的公司,不同的公司对信息传送的要求可能不同,因此将由多个类来表示不同的公司的信息传递方式:

class CompanyA{
public:
	...
	void sendCleartext(const string& msg);
	void sendEntrypted(const  string& msg);
	...
};
class CompanyB{
public:
	...
	void sendCleartext(const string& msg);
	void sendEntrypted(const string& msg);
	...
};
...						//其他公司类

. 既然有这么多公司,并且传递方式不同,那完全可以使用模板的方式来使用它们自己的函数:

扫描二维码关注公众号,回复: 4555197 查看本文章
template<typename Company>
class MsgSender{
public:
	...
	void sendClear(const string& info){
		Company c;
		c.sendCleartext(info);
	}
	...
};

. 现在这么做没问题,但假设需要在发送信息前后加上日志信息,可以对派生类加上这样的功能:

template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
	...
	void sendClearMsg(const string& info){
		writetolog();				//传送前写入日志
		sendClear(info);	
		writetolog();				//传送后写入日志
	}
	...
};

. 看上去合情合理,但是上述代码通不过编译,编译器会抱怨senClear不存在,这是因为当编译器遭遇class template LogMsgSender 定义式时,并不知道它继承自什么样的class,虽然写出来的是MsgSender<Company>,但其中的Company是template参数,不到将它具现化的时候无法确切知道它是什么,也就不知道它是否有sendClear函数。
  更具体的原因是编译器知道基类的模板可能被特化,比如有一个公司没有sendCleartext这个函数,只有加密传送的函数:

class CompanyZ{
public:
	...
	void sendEntrypted(const  string& msg);
	...
};

. 这就需要对它产生一个特化的版本:

template<>					//该语法表示这既不是template也不是标准class
							//而是一个特化版的MsgSender template
class MsgSender<CompanyZ>{
public:
	...
	void sendSecret(const string& info){
		...
	}
	...
};

. 有了这样的特化版的情况存在,派生类中就可能产生错误,比如派生类中的sendClearMsg函数类的Company为CompanyZ。
  解决方法有三种:第一是在基类函数调用动作前加上this指针:

template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
	...
	void sendClearMsg(const string& info){
		writetolog();				//传送前写入日志
		this->sendClear(info);	
		writetolog();				//传送后写入日志
	}
	...
};

. 第二是使用using声明式:

template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
	using MsgSender<Company>::sendClear;
	...
	void sendClearMsg(const string& info){
		writetolog();				//传送前写入日志
		sendClear(info);	
		writetolog();				//传送后写入日志
	}
	...
};

. 第三种是明确指出被调用的函数位于基类中,但是这种方法不值得推荐,因为如果被调用的是虚函数,这样的明确指出将关闭virtual绑定行为:

template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
	...
	void sendClearMsg(const string& info){
		writetolog();				//传送前写入日志
		MsgSender<Company>::sendClear(info);	
		writetolog();				//传送后写入日志
	}
	...
};

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

. template编程节省时间和避免代码重复的一个好办法,但是template还是可能带来代码膨胀的:其二进制码带着几乎相同的代码、数据或是两者。其结果可能源码看起来很合身且整齐,但是目标码却不是那么回事。
  避免这种二进制浮夸的主要工具是:共性和变形分析。名字很好听,但其实就是分析相同的部分和不同的部分,将相同的部分抽出来使原先的class共同使用,不同的保留在原位置不变。
  比如,为固定尺寸的正方形矩阵编写template,该矩阵的一个特性是支持逆矩阵运算:

template<typename T,size_t n>			//n表示是n x n的矩阵
class SquareMatrix{
public:
	...
	void invert();						//求逆矩阵的函数
};

//具现化的时候
SquareMatrix<double,5> sm1;
sm1.invert();							//调用SquareMatrix<double,5>::invert
SquareMatrix<double,10> sm2;
sm2.invert();							//调用SquareMatrix<double,10>::invert


. template中除了有类型参数T,还有一个表示矩阵行列的非类型参数n,这样的方式在具现化的时候出现了两份invert,但这两个函数除了常量5和10,其他部分应该是全部相同的,这就是造成代码膨胀的一个典型例子。
  大部分时候我们的第一想法是为他们建立一个带数值参数的函数,通过无数值参数的函数来调用这个函数,而不重复代码:

template<typename T>
class SquareMatrixBase{
protected:
	...
	void invert(size_t n);
	...
};

template<typename T,size_t n>
class SqureMatrix:public SquareMatrixBase<T>{
private:
	using SquareMatrixBase<T>::invert;			//防止基类中的名称被遮掩
public:
	...
	void invert(){
		this->invert(n);						//调用基类的函数
	}
};

. 这样确实减少了代码重复的问题,但是还有一个问题:基类的invert函数怎么知道要操作的矩阵的数据?它只知道矩阵的尺寸,但是并不知道数据在哪里,想必只有派生类知道,所以需要在两者之间做一个联络工作。
  一个办法是在传一个指针参数给函数,但是如果要用到矩阵数据的函数很多的话,需要逐个添加,这样很不好。
  另一个办法是令基类存贮一个指针,指向矩阵数值所在的内存,这样在构造函数中可以获得应有的数据:

template<typename T>
class SquareMatrixBase{
protected:
	SquareMatrixBase(size_t n,T* pMem)
		:size(n),pData(pMem){}
	void setDataPtr(T* ptr){
		pData = ptr;
	}
	...
private:
	size_t size;
	T* pData;
};
//关于派生类,可以有两种方式来决定内存分配方式
//------------------版本一----------------------
//不需要动态分配,但是可能导致对象自身很大
template<typename T,size_t n>
class SqureMatrix:public SquareMatrixBase<T>{
public:
	SquareMatrix():SquareMatrixBase<T>(n,data){}
	...
private:
	T data[n*n];						//矩阵数据存贮在class内部
};

//===========版本二=============
template<typename T,size_t n>
class SqureMatrix:public SquareMatrixBase<T>{
public:
	SquareMatrix()							
		:SquareMatrixBase<T>(n,0)				//现将基类的数据指针置空
		,pData(new T[n*n]){						//将指向该内存的指针存储起来
			this->setDataPtr(pData.get())		//然后将它的一副本交给基类
		}
	...
private:
	boost::scoped_array<T> pData;		//指向数组的智能指针
};

. 虽然减少代码膨胀是好事,但有时候盲目的追求精密,事情可能变得更加复杂,所以有时候一点点代码重复反而看起来幸运了。
  其实类型参数也会导致膨胀,例如在很多平台上int和long有相同的二进制表述,所以想vector<int>和vector<long>的成员函数有可能完全相同,类似情况,在大多数平台上,所有指针类型都有相同的二进制表述。因类型参数造成的代码膨胀,往往可降低,做法是让带有相同二进制表述的具现类型共享实现码,比如成员函数操作强型指针(即T*)的情况,可以让他们调用另一个操作无类型指针(即使void*)的函数。

猜你喜欢

转载自blog.csdn.net/rest_in_peace/article/details/84344543
今日推荐