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

模板与泛型编程

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

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;
}
  • 关键还是那一句,隐式转换的核心是,表达式的有效性
  1. 类和模板都支持接口和多态
  2. 对类而言接口是显式的,以函数签名为中心,多态则是通过虚函数发生于运行期
  3. 对template 参数而言,接口是隐式的,奠基于有效表达式,多态则通过template 具现化和函数重新解析发生于编译期

了解 typename 的双重意义

  1. 当我们声明类型参数,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++ “不进入模板化基类观察”的行为失效,

  1. 在基类函数调用动作之前加上this->
  2. using 声明式
using MsgSender<Company>::sendClear;
  1. 明白指出被调用的函数位于基类内
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>的函数去。
代价:尺寸专享版本中,尺寸是个编译期常量,可能产生更优化的代码。
另外:代码共享可减小文件大小,降低程序工作集大小,强化指令高速缓存区内的引用集中化,可能执行的更快。

记住:

  1. Template 生成多个classes 和多个函数,所以任何template 代码都不该与某个造成膨胀的template 参数产生相依关系
  2. 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class 成员变量替换template 参数
  3. 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码<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 的迭代器分类
分两步

  1. 名为iterator_category 的typedef
template<...>
class deque{
public:
	class iterator {
	public:
		typedef random_access_iterator_tag iterator_category;
	}
};
  1. 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:

  1. 确认若干你希望将来可取得的类型相关信息。例如,对迭代器而言,我们希望将来可取得其分类(category)
  2. 为该信息选择一个名称(例如,iterator_category)
  3. 提供一个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 呢?

  1. 建立一组重载函数,或函数模板,彼此间的差异只在于各自的traits 参数。令每个参数实现码与其接受之traits 信息相应和。
  2. 建立一个控制函数,或函数模板,它调用上述那些劳工函数,并传递traits class 所提供的信息。

另外,还有很多种类的,Traits。

总结:

  1. Traits classes 使得”类型相关信息“在编译期可用,它们以templates 和 templates 特化完成实现。
  2. 整合重载技术后,traits classes 有可能在编译期间对类型执行if else 测试

认识 template 元编程

所谓template metaprogram (模板元程序)是以c++写成、执行于c++ 编译器内的程序,一旦TMP 程序结束执行,其输出,也就是从templates 具现出来的若干c++ 源码,便会一如往常地被编译。

能力:

  1. 有些事情变得容易,没有它,那些事情将很难实现,甚至实现不了
  2. 执行于编译期间,可将工作从运行期转移到编译期间

在这里插入图片描述

即使,这里的iter += d 不会执行,但,编译报错。

TMP 可以声明变量、执行循环、编写及调用函数。
在这里插入图片描述

三个示例用途:

  1. 确保量度单位正确
  2. 优化矩阵运算
  3. 可以生成客户定制之设计模式

注意:
4. template meta programming (TMP ,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率
5. TMP 可被用来生成”基于政策选择组合“的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

猜你喜欢

转载自blog.csdn.net/qq_18218335/article/details/84981651