在effective c++中讲到,C++可以分为4个部分:
- 兼容C的部分;区块、语句、预处理、内置数据类型、数组、指针统统属于这个部分。但是,没有模板,没有异常,没有重载。
- 2.Object-Oriented C++, 这部分指的是C with class, 包括classes(构造函数,析构函数),封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)。
- Template C++.泛型编程(generic programming),函数模板,模板类,特化,偏特化,特性。
- STL,c++标准库,是一个template库,它对容器,迭代器,算法以及函数对象的规约有极佳的紧密配合与协调。
所以泛型编程是学习C++的一大主体,同时也是学习STL的基础。在掌握了C++泛型编程的基本知识以后,再去理解STL会让你事半功倍。
理解模板的原理
首先,我们来看一个简单的小例子,比较两个元素大小,返回较大的值。
//int类型版本
int Max(int a, int b)
{
return (a>b)?a:b;
}
//long类型版本
long Max(long a, long b)
{
return (a>b)?a:b;
}
//char类型版本
char Max(char a, char b)
{
return (a>b)?a:b;
}
以上3个函数的功能都为相同。但是,由于比较的元素不一样,所以我们往往需要编写多个Max比较函数,这样很烦。所以我们使用C++提供的模板机制进行解决。
template <typename T>
T Max(T a, T b)
{
return (a>b)?a:b;
}
这时我们还是有疑惑:为什么编写了以上代码以后,我们就可以实现泛型编程了,C++编译器到底是怎么实现这一机制的,同样看个小例子,考虑以下场景:
...
int a = 1;
int b = 2;
int max = Max(a, b);
...
当编译器编译到Max(a, b)时,由于编译器知道a,b是int类型的。所以,编译器就需要一个形参列表为Max(int, int)的版本。如果这时候我们没有声明这个版本。编译器就会使用上述模板代码生成一个int版本。(这个过程也称为实例化)有了这个新生成的int版本,我们的程序就可以通过编译了。
通过上述过程,我们可以知道:模板元编程是编写template-based C++程序并执行于编译期的过程。输出为从template具现出来的若干C++源码。再通过函数重载(通过形参列表,不包括返回值)在编译期决定调用的函数版本,这是一种编译器多态,区别于虚函数的运行期多态。
因为多态发生于编译期,所以模板程序往往在编译期需要花费更多的时间,但是运行更快。
函数模板
函数模板是参数化的一族函数(a family of functions)。
Template <typename T>
Inline T Max(const T& a, const T& b)
{
return (a>b) ? a : b;
}
上述是一个函数模板的简单例子,这里有两个注意点:
- 可以使用class代替typename,但是不能使用struct。
- 从语法上使用class和typename没有区别。但是从语义上class可能导致误区,使人以为只有类才能作为型别参数,事实上任何型别都可以。
类模板
与函数模板类似,类也可以通过参数泛化,从而可以构建出一族不同类型的类实例(对象)。类模板实参可以是某一类型或常量(仅限int或enum)。这里需要注意代码膨胀问题,因为如果在程序中填写不同的函数常量,将产生多个版本的代码。
const std::size_t DefaultStackSize = 1024;
template <typename T, std::size_t n = DefaultStackSize > class Stack {
public:
void Push(const T const & element);
int Pop(T& element);
int Top(T& element) const;
private:
std::vector<T> m_Members;
std::size_t m_nMaxSize = n;
};
类模板的声明
除了Copy constructor之外,如果在类模板中需要使用到这个类本身,比如定义operator=,那么应该使用完整的定义(Stack),而不是省略型别T。
template <typename T, std::size_t n> class Stack
{
public:
...
Stack(Stack<T, n> const&); //copy constructor
Stack<T>& operator= (Stack<T, n> const&); //assignment operator
...
};
类模板的实现
要定义一个类模板的成员函数,则要指明其实一个模板函数的定义应该如下:
template <typename T, std::size_t nMaxSize>
void Stack<T, nMaxSize>::Push(const T const& element)
{
if(m_Members.size() >= m_nMaxSize){
// error handing
return;
}
m_Members.push_back(element);
}
typename的细节问题
首先了解两个概念:
1.从属名称:template内出现的名称如果相依于某个template参数。
2.嵌套从属名称:从属名称在class内呈嵌套状。
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2){
C::const_iterator iter(container.begin());
++iter;
std::cout << value;
}
}
上述代码中value的类型为int,所以属于非从属名称。但是iter就依赖于template参数,而且在class内呈嵌套状,所以属于嵌套从属名称。但是,这个嵌套从属名称会导致解析困难。如下,如果C::const_iterator为一个C内的静态变量,x为一个全局变量,则下面的代码就变成了算术表达式,与我们所希望的相差甚远。
template <typename C>
void print2nd(const C& container)
{
//C内的静态变量 全局变量
C::const_iterator* x;
...
}
所以有一个一般性规则为:typename必须作为嵌套从属类型名称的前缀词。
typename C::const_iterator iter(container.begin());
但是故事并备有结束,还有一个例外:typename不可以出现在base classes list 内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。
template<typename T>
class Derived: public Base<T>::Nested{ //base class list中不允许”typename”
public:
explicit Derived(int x)
: Base<T>::Nested(x) //mem.init.list中不允许”typename”
{
typename Base<T>::Nested temp; //嵌套从属类型名称
...
}
...
};
类模板特化
类模板允许对一个类模板的某些模板参数型别做特化。声明如下:
template<>
class Stack<std::wstring> {
...
}
但是,特化一个类模板的时候也意味着需要特化其所有参数化的成员函数。以下是特化的作用或好处:
- 对于某种特殊的型别,可能可以做些特别的优化或提供不同的实现。
- 避免在实例化类的时候引起一些可能产生的诡异行为
特化以后,可以添加新成员函数SetStackSize,也可以采用list作为Stack的内部实现代替vector。
template <> class Stack<std::wstring> {
public:
void SetStackSize(const std::size_t n) { m_nMaxSize = n; }
void Push(const std::wstring const & element);
int Pop(std::wstring& element);
int Top(std::wstring& element) const;
private:
std::list<std::wstring> m_Members;
std::size_t m_nMaxSize = n;
};
类模板偏特化
所谓的偏特化是指提供另一份template定义式,而其本身仍为templatized;也就是说,针对template参数更进一步的条件限制所设计出来的一个特化版本。
特性
//计算一个数组的累加?
template <typename T> inline T Sigma(const T const* start, const T const* end)
{
T total = T(); //suppose T() actually creates a zero value
while(start != end)
{
total += *start++;
}
return total;
}
char szNames[] = "abc";
std::size_t nLength = strlen(szNames);
char* p = szNames;
char* q = szNames + nLength;
printf("Sigma(szNames) = %d\n", Sigma(p,q));
考虑上述代码,调用Sigma(szName)的结果是38(0x26)!而并非期盼的值(97+98+99 = 294)。怎么办呢?
template <typename T> class SigmaTraits
{ };
template <> class SigmaTraits<char>{
public: typedef int ReturnType;
};
template <> class SigmaTraits<short>{
public: typedef int ReturnType;
};
template <> class SigmaTraits<int>{
public:typedef long ReturnType;
};
template <> class SigmaTraits<unsigned int>{
public:typedef unsigned long ReturnType;
};
template <> class SigmaTraits<float>{
public:typedef double ReturnType;
template <typename T>
inline typename SigmaTraits<T>::ReturnType Sigma(const T const* start, const T const* end)
{
typedef typename SigmaTraits<T>::ReturnType ReturnType;
ReturnType s = ReturnType();
while(start != end)
s += *start++;
return s;
}
通过为每一个Sigma函数的参数型别T创建一种关联(association),关联的型别就是用来存储Sigma结果的型别。这种关联可以看作是型别T的一种特性(characteristic of the type T),因此Sigma函数返回值的型别叫做T的trait。
编译模型
标准c++为编译模板代码定义了两种模型:
包含编译模型。
// header file utlities.h template <class T> int compare(const T&, const T&); #include "utilities.cc" // get the definitions for compare etc // implemenatation file utlities.cc template <class T> int compare(const T &v1, const T &v2) { ... }
分别编译模型。
// class template header goes in shared header file template <class Type> class Queue { ... } // Queue.cc implementation file declares Queue as exported export template <class Type> class Queue; #include "Queue.h" // Queue member definitions