模板与泛型编程
了解隐式接口和编译期多态
w 必须支持哪一种接口,由template 中执行于w 身上的操作来决定的。
- 这里,是,size(),normalize 和 swap、copy构造函数、不等比较
凡涉及w 的任何函数调用,例如operator > 和 operator != ,有可能造成template 具现化,使这些调用得以成功,这样的具现行为发生在编译器。“以不同的template 参数具现化function templates”导致调用不同的函数,这便是所谓的编译期多态
显式接口和隐式接口的区别
-
显式接口由函数的签名式(函数名称,参数类型、返回类型)构成
-
隐式接口,由有效表达式组成
- 传入的类型,必须使模板中的表达式有效,无论是直接的支持,还是中间经过了多少层的封装与隐式转换
- 由于操作符重载,这里的 > 和 != 操作符,由很多可能,比如,只要 > 两边的数据类型能够在隐式转换之后相互比较就可以,示例代码如下,其实真正的代码有更多的可能性
class intA {
public:
intA(int iP) :m_iV(iP) { cout << "i'm intA" << endl; }
int getV()const { return m_iV; }
private:
int m_iV;
};
class intB
{
public:
intB(int iP) :m_iV(iP) { cout << "i'm intB" << endl; }
~intB() {}
int getV() { return m_iV; }
bool operator > (const intA& cA) const { return m_iV > cA.getV(); }
private:
int m_iV;
};
class Widget {
public:
Widget() {}
virtual ~Widget() {};
virtual intB size() const { return intB(sizeof(*this)); }
virtual void normalize(){}
void swap(Widget& other){}
bool operator !=(int a) { return true; }
};
int someNastyWidget = 0;
template <typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
int main()
{
Widget wTemp;
doProcessing(wTemp);
getchar();
getchar();
return 0;
}
- 关键还是那一句,隐式转换的核心是,表达式的有效性
- 类和模板都支持接口和多态
- 对类而言接口是显式的,以函数签名为中心,多态则是通过虚函数发生于运行期
- 对template 参数而言,接口是隐式的,奠基于有效表达式,多态则通过template 具现化和函数重新解析发生于编译期
了解 typename 的双重意义
- 当我们声明类型参数,class 和 typename 没有什么不同。
什么时候不同呢?看如下代码:
template <typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
参照第一条,我们这里的,C 总的来说是,STL 容器兼容的,size() 表示它的元素数量,C::const_iterator 表达它的一个迭代器,另外,我们发现它可以转换为int 类型,因为后面有*iter操作。
这里比较特别的就是,iter 和 value 了。首先,iter 的类型是模板相关的,它有一个专业名称:如果template 中出现的名称相依于某个template 参数,称之为从属名称(dependent names)。如果从属名称在class 内呈嵌套装,我们称之为,嵌套从属名称(nested dependent 那么)。C::const_iterator 就是这样一个名称。实际上它还是一个嵌套从属类型名称(nested dependent type name,也就是个嵌套从属名称并且指涉某类型。
value 没有上述属性,因此,它是,非从属名称(non-dependent names)。
上述代码中,编译器完全不知道C 是什么,书中举例说,C 完全可能是一个类名,且const_iterator 是其中一个静态变量,编译器不会想当然的认为C 就是一个类型。它的默认做法是如果解析器在template 中遭遇一个嵌套从属名称,它便假设这个名称不是一个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型,此规则有一个例外,下面介绍
如何告诉它是?
template <typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
//...
}
}
如上,使用typename 声明即可
typename 只被用来验明嵌套从属类型名称,其它名称不该有它存在:
template <typename C> // 这里使用"typename (或class)
void f(const C& container, // 不允许使用typename
typename C::iterator iter);// 一定要用typename
例外:
typename 不可以出现在base classes list 内的嵌套从属类型名称之前,也不可在member initialization list 中作为base class 修饰符
template<typename IterT>
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;// value_type 即是IterT 中的数据的类型
value_type temp(*iter);
}
学习处理模板化基类内的名称
class CA
{
public:
CA() {}
~CA() {}
void sendC(const std::string& msg) {}
void sendE(const std::string& msg) {}
private:
};
class CB
{
public:
CB() {}
~CB() {}
void sendC(const std::string& msg) {}
void sendE(const std::string& msg) {}
private:
};
class MsgInfo{...};// 这个类用来保存信息,以备将来产生信息
template <typename Company>
class MsgSender
{
public:
void sendClear(const MsgInfo& info)
{
std::string msg;
// 在这里根据info 产生信息
Company c;
c.sendC(msg);
}
void sendSecret(const MsgInfo & info){/*唯一的不同,调用sendE*/}
private:
};
现在,加入,有子类,想在M’s’gSender 基础上,在消息发送的前后添加日志信息:
template <typename C>
class LoggingMsgSender: public MsgSender<C> {
public:
void sendClearMsg(const MsgInfo& info) {
// 写日志
sendClear(info);
// 写日志
}
}
但,此时,编译不通过。sendClear 不认识,为什么会出现这种情况?
当编译器遭遇class template LoggingMsgSender ,并不知道它继承什么样的class,当然它继承的是
MsgSender<Company>
但其中的Company 是个template 参数,不到后来具现化的时候无法确切的知道它是什么。如果不知道company 是什么,就无法知道
class MsgSender<Company>
像什么,更明确的说,没办法知道它是否有一个sendClear 函数。子类往往拒绝在模板化基类内寻找继承而来的名称《父类的特化版本可能与继承的名称不兼容》。
当然,我们有三种方法令C++ “不进入模板化基类观察”的行为失效,
- 在基类函数调用动作之前加上this->
- using 声明式
using MsgSender<Company>::sendClear;
- 明白指出被调用的函数位于基类内
MsgSender<Company>::sendClear(info);
这种做法,如果被调用的是虚函数,上述的明确资格修饰会关闭“virtual 绑定行为”。
上述三种办法都做了一件事:对编译器承诺,基类模板的任何特化版本都将支持其一般化(泛化)版本所提供的接口“。
当然,如果有个特化版本的基类就是不支持,在调用LoggingMsgSender::sendClearMsg时将报错。//且是编译期间报错
将与参数无关的代码抽离template
template 是节省时间和避免代码重复的一个好方法。但,如果不小心,使用template 可能会导致代码膨胀,其二进制码带着重复的代码、数据,或两者,结果是,产生的二进制文件可能很大。
处理:共性与变性分析,将重复的代码抽离,放到公共函数。
编写tempalte 时,做相同的分析,但,template 中,重复是隐晦的;如下:
template<typename T,std::size_t n>
class SM{
public:
//...
void invert();// 求逆矩阵
}
SM<double,5> sm1;
sm1.invert();
SM<double,10> sm2;
sm2.invert();
这回具现化两份invert,这些函数并非完全相同,但除了常量5 和 10,两个函数的其它部分都相同。
方法:
建立 一个带数值参数的函数,然后以5 和 10 来调用这个带参数的函数,而不重复代码:
template<typename T>
class SMBase{
....
SMBase(std::size_t n,T* pMem):size(n),pData(pMem){}
void setDataPtr(T*ptr){pData=ptr;}
void invert(std::size_t mSize);
}
template<typename T,std::size_t n>
class SM:private SMBase<T>{
SM():SMBase<T>(n,o),pData(new T[n*n]{this->setDataPtr(pData.get());}
private using SMBase<T>::invert;// 避免掩盖
public:void invert(){this->invert(n);}
boost::scoped_array<T> pData;
}
SM 成员函数可以单纯的以inline 方式调用base class 版本,后者由”持有同类型元素”之所有矩阵共享。同时,不同大小的SM 对象有不同的类型,所以使SM<int,5>与SM<int,10>)对象使用相同的SMBase< int >成员函数,我们也没机会投递一个SM<int,5>对象到一个期望获得SM<int,10>的函数去。
代价:尺寸专享版本中,尺寸是个编译期常量,可能产生更优化的代码。
另外:代码共享可减小文件大小,降低程序工作集大小,强化指令高速缓存区内的引用集中化,可能执行的更快。
记住:
- Template 生成多个classes 和多个函数,所以任何template 代码都不该与某个造成膨胀的template 参数产生相依关系
- 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class 成员变量替换template 参数
- 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码<int与long>,所有指针类型.
运用成员函数模板接受所有兼容类型
真实指针做的很好的一件事是:支持隐式转换,派生类指针可以隐式转换为基类指针,指向non-const 对象的指针可以转换为指向const 对象,等等。
多重继承体系中,如果想让指针指针在复合要求的前提下,自动的支持,这种隐式的类型转换,我们需要做一些工作:
template<typename T>
class SmartPtr{
public:
template<typename U>// 成员模板
SmartPtr(const SmartPtr<U>& other);// 为了生成copy 构造函数
以上代码的意思是:对任何类型T 和任何类型U ,这里可以根据SmartPtr< U >生成一个SmartPtr< T > ,而U 和 T 的类型是同一个template 的不同具现体,有时我们称之为泛化copy 构造函数。
声明搞定了之后,我们需要添加规则,因为这种转换并不是万能的,必须符合继承规则:
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U> & ohter) :heldPtr(other.get()){}
T* get() const {return heldPtr;}
private:
T* heldPtr;
这个行为,只有当“存在某个隐式转换可将一个U* 指针转换为一个T* 指针”时才能通过编译,这正是我们想要的。最终效益是Smart< T > 现在有了一个泛化copy 构造函数,这个构造函数只在其所获得的实参隶属适当(兼容)类型时才通过编译。
当T 和 U 类型相同,泛化copy 构造函数会被具现化为”正常的“copy 构造函数。那么究竟编译器会暗自生成一个copy 构造函数,还是具现化一个”泛化copy 构造函数模板“?
member templates 并不改变语言规则,如果程序需要一个copy 构造函数,你却没有声明它,编译器会为你暗自生成一个。在class 内声明泛化copy 构造函数并不会阻止编译器生成它们自己的copy 构造函数,所以如果你想要控制copy 构造函数的方方面面,必须同时声明泛化copy 构造函数和”正常的“copy 构造函数。相同规则也适用于赋值操作。
- 使用member function templates (成员函数模板)生成“可接受所有兼容类型”的函数
- 如果你声明member templates 用于“泛化copy 构造”或“泛化assignment 操作”,你还是需要声明正常的copy 构造函数和copy assignment 操作符。
需要类型转换时请为模板定义非成员函数
// 为了让下面的代码合法
Rational oneHalf(1,2);
Rational result = oneHalf * 2;
template<typename T>
class Rational {
public:
//...
friend const Rational operator* (const Rational& lhs,const Rational& rhs);
};
template<typename T>
const Rational<T> operator*(const Rational<T> & lhs,const Rational<T>& rhs)
{...}
}
我们将,operator* 声明为友元函数。因为:,当不是,编译器不知道我们想要调用哪个函数,它们试图想出什么函数被命名为operator* 的template 具现化出现。它们知道它们应该可以具现化某个“名为 operator* 并接受两个Rational< T > 参数“的函数,但它们无法知道T 的类型。
为了得到T 的类型,它首先观察,operator * 两边的类型,第一个类型,Rational < T > ,T 为int,但第二个,类型为int。编译器在template 实参推导过程中从不将隐式类型转换函数纳入考虑。绝不。这样的转换在函数调用过程中的确被使用,但在能够调用一个函数之前,首先必须知道这个函数存在,而为了知道它,必须先为相关的function template 推导出参数类型(然后才可将适当的函数具现出来)。然而,template 实参推导过程中并不考虑采纳”通过构造函数而发生的“隐式类型转换。
但,上面使用了,friend后,class Rational< T > 可声明operator * 是它的一个friend 函数。class templates 并不依赖template 实参推导,所以编译器总是能够在class Rational< T > 具现化时得到T。因此,令Rational < T > class 声明适当的operator* 为其friend 函数,可简化整个问题
使用 traits classes 表现类型信息
五类迭代器分类:
input 只能向前,一次一步,客户只可读取,只能读取一次
output 与input 类似,写入
forward input 和 output 的叠加
Bidirectional 可以向前和向后
random access,随机访问
对于这5种分类,C++ 标准库分别提供专属的tag struct:
strcut 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{};
上面有明显的继承关系,下面看advance,其,将迭代器移动指定的位置(正/负)
template<typename IterT,typename DistT>
void advance(IterT& iter,DistT d)
{
if (iter is a random access iterator) {
iter += d;
} else {
if (d > 0) {while (d--) ++ iter;}
else { while(d++) --iter;}
}
}
问题:
这里刚开始的判断,是,执行时,还是编译器?
traits 可以做到。
Traits 是一种技术,一个C++ 程序员共同遵守的协议,这个技术的要求之一,对内置类型和用户自定义类型的表现必须一样好。
traits 必须能够施行于内置类型,意味着,类型内的嵌套信息,这种东西出局了,因为我们无法将信息嵌套 于原始指针内。因此类型的traits 信息必须位于类型自身之外。标准技术是把它放进一个template 及其一个或多个特化版本中。这样的templates 在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits:
template<typename IterT>
struct iterator_traits;
原理:
针对每个类型IterT,在struct iterator_traits< IterT >内一定声明某个typedef 名为iterator_category,这个typedef 用来确认IterT 的迭代器分类
分两步
- 名为iterator_category 的typedef
template<...>
class deque{
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
}
};
- iterator_traits,只是鹦鹉学舌般响应iterator class的嵌套式typedef:
template<typename IterT>
struct iterator_traits{
typedef typename IterT::iterator_categoty iterator_category;
...
}
这对用户自定义类型行得通,但对指针行不通,因为指针不可能嵌套typedef。iterator_traits第二部分,专门用来对付指针。
为了支持指针迭代器,iterator_traits 特别针对指针类型提供一个特偏化版本。由于指针的行径与random access 迭代器类似,所以iterator_traits 为指针指定的迭代器类型是:
template<typename IterT>
struct iterator_traits<IterT*>
{ typedef random_access_iterator_tag iterator_categoy;}
设计并实现一个traits class:
- 确认若干你希望将来可取得的类型相关信息。例如,对迭代器而言,我们希望将来可取得其分类(category)
- 为该信息选择一个名称(例如,iterator_category)
- 提供一个template 和一组特化版本,内含你希望支持的类型相关信息。
最开始的,if 判断,还是将操作放到了运行期,其实我们有更好的,编译期间的方法,如下:
template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::random_access_iterator_tag)
{
iter +=d;
}
template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::bidirectional_iterator_tag)
{
if (d >= 0) {while (d--) ++iter;}
else {while(d++) --iter;}
}
//等等
之后,advance中,直接根据类型,调用这里的doAdvance,利用多态进行编译期间的选择就好。
实际测试如下:
int main()
{
std::list<int> temp = { 1,2,3,4,5,6,7 };
auto itor = temp.begin();
advance(itor, 5);
getchar();
getchar();
return 0;
}
template<class _InIt,
class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off)
{ // increment iterator by offset, arbitrary iterators
// we remove_const_t before _Iter_cat_t for better diagnostics if the user passes an iterator that is const
_Advance1(_Where, _Off, _Iter_cat_t<remove_const_t<_InIt>>());
}
而_iter_cat_t 为:
template<class _Iter>
using _Iter_cat_t = typename iterator_traits<_Iter>::iterator_category;
我们如何使用一个traits class 呢?
- 建立一组重载函数,或函数模板,彼此间的差异只在于各自的traits 参数。令每个参数实现码与其接受之traits 信息相应和。
- 建立一个控制函数,或函数模板,它调用上述那些劳工函数,并传递traits class 所提供的信息。
另外,还有很多种类的,Traits。
总结:
- Traits classes 使得”类型相关信息“在编译期可用,它们以templates 和 templates 特化完成实现。
- 整合重载技术后,traits classes 有可能在编译期间对类型执行if else 测试
认识 template 元编程
所谓template metaprogram (模板元程序)是以c++写成、执行于c++ 编译器内的程序,一旦TMP 程序结束执行,其输出,也就是从templates 具现出来的若干c++ 源码,便会一如往常地被编译。
能力:
- 有些事情变得容易,没有它,那些事情将很难实现,甚至实现不了
- 执行于编译期间,可将工作从运行期转移到编译期间
即使,这里的iter += d 不会执行,但,编译报错。
TMP 可以声明变量、执行循环、编写及调用函数。
三个示例用途:
- 确保量度单位正确
- 优化矩阵运算
- 可以生成客户定制之设计模式
注意:
4. template meta programming (TMP ,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率
5. TMP 可被用来生成”基于政策选择组合“的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。