读书笔记(7)——模板与泛型编程"/>
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 classtypename std::iterator_traits<IterT>::value_type temp(*iter);//如果你为了简化名字,你可以使用typedeftypedef 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 elsedoAdvance(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可以来用作编译期计算的功能,可将工作移动到编译期间,进而实现早期错误侦察与更高的执行效率。
可以用来生成机遇政策选择的客户定制代码,避免生成一些对特殊类型不合适的代码。
更多推荐
Effective C++读书笔记(7)——模板与泛型编程
发布评论