Effective C++读书笔记(7)——模板与泛型编程

泛型编程写出来的代码与其所处理的对象类型彼此独立。
TMP是图灵完备的。

这部分很大一部分的东西我还是不太理解,更谈不上实际运用了。

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

面向对象是以显式接口和运行期多态解决问题

//某个无意义的类与函数
class Widget{
public:
	Widget();
	virtual ~Widget();
	virtual std::size_t size() const;
	...
};

//w的某些函数是virtual,显示出运行期多态
//w的类型被声明为Widget,w支持Widget的接口
void doProcessing(Widget& w){
	if(w.size() > 10 && w != someNastyWidget){
		...
	}
}

但是TMP及泛型编程的世界,隐式接口与编译期多态变得更加重要。

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构造函数,不等比较。(不完全正确,后面会有说明)

凡是涉及w的任何调用,如operate> 和 operate!=,有可能会造成template具现化,使这些调用得以成功。这些具现化发生在编译期。

隐式接口是不基于函数签名式,而是由有效表达式组成

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

看上去上面的T的隐式接口需要有这样的约束。
必须提供一个名为size的成员函数,函数返回一个整数值。
必须支持一个operate != 函数来比较两个T对西哪个。

但是操作符重载使的两个约束都不用满足。

size函数可以是继承于base class,甚至,这个函数不需要返回整数值。比如假设这个返回值应该是Y,但是他返回了X,只要有隐式转换可以将这个返回值X转换为Y(或者返回的值可以满足针对于Y的操作),就可以调用。

同样的道理,T并不需要支持operate !=,因为以下的情况也是可以实现的。operate != 可以接受一个X和一个Y,T可以转化成X,而someNastyWidget可以转化成Y。

对于template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template的具现化和函数重载解析,发生于编译期。

了解typename的双重含义

一般来说没有不同,但是有的时候你必须使用typename

//该代码不能编译!
template<typename C>
void print2nd(const C& container){
	if(container.size() >= 2){
	//C::const_iterator 是一个嵌套从属类型名称
		C::const_iterator iter(container.begin());
		++iter;
		int value = *iter;
		std::cout << value;
	}
}

编译器在template中遇到一个嵌套从属名称的时候先假设他不是一个类型,除非你告诉他是。

上述代码不能编译是因为iter的声明式是在C::const_iterator是一个类型的时候才是合理的。

所以一般性的规则是,在它之前放置一个typename即可

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

但是有一个例外。
即typename 不可以出现在base class list内的嵌套从属类型名称之前,也不可以在成员初值列中作为base class修饰符。

//不允许的示例
template<typename T>
class Derived: public Base<T>::Nested{
public:
	explicit Derived(int x):Base<T>::Nested(x)
	{
		typename Base<T>::Nested temp;	//不允许!!
		...
	}
};

typename只是用来验明嵌套从属类型的名称;其他的名称不允许有他的存在!

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

一个典型的typename的例子,真实程序的代表性的例子。

//function template,他接受一个迭代器,而我们要为这个迭代器做一个local副本temp
template<typename IterT>
void workWithIterator(IterT iter){
	//标准的trait class
	typename std::iterator_traits<IterT>::value_type temp(*iter);
	//如果你为了简化名字,你可以使用typedef
	typedef typename std::iterator_traits<IterT>::value_type value_type;
	value_type temp(*iter);
	...
}

但使用typename在移植上面会有一些问题。
主要是typename在不同的编译器上会有不同的实践,甚至有些旧的编译器根本就拒绝typename。

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

书中提到了一个例子,传递信息到不同的公司,如果在编译期间有足够的信息,就可以采用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;
		Company c;
		c.sendCleartext(msg);
	}
	void sendSecret(const MsgInfo& info)
	{
		...			//类似于sendClear,但是这里面调用的是sendEncrypted
	};
};

如果想在每次发送的过程中添加上一点信息的话,可以使用derived class

//下面代码无法通过编译
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
	void sendClearMsg(const MsgInfo& info){
		sendClear(info);				//调用base函数
	}
};

原因是:
在编译器遭遇到class template LoggingMsgSender的时候,并不知道他继承的是哪一种类型,虽然说是template <typename Company>,但是只有当LoggingMsgSender具现化了之后,才能知道。

//假设这个Z,Z坚持使用加密,但是Z没有sendCleartest函数
class CompanyZ{
public:
	...
	void snedEncryted(const std::string& msg);
	...
};
//此时对于一般的MsgSender template来说,不太适用,但是可以用特化版本的MsgSender template.
template<>
class MsgSender<CompanyZ>{
public:
	...			//删掉了sendClear
};

只有在template是CompanyZ的时候才能被使用,
这是模板的全特化,这个template是针对类型CompanyZ特化了,而且他的特化是全面性的,一旦类型参数被定义为CompanyZ,就没有其他的template参数可供变化。

回到上面那个无法编译的代码:

//下面代码无法通过编译
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
	void sendClearMsg(const MsgInfo& info){
		sendClear(info);				//如果Company == CompanyZ的时候,不存在sendClear
	}
};

C++拒绝这个调用的原因,就是考虑到base class 可能被特化,特化的版本会提供不同的接口。

所以在Template C++中,继承可能没有那么有效。

有三个办法可以解决:
1、在base函数调用前加上this->

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
	void senderClearMsg(const MsgInfo& info){
		this->sendClear(info); //假设sendClear被继承	
	}
};

2、使用using声明式

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
	using MsgSender<Company>::sendClear;		//告诉编译器,请他假设sendClear在base class内。
	void senderClearMsg(const MsgInfo& info){
		sendClear(info); 	
	}
};

3、明确指出被调用函数在base class中,但是如果被调用的函数是virtual函数的话,就会关闭virtual绑定的行为。

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
	void senderClearMsg(const MsgInfo& info){
		MsgSender<Company>::sendClear(info); 
	}
};

上述的三种做法都是给编译器一个承诺,任何的特特化版本都支持一般(泛化)版本所提供的接口

如果这个承诺没有被实践,编译器就不会通过。

LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
//无法通过编译,CompanyZ不提供sendClear,但是却是sendClear尝试调用的函数。
zMsgSender.sendClearMsg(msgData);

将参数无关的代码抽离templates

template是一个节省时间与避免代码重复的一个奇方妙法。
但是有的时候使用template可能会导致代码膨胀。

所以你要找出重复。

在non-template中,重复非常的明确,但是在template中,是相当的隐晦的。

//一个代码膨胀的典型例子,支持逆运算的正方形矩阵
template<typename T,std::size_t n>
class SquareMatrix{
public:
	...
	void invert();
};

//这会具现化两个invert,这两个函数除了常量5和10之外,其他的操作完全相同。
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();

第一次修改:

template<typename T>		//与尺寸无关的base class,用于正方形矩阵
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;		//避免遮盖base class中的invert
public:
	void invert(){ this->invert(n); }		//调用base class的invert
};

因为只是企图避免代码重复的一种方法,所以用protect替换public。调用它造成的额外成本是0,因为derived class是使用inline调用base class 版本的。使用this->是避免函数名称被掩盖。private继承表示的是base class帮助derived class实现,而不是表现is-a关系。

问题:
只有derived class知道操作的数据是什么,但是该如何联系base class进行逆运算操作呢?

一个办法是在SquareMatrixBase::invert添加另一个参数(可以是指针,指向矩阵内存的起始点)

但invert可能并不是唯一一个可写成形状与尺寸无关并可以移至SquareMatrixBase的SquareMatrix函数。

如果是有若干个这样的函数的话,我们对所有的这样的函数添加一个额外的参数,却得一次又一次地告诉SquareMatrixBase相同的信息,这样不太好。

另一个办法:
令base class存储一个指针指向矩阵数值和尺寸。

template<typename T>
class SquareMatrixBase{
protected:
	SquareMatrixBase(std::size_t n,T* pMen)
	:size(n),pData(pMen){ }				//储存矩阵大小和一个指针指向矩阵数值
	void setDataPtr(T* ptr){ pData = ptr; }		//重新赋值给pData
	...
private:
	std::size_t size;		//矩阵大小
	T* pData;				//指针,指向矩阵数值
};

也可以在derived class中存储矩阵数据

template<typename T,std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
	SqyareMatrix():SquareMatrixBase<T>(n,data){}	//送出矩阵大小和数据指针给base class
private:
	T data[n*n];
};

上述类型的对象不需要动态分配内存,但是对象自身可能是相当的大,还有一种做法就是可以将每个矩阵的数据放进heap中,就是说用new来动态分配内存

template<typename T,std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
	SquareMatrix(): SquareMatrixBase<T>(n,0),
	pData(new T[n*n])
	{ this->setDataPtr(pData.get()); }
private:
	boost::scoped_array<T> pData;
};

单纯从膨胀的角度来讲,许多的SquareMatrix成员函数以内联的方式调用基类,基类不论矩阵的大小,持有同类型元素的所有矩阵是共享。

但有代价。带有矩阵尺寸的invert版本比共享版本(尺寸由函数参数传递或存储在对象中)更佳的代码。带有尺寸的版本中尺寸是一个编译期常量,可以折进指令变成直接操作数。(我不太理解,但是说的应该就是,两个版本中调用尺寸这个参数的代价不一样)

另一个角度看,不同大小的矩阵只拥有单一的invert的话(不降低重复的话),可以减少执行文件的大小,降低程序的working set。

对于对象大小来说,详细请看书吧,好复杂。就是base class中的函数中含有与矩阵大小无关的函数版本。这会增加每个对象的大小。

运用成员函数模板接受所有兼容的类型

书上先提到了智能指针与指针的行为。
智能指针的行为像指针,并提供指针没有的一部分功能,但是有一点,指针可以很好的进行隐式转换。derived class 可以转换成 base class。在智能指针中实现比较麻烦。

class Top{..};
class Middle: public Top{...};
class Bottom: public Middle{...};
Top* pt1 = new Middle;
Top* pt2 = new Bottom;
const Top* pct2 = pt1;

我们希望以下代码通过编译(此处应该是探讨智能指针隐式转换的实现):

template<typename T>
class SmartPtr{
public:
	explicit SmartPtr(T* realPtr);
	...
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Middle>(new Bottom);
SmartPtr<const Top> pct2 = ptr1;

有一点很重要,不同的template具现化之间没有关系,没有关系,完全是不同的。

我们目的是根据SmartPtr<T1>对象,转化成一个SmartPtr<T2>对象。我们可以为SmartPtr写一个构造模板,用来为class生成函数。

template<typename T>
class SmartPtr{
public:
	template<typename U>
	SmartPtr(const SmartPtr<U>& other);
};

上面的这个构造函数是根据对象U来创建对象T的,正好符合我们的要求,这就是泛化的copy构造函数。

泛化的copy函数未声明explicit是蓄意的,因为原始指针的类型转化是隐式的。

但有个问题。

泛化的copy函数提供的比我们想要的更多。因为public继承的原因,所以我们希望derived class对象可以生成一个base class对象,但反过来不允许。我们也不想做SmartPtr<double>转化为SmartPtr<int>之类的东西。

我们可以在构造模板实现代码的约束转换行为:

//成员初值列来初始化SmartPtr<T>的成员变量
template<typename T>
class SmartPtr{
public:
	template<typename U>
	SmartPtr(const SmartPtr<U>& other): heldPtr( other.get())	//初始化this的heldPtr
	{...}
	T* get() const{ return  heldPtr; }
private:
	T* heldPtr;	//这个SmartPtr内置的原始指针
};

只有在存在一个隐式转换将U的指针转换成T的指针上述代码才能通过编译。

成员函数模板除了上述的功能,常扮演的另一个角色是,支持赋值操作。

比如说TR1的shared_ptr支持所有的来自兼容的内置指针、auto_ptrs等智能指针的构造行为,以及上述各物的赋值操作。(除了 weak_ptr)

template<class T>
class shared_ptr{
public:
	template<class Y>
	explicit shared_ptr(Y* p);	//构造来自任何兼容的内置指针

	//此处传递的r并没有声明为const,所以当你复制一个auto_ptr的时候,其实是被改动的了。
	template<class Y>
	explicit shared_ptr(auto_ptr<Y>& r);	//或是兼容auto_ptr
	
	//此处没有使用explicit说明shared_ptr到shared_ptr是可以进行隐式转换的。
	template<class Y>
	shared_ptr(shared_ptr<Y> const& r);		//或是shared_ptr
	...
};

关于成员函数模板,书上还提到了一点。如果在class内声明了一个泛化的构造函数的话,并没有声明正常的构造函数。编译器还是会为我们生成一个默认的构造函数。

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

现在我们面对的是template里的东西,原来class里面的有些东西不太适用。

在面向对象中,只有non-member函数才有能力在所有实参上施行隐式类型转化。

例子为条款24的例子

但如果进行了模板化的话:

//用于支持混合式运算
template<typename T> class Rational{
public:
	Rational(const T& numerator = 0, const T& denominator = 1);
	const T numerator() const;
	const T denominator() const;
	...
};

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

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

//template在实参推导过程中不将隐式类型转换函数考虑在内,所以说,int类型是不能转化成Rational<int>类型的。

我们可以声明一个friend函数,使其支持混合式调用。

template<typename T> class Rational{
public:
	//这里很有意思的一件事是,在class template中,template的名称可以作为template及参数的缩写。
	//比如这里的Rational,可以代表的是Rational<T>,但是很可惜,他代表的是Rational。这个好麻烦,我个人感觉。
	friend const Rational operator* (const Rational& lhs, const Rational& rhs);	//声明operator*
};
template<typename T>		//定义operator*
const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs)
{...}

很可惜,上述代码只能编译,无法连接。原因是没有定义friend的Rational。

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

…我对模板这章,很迷惑,看了挺多遍的,但还是有些不能理解。

只好说记住:
当我们编写一个class template,而他所提供之“与之template相关的”函数支持“所有参数之隐式类型转换”时,请把这些函数定义为class template内的friend函数。

使用traits class表现类型信息

Traits是一种技术,是C++程序员共同遵守的协议。这个技术要求之一是,对于内置类型和用户自定义类型的表现必须一样的好。

STL中算法与容器是独立设计的,迭代器的算法要通过trait class来从容器中取出容器的类型。

//比如说advance迭代器
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
	if(iter is a random access iterator)		//伪码,而且这个if语句不能执行,后面会说
	{
		iter += d;
	}
	else {
		if(d >= 0){while(d--) ++iter; }
		else{ while(d++) --iter;}
	}
}

trait信息必须位于类型自身之外,标准技术是把它放进一个template或及其一个或多个特化版本中。

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

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

OK,那么我们先前的伪码可以这样实现:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
	if(typeid(typename std::iterator_traits<IterT>::iterator_category) ==
	typeid(std::random_access_iterator_tag))		
	{...}
	
}

其中的typeid函数用来获取变量的类型
typeid函数

但是上面的语句是不能通过编译的,因为if语句是运行期进行判断,但template是编译期再进行判断,此时我们可以使用函数重载的功能,这个可以在编译期中进行类型的核定。

哪个重载的条件最匹配就调用哪个函数。

template<typename IterT, typename DistT>
void  doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{...}

template<typename IterT, typename DistT>
void  doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
{...}

此时,对于先前的代码,我们只是需要调用一个对象即可

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
	//用doAdvance替换先前的if else
	doAdvance(iter,d,tyname std::iterator_trait<IterT>::iterator_category());
}

所以设计实现一个trait class的话:
1、确认你想要取得的类型信息,如迭代器需要的是取得分类
2、为信息取一个名称,如iterator_category
3、提供一个temlate以及一组特化的版本,内含你所希望支持的类型相关信息,如iterator_trait

对于使用来说:
1、建立一组重载函数或是函数模板(如doAdvance),彼此之间差别为trait参数,令每个函数码与接受的trait信息相应和
2、建立一个控制函数或函数模板(如advance),调用上述的重载函数或是模板来传递信息

认识template元编程

主要是讲TMP的一些作用,他说TMP可以来用作编译期计算的功能,可将工作移动到编译期间,进而实现早期错误侦察与更高的执行效率。

可以用来生成机遇政策选择的客户定制代码,避免生成一些对特殊类型不合适的代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值