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

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

请使用traits classes表现类型信息

. traits并不是C++的关键字或是预先定义好的构件,它们是一种技术,也是一个C++程序员共同遵守的协议。这项技术的要求之一是:它对内置类型和用户自定义类型的表现必须一样好,即traits技术能够施行于用户自定义类型,也能施行于内置类型如指针身上。
  “traits必须能施行于内置类型”意味着“类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原始指针内(PS:听说C++20之后要删除原始指针了)。因此类型的traits信息必须位于类型自身外。标准技术是将它放进一个template以及其一个或多个特化版本中。这样的template在标准库中有若干个,其中针对迭代器的被命名为iterator_traits:

template<typename IterT>			//template,用来
struct iterator_traits;			//处理迭代器分类的相关信息

. 这里iterator_traits是一个struct,实际上traits总是被实现化struct,但是往往被称为traits classes。
  那这种技术所指的分类的相关信息是什么呢?举个例子,在STL中迭代器有5种分类:
  -input迭代器:只能向前移动,一次一步,客户只可读取它们所指的内容,而且只能读取一次,例如C++程序库中的istream_iterators;
  -output迭代器:只能向前移动,一次一步,客户只可涂写它们所指的内容,而且只能涂写一次,例如C++程序库中的ostream_iterators;
  -forward迭代器:这种迭代器可以做上述两种分类所能做的每一件事,而且可以读或写其所指物一次以上;
  -bidirectional迭代器:这种迭代器比上一个分类威力更大:它除了可以向前移动,还可以向后移动。例如STL的list迭代器、set、map的迭代器等;
  -random access迭代器:这种迭代器比上一个迭代器的威力更大的地方在于它可以执行“迭代器算术”,也就是它可以在常量时间里向前或向后跳跃任意距离。例如vector、deque和string的迭代器。
  对于这五种分类,C++标准库分别提供专属的卷标结构加以确认:

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{};

其中有的能实现随机访问,有的不能,因此要将迭代器进行移动多个单位的时候,有的只需要使用一次+=,有的只能通过多次++来实现,在函数模板中,traits便是通过判断迭代器类型给出不同的方法。
  对于这五种分类,C++标准库分别提供专属的卷标结构加以确认:
  iterator_traits的运作方式是,针对每个类型的IterT,在struct iterator_traits<IterT>内一定声明某个typedef为iterator_category。就用这个typedef来确认IterT的迭代器分类。
  iterator_traits以两个部分实现上述所言。首先它要求每一个“用户自定义的迭代器类型”必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构。例如deque的迭代器可以随机访问,所以一个针对deque的迭代器设计的class看起来如下:

template<...>			//省略没写template参数
class deque{
public:
	class iterator{
	public:
		typedef random_access_iterator_tag iterator_category;
		...
	};
	...
};

. 置于iterator_traits,只是鹦鹉学舌般的相应typedef:

template<typename IterT>
struct iterator_traits{
	//IterT说它自己是什么,就相信是什么
	typedef typename IterT::iterator_categroy iterator_category;
	...
};

. 这对用户自定义类型行的通,但是对指针(也是一种迭代器)不行,因为指针不能嵌套typedef。因此iterator_tratis的第二部分专门用来对付指针,它提供一个偏特化的版本,由于指针的行径与random access迭代器类似,所以为指针指定的迭代器类型是:

template<typename IterT>
struct iterator_traits<IterT*>{
	typedef random_access_iterator_tag iterator_category;
	...
};

. 在有了iterator_traits后,你可能在判断迭代器类型时使用if条件判断:

//IterT是一个模板参数
if(typeid(typename std::iterator_traits<IterT>::iterator_category)
	== typeid(std::random_access_iterator_tag))
	...

. 看上去没毛病,但这并不是我们想要的,首先这会导致编译问题,还有一个更根本的问题是:IterT类型在编译期间就获知,所以iterator_traits<IterT>::iterator_category也可以在编译期间获知,但是if语句却在运行期才会核定。为什么编译期完成的事要延迟到运行期才做呢?那样不仅浪费时间,也可能造成可执行文件膨胀。
  取而代之的一种方法是用到C++的重载。当重载一个函数时,必须详细的叙述各个重载版本的参数类型。当调用函数时,编译器就根据传来的实参选择最合适的重载版本。这正是针对类型所发生的“编译期条件语句”。因此针对五种类型的迭代器所实现不同版本的函数可以如下编写:

template <typename IterT,typename DistT>			//用于random access迭代器
void doAdvance(IterT& iter,DistT& d,std::random_access_iterator_tag){
	iter += d;
}
template <typename IterT,typename DistT>			//用于bidirectional迭代器
void doAdvance(IterT& iter,DistT& d,std::bidirectional_iterator_tag){
	if(d > 0){
		while(d--){
			++iter;
		}
	}
	else{
		while(d++){
			--iter;
		}
	}
}
template <typename IterT,typename DistT>			//用于input迭代器
void doAdvance(IterT& iter,DistT& d,std::input_iterator_tag){
	if(d<0){
		throw std::out_of_range("不能是负数");
	}
	while(d--){
		++iter;
	}
}

. 由于forward_iterator_tag继承自input_iterator_tag,所以最后那个版本也能处理forward迭代器。有了这些重载版本后,调用这些函数时只需要额外传递一个对象,后者必须带有适当的迭代器分类,例如:

template <typename IterT,typename DistT>
void advance(ItertT& iter,DistT d){
	doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
}

. 现在可以总结如何使用一个traits class了:
  1.建立一组重载函数或函数模板(如doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受的traits信息相应;
  2.建立一个控制函数或函数模板(如advance),它调用上述那些函数并传递traits class所提供的信息。

认识模板元编程

. Template metaprograming(TMP,模板元编程)是编写template-based C++程序并执行于编译期的过程。所谓模板元程序是以C++编写,执行于编译器内的程序。一旦TMP程序结束执行,其输出(也就是从template具现出来的若干C++源码)便会一如往常的被编译。
  TMP有两个伟大的作用。第一:它使得某些事情变得更加简单;第二:由于TMP执行于C++编译期,因此可将工作从运行期转换至编译期,这导致的一个结果是某些错误可以更早的被发现,但是编译的时间将会变得更加长,另一个结果是使得程序更加高效:较小的可执行文件、较短的运行期、较少的内存需求。
  上一个条款中的advance函数中介绍了两种方法来判断迭代器的种类:通过typeid的方式和重载的方式。推荐的是后者,其效率会更加高,因为那就是TMP。另外typeid的方式将会带来编译的错误,以下就是例子:

std::list<int>::iterator iter;
...
advance(iter,10);					//移动iter向前走10个元素
									//上述实现无法通过编译

//针对以上的调用,产生的template版本如下
void advance(std::list<int>::iterator& iter,int n){
	if(typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_category)
	== typeid(std::random_access_iterator_tag)){
		iter += n;								//错误
	}
	else{
		if(d > 0){
			while(d--){
				++iter;
			}
		}
		else{
			while(d++){
				--iter;
			}
		}
	}
}

. 问题出在使用了+=操作符的那行代码,那尝试在list<int>::iterator上使用+=,但是它并不支持,只有random access迭代器才支持+=,此刻我们知道绝不会执行+=那一行,因为typeid那一行总是会因为list<int>::iterator而失败,但是编译器必须确保所有的源码都有效,纵使是不会执行的代码。
  TMP已被证明是个强大到足以计算任何事物的机器。使用TMP你可以声明变量、执行循环、编写及调用函数…但这般构件相对于“正常的”C++对应物看起来很是不同。针对TMP设计的程序库(如Boost‘s MPL)提供更高层级的语法——尽管目前还是不足以让你误以为是“正常的”C++。
  比如在TMP中是怎么实现循环的呢?TMP没有真正的循环构件,所有循环效果是由递归来实现的,TMP主要是个“函数式语言”(我想起学了一周的erlang)。TMP的递归甚至不是正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”。
  像学习hello world一样看看TMP的起手程序——在编译期计算阶乘:

template<unsigned n>
struct Factorial{
	enum { value = n * Factorial<n-1>::value };
};
template<>							//特化
struct Factorial<0>{
	enum {value = 1 };
};

//你可以这样使用
int main(){
	std::cout<< Factorial<5>::value<<endl;
}

. 当然TMP还能做更多更有用处的事情,下面举三个例子:
  1.确保度量单位的正确。在科学和工程应用程序中,确保度量单位的正确结合是很重要的事情,比如将一个质量变量赋值给一个速度变量时不对的,但是可以将一个距离变量除以一个时间变量后的结果赋值给一个速度变量。如果使用TMP,就能确保在编译期程序中所有度量单位的组合都正确,不论其计算多么复杂;
  2.优化矩阵运算(唉!不懂)。假设有多个矩阵,求它们相乘的结果,如:
 Martrix<double,1000> m1,m2,m3,m4,m5;
 …
 Martrix<double,1000> = m1m2m3m4m5;
  以“正常的”函数调用动作来计算,会创建4个暂时性矩阵,每一个用来存储对operator*的调用结果,并且各自的乘法产生4个作用于矩阵元素身上的循环。如果使用高级、与TMP相关的template技术,即所谓expression template,就能消除那些临时变量并合并循环。于是TMP软件使用较少的内存,却又提高了执行速度。
  3.可以实现客户定制之设计模式实现品。设计模式如Strategy,Observer等都可以使用多种方式实现出来。运用TMP-based技术,有可能产生一些template用来表述独立的设计选项,然后可以任意结合它们,导致模式实现品带着客户定制的行为。

猜你喜欢

转载自blog.csdn.net/rest_in_peace/article/details/84653221