模板

Posted 小键233

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模板相关的知识,希望对你有一定的参考价值。

隐式接口与编译器多态

假如有如下的模板函数:

template<typename T>
void doSomething(T& t)

    if(t.getIs() )
    
        t.traver();
    

那么,对于T 而言,它的隐式接口便是:

bool getIs();
traver();

T 的类型必须要支持这两个接口才能通过编译。

而编译器多态是:

以不同的template 参数具现化function templates ,会导致调用不同的函数

关于typename 的含义

在template 的声明式中,经常有看到class 代替typename 的情况

template<class T> SomeClass;
template<typename T> SomeClass;

其实这是因为历史原因,一开始是让用class 的,但是后来为了把class 归化到类的声明中,而引入了新的关键字typename。
除了多打了几个字符,它们并没有什么分别。

然而,typename 存在的意义并不只是做一点原来class 能做的事情。

考虑有这样的代码:


template<typename T>
void doSomething(T& t)

    T::some_type * x;  //1
    //...

现在问题来了,怎么解释注释1 的那句。
如果some_type 是一种类型,那么x 就是一个指针。
如果some_type 是一个变量(T 中的静态变量,或者其他),那么这句就变成了some_type 乘以x了。

那么在C++中,有个规则解析这一中歧义的状态:

如果解析器在template 中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。[1]

所以,对于上述的语句,会被解释为some_type 乘以x。
如果想声明的话,那么应该在嵌套从属名称之前放置typename:

template<typename T>
void doSomething(T& t)

    typename T::some_type * x;  //1
    //...

它告诉编译器,这是个类型!这就是typename 的第二个含义了。

但是“typename 必须作为嵌套从属类型名称的前缀词”这一规则有个例外:

typename 不可以出现在base classed list 内嵌套从属类型名称之前,也不可在member initialization 中作为 base class 修饰词。
比如:

template<T>
class SomeClass: Base<T>::Nested

public:
    SomeClass(int x): Base<T>::Nested(x)
    
        ...
    
;

模板类成为基类时

看下面的代码

class Base

public:
    void doSomething()
    
    
    virtual ~Base() 
;

class Derived: public Base

public:
    void func()
    
        doSomething();
    
;

如果有使用:

    Derived d;
    d.func();

ok。这个表现得很好,程序能够编译和运行。
现在我们要给它们加上模板了。

template<typename T>
class Base

public:
    virtual ~Base() 
    void doSomething()
    
    
;

template<typename T>
class Derived: public Base<T>

public:
    void func()
    
        doSomething();
    
;

除了加上模板,并没有什么不同的。
但是,当这样的时候:

    Derived<int> d;
    d.func();

ok,它挂了,它报错了,它变了,它以前不是这样的!

为什么呢?
在回答这个问题之前,我们不妨给Base模板类写个全特化的版本:

struct Special
;

template<>
class Base<Special>

public:
    virtual ~Base() 
    //Special 的特化版本中没有dosomething 的函数
;

这完全没有问题,也能通过编译,这时候,假如有下面的使用:

Derived<int> d;  //这个有api  doSomething
Derived<Special> ds;//这个没有api

看明白了吗,如果模板类的Base 特化以后,它的行为可能会和泛化的完全不一样,那么在继承了模板类的Derived Class 中,不问结果地调用基类的接口,可能会出错。

这就是为什么编译出错的原因:

它知道base template class 有可能会特化,而那个特化版本可能不提供和一般性的template 相同的接口。因此,它往往拒绝在templatized base class (模板化基类)内寻找继承而来的名称。[1]

直白点,就是说,当继承模板化基类时,它的派生类会假装我并不知道基类的接口是什么

如果我确实是想调用一般化的基类的接口,那么有两种方法

  • 加this 指针调用
  • 使用using

对于代码如下:

template<typename T>
class Derived: public Base<T>

public:
    void func()
    
        using Base<T>::doSomething(); //两种选一种就可以了
        this->doSomething();
    
;

模板类型转换

假设我们现在写一个封装类型的类:

template<typename T>
class Type

    T i;
public:
    Type(T ini) : i(ini) 
    Type doOpe (const Type& b) const
    
        return i*b.i;
    
;

template<typename T>
Type<T> operator * (const Type<T>& a, const Type<T>& b)

    return a.doOpe(b);

我们希望能够做乘法运算,于是重载了操作符。
那么,假如现在这么使用:

    Type<int> a(3);
    Type<int> b(2);
    Type<int> c = a*b;
    Type<int> d = a*4;  //不通过

你会发现其中d 不会被编译通过。
但是在使用的过程中,我们是希望这句能够通过的,因为我们将int 封装为Type ,自然希望这样的乘法能够通过。

不能通过是因为:

template实参推导过程中从不将隐式类型转换函数纳入考虑。[1]

也就是说,必须这样显示地使用才会让你通过编译:

    Type<int> d = ::operator*<int>(a,4);

但是,这样使用未免不够直观,所以,我们可以把operator × 操作符声明为Type 的友元函数。这样,当具现化Type 时,友元函数自然也会被推导出来,于是编译器找到合适的函数,运行下去:

template<typename T>
class Type

    T i;
public:
    Type(T ini) : i(ini) 
    friend Type operator * (const Type& a, const Type& b)
    
        return a.i*b.i;
    
;

*有些编译器强制要求模板类的定义和实现放在一起。

事已至此,皆大欢喜。

[参考资料]
[1] Scott Meyers 著, 侯捷译. Effective C++ 中文版: 改善程序技术与设计思维的 55 个有效做法[M]. 电子工业出版社, 2011.
(条款41:了解隐式接口和编译器多态;
条款42:了解typename 的双重含义;
条款43:学习处理模板化基类内的名称;
条款46:需要类型转换时请为模板定义非成员函数)

以上是关于模板的主要内容,如果未能解决你的问题,请参考以下文章

Effective C++学习笔记

评估条款开头的表达式

在 Cognito 托管 UI 中添加指向条款和条件的链接

条款03:尽可能使用const

条款03:尽可能使用const

Effective C++ 条款45