您的位置:首页 > 编程语言 > C语言/C++

C++模板阶段性小结

2016-08-20 15:02 295 查看
以前在学校C++模板用的比较少,碰到的问题也就少。

而在工作中模板的使用随处可见,在遇到问题中学习,也就对模板有了新的认识和理解。

下面是一个简单的小结。

模板本身不是类或函数

首先这一点是需要最先明确的,之前就是没有理解这一点,所以对模板的认识一直停留在表明。

我们借助以下例子来理解这一个点:

template <typename T>
class AutoList
{
public:
AutoList() {}
~AutoList() {}

bool getAutoList() {return true;}
private:
T value;
};


上面我们定义了一个类模板,但是它不是类——AutoList并不是一个类类型,而是一个类模板。

因此你如果写出以下代码,编译器将会拒绝:

int main()
{
AutoList myList;

return 0;
}


In function ‘int main()’:
错误:missing template arguments before ‘myList’
错误:expected ‘;’ before ‘myList'


你必须为AutoList提供一个模板参数,使它实例化成为一个类型:

int main()
{
AutoList<int> intList;
AutoList<long> longList;

return 0;
}


上面我们分别传入了两个参数int和long,于是类模板将通过实例化产生了两个独立的类型。

你可以理解为,上面代码经过编译器处理之后,变成了如下代码:

class AutoList<int>
{
public:
AutoList() {}
~AutoList() {}

bool getAutoList() {return true;}
private:
int value;
};

class AutoList<long>
{
public:
AutoList() {}
~AutoList() {}

bool getAutoList() {return true;}
private:
long value;
};

int main() { AutoList<int> intList; AutoList<long> longList; return 0; }


举个简单的例子:你在做蛋糕的时候,类模板AutoList就好比提供给编译器的一个模子,而实例化后的AutoList< int >就好比蛋糕类型。

模子它本身并不属于蛋糕类型,你需要把各种做蛋糕的材料填到模子里面去,才能得到一个实实在在的蛋糕,而这些蛋糕,根据你加入的材料不同,会有不同的口味——草莓味的、巧克力味的、牛奶味的——这就是实例化的过程。

也许以上例子不是特别恰当,但是还是非常直观的。

回到我们的例子,编译器在编译以上代码的过程中,碰到AutoList< int >,于是用int去实例化类模板AutoList,得到一个类型AutoList< int >,然后又碰到AutoList< long >,于是用long去实例化类模板AutoList,得到一个类型AutoList< long >,两个类AutoList< int >和AutoList< long >各有一份代码。之后如果又遇到AutoList< int >,因为已经有一份该类的定义代码了,所以直接用就可以了。在编译器编译完成之后,类模板也就没用了,说不定在最后生成的代码中,类模板已经被丢弃,只剩下一个个根据模板实例化来的类类型。

总结一下:每次实例化,编译器根据传入的模板参数来实例化模板,生成一份新的代码。

模板的这种实例化行为,带来的一个问题就是——代码膨胀。

一开始你以为只有一份代码,可是事实上,你实例化了多少个类型,就有多少份类似的代码——只是会用具体的参数来替换掉T。

模板函数声明为inline

// ok
template <typename T> inline T test(const T& value);

// error
inline template <typename T> T test(const T& value);


模板实参推断

一般情况下,模板实参推断的过程中不会进行隐式类型转换来匹配已有的实例,而是会生成新的实例。

看以下两个例子:

template <typename T>
bool test(const T& l, const T& r)
{
return true;
}

int main()
{
short l = 128;
int r = 1024;

test(l, r); //error:此处将试图实例化test(shor, int),而模板中两个参数必须是同一个类型

return 0;
}


我们做一下修改:

template <typename L, typename R>
bool test(const L& l, const R& r)
{
return true;
}

int main()
{
short l_short = 128;
int l_int = 128;
int r = 1024;

test(l_int, r); //实例化了test(const int&, const int&)

test(l_short, r); // 实例化了test(const short&, const int&)

return 0;
}


在上面的第一个例子中,最容易出错的就是下面这种情况:

int a[10], b[12];

test(a, b);


你以为a和b都是int类型的数组,然而结果确出乎你的意料:

错误:对‘test(int [10], int [12])’的调用没有匹配的函数


我们知道,数组作为实参时,实际上会发生类型退化——int类型的数组将退化为指针int *。然而当我们为这个参数加上引用时,退化将不会发生,此时传递的不再是一个指针,而是整个数组,别忘了,数组是有大小的——也就是 int[10]和int[12]实际上是两种不同的类型!

关于const转换:

1.接受const**引用**或const指针的函数可以分别使用非const对象的引用或指针来调用,无需产生新的实例化——将非const传递给const是安全的。

2.如果函数接受非引用类型,形参类型和实参都忽略const——对于pass by value来说,const实参传递给非const形参并不会对实参造成任何影响。

模板编译模型

这一个点在前面的文章已经有提及过:

http://blog.csdn.net/jiange_zh/article/details/52016591

如果你已经理解了模板本身不是类或函数,那么这个知识点对你来说是很好理解的。

首先关于编译的一些知识,前面我也已经介绍过:

http://blog.csdn.net/jiange_zh/article/details/52187611

有了以上的基础,我们再来聊一聊模板编译模型。

模板编译模型有两种:包含编译模型 和 分别编译模型。

在包含编译模型中:

1.只有在看到用到模板时编译器才产生特定的模板实例。

2.要进行实例化,编译器必须能够访问定义模板的源代码。

第一点很好理解,没有往模子到倒任何材料,编译器永远不知道会生产出什么味道的蛋糕。

而第二点可以这样理解:你说你有个蛋糕模子,但是却不把模子给我,我无法给你生产你要的蛋糕。

这种编译模型是最直观的,所以编译器都支持。

而分别编译模型只有小部分编译器支持,然而新的C++标准已经把支持这一方式的export关键字去掉了,因此此处不再讨论。

非类型模板参数

非类型模板参数实参必须是编译时常量表达式。

template <int height, int width>
class Test
{
public:
Test(): m_height(height), m_width(width) {}
private:
int m_height;
int m_width;
};

int main()
{
Test<80, 60> myTest;
}


模板特化

参加之前的博文:

http://blog.csdn.net/jiange_zh/article/details/50784537

几个常用的技巧

1.模板参数还是一个模板



MyStack模板接受两个参数,第一个参数是T,第二个参数还是一个类模板DataContainer,该类模板DataContainer接受两个参数ContainT和AllocT,默认值类型为vector。

同样的,模板类里面的成员函数也可以是模板。

stack1和stack2是两种不同的类型,它们的赋值操作能够成功,是因为其operator = 操作符接收的两个参数分别是MyStack< T >和MyStack< T2 >。

2.模板的显式实例化

这个方法前面已经提过:

http://blog.csdn.net/jiange_zh/article/details/52016591

它可以让我们将模板的声明和定义分开放置。

//test.h

template<typename T>
class Test
{
public:
void setData(const T&);
const T getData();
private:
T m_value;
};

//test.cpp
#include "test.h"

template<typename T>
void Test<T>::setData(const T& value)
{
m_value = value;
}

template<typename T>
const T Test<T>::getData()
{
return m_value;
}

//显式实例化
template class Test<int>;

//错误语法
//template Test<int>;
//template<> Test<int>;


上面的一个缺陷就是,我们必须为我们要用到的类型实例化,比如针对上述代码,如果我们使用Test< long >,将会出错。

模板元编程

在C++编译器内执行并于编译完成时停止执行的程序——它将工作从运行期转移到编译期,因此某些原本需要在运行期才能检测到的错误,现在能够在编译器找出来。当然,不可避免地,编译时间将会变长。

一个很典型的例子就是计算斐波那契数列:

template <unsigned n>
struct Factorial
{
enum { value = n * Factorial<n-1>::value };
};

template<>  //特殊情况
struct Factorial<0>
{
enum { value = 1 };
}


关于模板元编程的讨论,移步知乎:

https://www.zhihu.com/question/21656266

隐式接口和编译时多态

class Widget
{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
};

//显式接口和运行时多态的例子
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}


对象w的类型被声明为Widget,那么w必须支持Widget的所有接口,我们可以在源码中找出这个接口(例如Widget的.h文件中)——因此说是显式接口。

由于Widget的某些成员函数是virtual,因为w对这些函数的调用,将在运行时根据w的动态类型决定调用哪一个函数,表现出运行期多态。

如果我们将doProcessing写成模板:

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


w必须支持哪一种接口呢?这是由template中执行于w身上的操作来决定,本例中它至少需要支持size,normalize和swap成员函数、copy构造函数、不等比较。

凡涉及w的任何函数调用,都有可能造成template实例化,使这些调用得以成功,这些实例化发生在编译期。

显式接口与隐式接口的差异:显式接口基于函数签名式,而隐式接口由有效表达式组成。

什么意思呢?

一个例子来看一下:

if (w != someNastyWidget)


T的隐式接口似乎有以下约束:

它必须支持一个operator !=的函数,用来比较两个T对象(我们假设someNastyWidget的类型为T)。

而事实上,T并不需要支持operator !=,因为该表达式可能成立:operator !=接受一个类型为X的对象和一个类型为Y的对象,T可被转换为X而someNastyWidget可被转换为Y,这样就可以有效调用operator !=了。

typename的双重意义

一般情况下,在template声明式中,class和typename是等价的,并没有什么不同。

特殊情况是,typename被用来验明嵌套从属类型的名称。

template <typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
...
}


看起来好像我们声明x为一个变量,它是一个指针,指向一个C::const_iterator,然而编译器可不这么理解。

也许C有个static成员变量恰好被命名为const_iterator,如果恰好x是个global变量名称呢,那么上述代码可能被理解为C::const_iterator乘以x——听起来有些疯狂,但是完全是有可能的。

C++有个规则可以解析此歧义状态:如果解析器在template中遭遇一个嵌套丛书名称,它便假设这名字不是个类型,除非你告诉它。

那么,要如何告诉编译器这是一个类型呢?——只要紧临它之前放置关键字typename即可:

template <typename C>
void print2nd(const C& container)
{
typename C::const_iterator* x; //这才是合法的C++代码
...
}


typename必须作为嵌套从属类型名的前缀词有一个例外:

typename不能出现在base classes list内的嵌套从属类型名之前,也不能出现在成员初始化列表中作为base class的修饰符。例如:

template <typename T>
class Derived: public Base<T>::Nested   //不允许typename
{
public:
explicit Dervied(int x)
: Base<T>::Nested(x)    //不允许typename
{
typename Base<T>::Nested temp;    //必须typename
...
}
};


SFINAE(Substitution Failure Is Not An Error,匹配失败并不是一种错误)

SFINAE是C++的一个特性

我们都知道对于非模板函数的重载来说,无论是否被调用,或是无论调用点需要的是什么类型的重载,编译器会将所有参与了重载的函数一个不落的全部编译。而且这些函数的所有信息已经具备,当进行调用的时候,编译器就能根据参数的个数跟类型来调用相关度最高的函数。

但对于模板函数来说就不一样了,因为事先编译器根本无法获得所有信息,编译器也不可能为所有重载的模板函数生成真正的执行代码,而是会选择最相关的模板函数进行实例化

C++中,函数模板与同名的非模板函数重载时,应遵循下列调用原则:

寻找一个参数完全匹配的函数,若找到就调用它。若参数完全匹配的函数多于一个,则这个调用是一个错误的调用。

寻找一个函数模板,若找到就将其实例化生成一个匹配的模板函数并调用它。

若上面两条都失败,则使用函数重载的方法,通过类型转换产生参数匹配,若找到就调用它。

若上面三条都失败,还没有找都匹配的函数,则这个调用是一个错误的调用。

看下面的例子:

#include <iostream>

using namespace std;

void print( int iNum )
{
cout<<"int print( int )"<< endl;
}

template < typename T >

void print( T type )
{
typename T::value_type vt_someval;
cout<<"template < typename T >"<< endl;
}

int main()
{
short sNum = 10;
print( sNum );
return 0;
}


以上代码将出现编译错误:

In function ‘void print(T) [with T = short int]’:
instantiated from here
错误:‘short int’不是类、结构或联合类型


根据上面的匹配规则,先查找void print( short iNum ),结果没找到,于是寻找模板,发现有void print( T type ),于是选择了它进行实例化,但是在实例化过程中却发现short不是类、结构或联合类型,所以short::value_type将造成编译失败。

以上情况是匹配成功了——但是实例化失败。

修改以上代码为以下形式:

#include <iostream>

using namespace std;

void print( int iNum )
{
cout<<"int print( int )"<< endl;
}

template < typename T >
void print( T type, typename T::value_type* pvt_dummy = NULL )
{
typename T::value_type vt_someval;
cout<<"template < typename T >"<< endl;
}
int main()
{
short sNum = 10;
print( sNum );
return 0;
}


这时候发现能够编译成功,输出的信息为:int print(int)。

这次为什么能够成功调用void print(int iNum)呢?

对于第一次的代码,由于对于模板函数来说,返回值跟参数都能匹配成功,就表示编译器会认为特化成功而选择模板函数进行特化进而放弃其他选择,然而在实例化的时候自然会产生错误。但是多了typename T::value_type*后,编译器在匹配的时候就会发现错误。这时由于SFINAE的存在,编译器就会放弃特化转而去选择void print(int INum),而不是简单报错。

一个简单的应用:

目标:在模板推导过程中,得到正确的类型或表达式。

首先,我们构造不同的结构数据,它们分别代表两种意义:

typedef char TrueType;
typedef struct{char a[2];} FalseType;


然后定义下面两个函数:

template<typename C>
static TrueType TestIsClass(int C::*); //参数是int*类型,在C作用于下,返回类型为TrueType

template<typename C>
static FalseType TestIsClass(...); // 参数任意,返回类型为FalseType


然后依赖编译期间的操作比如sizeof来快速区分当前的数据类型:

template<typename T>
class IsClass
{
public:
enum { OK = (sizeof(TestIsClass<T>(0)) == sizeof(TrueType)) };
};


我们用两个类型MyTest和int来做分析,看看编译时发生了什么:

代码中使用了 IsClass< MyTest >,于是编译器进行实例化:

class IsClass<MyTest>
{
public:
enum { OK = (sizeof(TestIsClass<MyTest>(0)) == sizeof(TrueType)) }; //这里的计算将会在编译时完成,因此编译结束后,OK只是一个true或者false而已。
};


而在实例化TestIsClass< MyTest >时,首先寻找完全匹配,没找到,于是找模板,找到并成功匹配了下面的模板:

template<typename C>
static TrueType TestIsClass(int C::*);


而sizeof计算的是函数的返回值的大小,因此其计算变为:

enum { OK = (sizeof(TrueType) == sizeof(TrueType)) };


最终当然OK为true。

而如果代码使用了 IsClass< int >,于是编译器进行实例化:

class IsClass<int>
{
public:
enum { OK = (sizeof(TestIsClass<int>(0)) == sizeof(TrueType)) };
};


而在实例化TestIsClass< int >时,首先寻找完全匹配,没找到,于是找模板,由于int不是类或者结构体或者联合类型,因此模板也匹配失败,最终匹配到了任意参数的函数版本。(此处有赖于SFINAE)

因此其计算变为:

enum { OK = (sizeof(FalseType) == sizeof(TrueType)) };


最终当然OK为false。

可以看到,所以的行为都在编译时就完成了,最终的二进制代码中,恐怕只留下OK = true或者OK = false了。

于是我们顺利在编译时就完成了内置类型还是类类型的一个区分。

结语

本文仅针对部分最近学习到的template知识进行总结,这并不是template的全部。Template C++还有许多高深的内容值得学习,在此之前,我对C++的印象就是——面向对象,接触了模版之后,才发现C++的另一面——泛型编程也是十分博大而且有趣。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: