ANSI/ISO C++ Professional Programmer's Handbook(9)

Unity新手必看:5分钟搞定TextMeshPro中文乱码,告别小方块(附7000+常用字库) 本文详细介绍了Unity中TextMeshPro中文乱码问题的解决方案,包括快速生成中文字体的步骤和优化技巧。通过使用支持中文的.ttf字体文件和自定义字符集,开发者可以在5分钟内解决TMP显示小方块的问题,提升游戏或应用的文本显示效果。 阅读详情
 摘自: http://sttony.blogspot.com/search/label/C%2B%2B

9


Templates


by Danny Kalev




简介


模板是一种编译器产生类或函数体的模式。自从在1991年第一次实现模板以来(cfront2.0),C++的程序设计和概念有了相当大的变化。最初模板被视为对如Array和List这种泛型容器类的一种支持。近几年,使用模板的经验显示,这种特性对于设计和实现通用库十分有用,比如STL。模板化的库和frameworks都是高效方便的。通过模板的广泛使用,C++增加了许多复杂高级的结构来控制模板的行为何性能,包括部分特定化、显式特定化、模板成员、引出模板、默认参数类型等等。


本章讨论了设计和实现模板的各个的方面。首先解释类模板。接着是函数模板。最后,讨论一些被特别关注的模板问题——比如指向成员的指针、类模板的虚成员函数、继承关系和显式实例化。


类模板


许多算法和结构可以独立于他们所操作数据的类型。一般的,对数据类型的依赖性仅仅是人造的产物。例如,复数的概念并不排外的限制与基本类型double。相反,它应该能接受任何浮点类型。设计不依赖类型的抽象复数类游很多优点,因为它使用户能够为特定的应用程序选择所需的精度,而不需要手工重复代码。另外,不依赖类型的类在不同平台之间更容易转换。


类模板的申明


使用关键字template来申明类模板,template之后是模板参数列表(template parameter list),用尖括号括起来,之后是类申明或类定义。例如



template <class T> class Vector; //申明
template <class T> class Vector //定义
{
private:
size_t sz;
T * buff;
public:
explicit Vector<T>(size_t s = 100);
Vector<T> (const Vector <T> & v); //拷贝构造器
Vector<T>& operator= (const Vector<T>& v); //赋值运算符
~Vector<T>(); //销毁器
//其他成员函数
T& operator [] (unsigned int index);
const T& operator [] (unsigned int index) const;
size_t size() const;
};

类模板的成员函数可以在类体外面定义。这是,他们必须显式申明为他们类模板的成员函数。例如



//定义Vector的成员函数
//之后是类申明
template <class T> Vector<T>::Vector<T> (size_t s) //构造器定义
: sz(s),
buff (new T[s])
{}
template <class T> Vector<T>::Vector<T> (const Vector <T> & v) //拷贝构造器
{
sz = 0;
buff = 0;
*this = v; //使用重载赋值运算符
}
template <class T> Vector<T>& Vector<T>::operator= //赋值运算符
(const Vector <T> & v)
{
if (this == &v)
return *this;
this->Vector<T>::~Vector<T>(); //调用销毁器
buff = new T[v.size()]; //分配足够的存储空间
for (size_t i =0; i < v.size(); i++)
buff[i] = v[i]; //成员分别拷贝
sz = v.size();
return *this;
}
template <class T> Vector<T>::~Vector<T> () //销毁器
{
delete [] buff;
}
template <class T> inline T& Vector<T>::operator [] (unsigned int i)
{
return buff[i];
}
template <class T> inline const T& Vector<T>::operator [] //常量版本
(unsigned int i) const
{
return buff[i];
}
template <class T> inline size_t Vector<T>::size () const
{
return sz;
}
//Vector.hpp

前缀template <class T>指示T是一个模板参数(template parameter),它是未指定类型的占位符。关键字class应该特别关注,因为形参T的实参可以不是用户定义类型;也可以是char和int这样的基本类型。如果你喜欢更不确定的含义,也可以使用关键字typename来代替class(当然稍后你就会看到typename夜有其他用途):



template <typename T> class Vector //用typename代替class
//两种形式没有语义上的差别
{
//...
};
template <typename T> Vector<T>::Vector<T> (size_t s)
: sz(s), buff (new T[s]) {}

在Vector地范围内,参数T不需要加上范围限制,所以不用它也可以定义申明成员函数。例如构造器可以有如下申明:



template <class T> class Vector
{
public:
Vector (size_t s = 100); //等价与Vector <T>(size_t s = 100);
};

同样的,构造器的定义可以省略多余的形参:



//等价与template <class T> Vector<T>::Vector<T>(size_t s)
template <class T> Vector<T>::Vector (size_t s) :
buff (new T[s]), sz(s)
{}

实例化和特殊化


类模板不是类。从类模板和类型形参实例化类的过程叫模板实例化(template instantiation)。 模板 id,就是模板名字和紧接其后尖括号括起来的形参列表,叫特殊化(specialization)。类模板的特殊化可以同类一样使用。考虑下面的例子:



void func (Vector <float> &); //函数实参
size_t n = sizeof( Vector <char>); //sizeof表达式
class myStringVector: private Vector<std::string> //基类
{/*...*/};
#include <iostream>
#include <typeinfo>
#include <string>
using namespace std;
cout<<typeid(Vector< string>).name(); //typeid表达式
Vector<int> vi; //创建对象

编译器只实例化给出特殊化的成员函数。在下面的例子中,统计了成员实例化的数目:



#include <iostream>
#include "Vector.hpp"
using namespace std;
int main()
{
Vector<int> vi(5); // 1
for (int i = 0; i<5; i++)
{
vi[i] = i; //fill vi // 2
cout<<vi[i]<<endl;
}
return 0; // 3
}

编译器只为显式或隐含在程序中使用过的Vector成员函数产生代码::



Vector<int>::Vector<int> (size_t s) //1: 构造器
: sz(s), buff (new int[s]) {}
inline int& Vector<int>::operator [] (unsigned int idx) //2: 运算符 []
{
return buff[idx];
}
Vector<int>::~Vector<int> () //3: 销毁器
{
delete [] buff;
}

相反的,编译器没有产生成员函数size_t Vector<int>::size() const的代码,因为并不需要。对于有些编译器,可能简单的一次产生所有成员函数的代码,不管需不需要。但是,“需要才产生(generate on demand)”方针是标准所要求的,有两种功能:




  • 效率——定义了成百个成员函数的类模板也很常见(例如STL容器);而实际上在程序中使用的不到一打。为在程序中类有特殊化而没有使用过的成员函数产生代码,可以轻易的使科执行代码膨胀——而且增加了不必要的编译和链接时间。





  • 灵活性——有些情况下,并不是所有的类型都支持类模板中定义的所有运算。例如,容器类可能使用ostream的运算符<<来显示像char和int这样的基本类型以及重载了<<运算符的用户定义类型。然而,没有重载<<的POD(无格式)结构也可能被存储在容器中,而没有调用<<。





模板形参


模板可以使用类型实参,就是表示还没有指定的类型的符号。例如



template <class T > class Vector {/*...*/};

模板也可以接受普通类型如int、long作为实参:



template <class T, int n> class Array
{
private:
T a[n];
int size;
public:
Array() : size (n){}
T& operator [] (int idx) { return a[idx]; }
};

但是注意,当普通类型作为形参时,整数类型的模板形参必须是常量或常量表达式。例如



void array_user()
{
const int cn = 5;
int num = 10;
Array<char, 5> ac; // OK, 5是常量
Array<float, cn> af; // OK, cn是常量
Array<unsigned char, sizeof(float)> auc; // OK, 常量表达式
Array<bool, num> ab; // 错误,num不是常量
}

除了常量表达式以外,其他允许的形参有非重载的指向成员的指针和对象或外部链接函数的地址。这也意味着显式字符串不能用于模板形参,因为它是内部链接。例如:



template <class T, const char *> class A
{/*...*/};
void array_user()
{
const char * p ="illegal";
A<int, "invalid"> aa; // 错误,显式字符串用于模板形参
A<int, p> ab; //也是错误的,p没有外部链接
}

模板也可以接受模板作为形参。例如



int send(const Vector<char*>& );
int main()
{
Vector <Vector<char*> > msg_que(10); //模板用于形参
//...填充 msg_que
for (int i =0; i < 10; i++) //发送消息
send(msg_que[i]);
return 0;
}

注意模板作为形参时,尖括号间的空格是必需的:



Vector <Vector<char*> > msg_que(10);

否则,两个>>与右移运算符混淆。


一个typedef可以避免这种麻烦并且使编译器和其他程序员的更好读懂:



typedef Vector<char *> msg;
Vector<msg> msg_que;

默认类型参数


类模板可以有默认参数。


就像函数的默认参数一样,模板的默认类型参数给了程序员为特定应用程序选择最佳类型的灵活性。例如 ,Vector模板可以对特定内存管理任务进行优化。不使用硬编码的size_t类型来存储Vector大小,可以将大小类型作为一个参数。对于大多数用途,使用默认类型size_t。但是。对于管理非常大的内存缓冲——或非常小的——程序员可以自由选择适当地数据类型代替。例如



template <class T, class S = size_t > class Vector
{
private:
S sz;
T * buff;
public:
explicit Vector(S s = 100): sz(s), buff(new T[s]){}
~Vector();
//其他成员函数
S size() const;
};
template <class T, class S> Vector<T,S>::~Vector<T,S>()//销毁器定义
{
delete [] buff;
}
template <class T, class S> S Vector<T,S>::size() const
{
return sz;
}
int main()
{
Vector <int> ordinary;
Vector <int, unsigned char> tiny(5);
return 0;
}

默认大小类型的另一个优点是支持有具体编译器决定的类型。在支持64位整数的机器,程序员可以使用Vector来简单的操作非常大的内存缓冲区:



Vector <int, unsigned __int64> very_huge;

不需要过分强调这个事实,程序员不必要更改Vector的定义就可以恰当地特殊化。如果没有模板,像这样高级的自动化很难达到。


静态数据成员


模板可以有静态数据成员。例如



template<class T> class C
{
public:
static T stat;
};
template<class T> T C<T>::stat = 5; //定义

静态数据成员可以这样访问:



void f()
{
int n = C<int>::stat;
}

友元


类模板的友元可以是函数模板或类模板,特殊化的函数模板和类模板或是普通(非模板的)函数或类。下面的章节讨论了类模板可以有的各种友元。


非模板友元

模板类的非模板友元申明和非模板类的友元申明十分相似。在下面的例子中,类模板Vector申明普通函数f()和普通类Thing作为它的友元:



class Thing;
template <class T> class Vector
{
public:
//...
friend void f ();
friend class Thing;
};

每一个Vector的特殊化都有友元:函数f()和类Thing。


特殊化

你还可以申明特殊化模板作为类模板的友元。在下面的例子中,类模板Vector申明特殊化C<void*>作为自己的友元:



template <class T> class C{/*...*/};
template <class T> class Vector
{
public:
//...
friend class C<void*>; //其他特殊化不是Vector的友元
};

模板友元

类模板的友元类可以是模板。例如,你可以申明磊模板作为其他类模板的友元:



template <class U> class D{/*...*/};
template <class T> class Vector
{
public:
//...
template <class U> friend class D;
};

每一个D的特殊化是每一个Vector特殊化的友元。你也可以申明函数模板作为友元(函数模板将在稍后详细讨论)。例如,你可以增加一个重载运算符==的函数模板来测试两个Vector对象的相等性。从而对于每一个Vector的特殊化,编译器将产生相应重载运算符==特殊化的代码。为了申明一个友元模板函数,你必须先申明类模板和友元函数模板:



template <class T> class Vector; // 先申明类模板
//作为友元使用之前先申明函数模板
template <class T> bool operator== (const Vector<T>& v1, const Vector<T>& v2);

然后在类体中申明友元函数模板:




template <class T> class Vector
{
public:
//...
friend bool operator==<T> (const Vector<T>& v1, const Vector<T>& v2);
};

最后定义友元函数模板:



template <class T> bool operator== (const Vector<T>& v1, const Vector<T>& v2)
{
// 两个Vectors当且仅当:
// 1) 他们有相同的数量的元素;
// 2) 每一个元素都必须相同
if (v1.size() != v2.size())
return false;
for (size_t i = 0; i<v1.size(); i++)
{
if(v1[i] != v2[i])
return false;
}
return true;
}

部分特殊化


可以定义一个类模板的部分特殊化(partial specialization)。部分特殊化提供一种定义原始模板(primary template)的选择。在任一个特殊化的实参与在部分特殊化中给出的参数相匹配时,部分特殊化用来代替原始定义。例如,Vector的部分特殊化可以专门处理指针类型。因此,对于基本类型和用户定义类型,使用原始Vector类。对于指针,部分特殊化用来代替原始的类模板定义。一个指针部分特殊化可以在许多方面使对指针的操作最优化。


另外,解除指针的引用和->运算符,其他类型是不会用的。


部分特殊化定义为:



//filename: Vector.cpp
template <class T> class Vector <T*> //Vector <T>的部分特殊化
{
private:
size_t size;
void * p;
public:
Vector();
~Vector();
//...成员函数
size_t size() const;
};
//Vector.cpp

部分特殊化通过类模板名字后面的形参列表来指定(记住原始类模板申明时名字后面没有表)。比较两种形式:



template <class T> class Vector //原始模板
{};
template <class T> class Vector <T*> //部分特殊化
{};

有多个形参的部分特殊化类模板申明为:



template<class T, class U, int i> class A { }; // 原始
template<class T, int i> class A<T, T*, i> { }; // 部分特殊化
template<class T> class A<int, T*, 8> { }; // 其他部分特殊化
部分特殊化必须在原始类模板申明之后出现,而且其形参不能包括默认类型

类模板的显式特殊化


类模板的显式特殊化(explicit specialization)提供了原始模板定义的选择。如果部分特殊化中的实参与显式特殊化中给出的相匹配时,显式特殊化用来代替原始定义。它在什么时候有用呢?考虑模板Vector:编译器为特殊化Vector<bool>生成的代码是十分低效。并不是用一位来存储每一个Boolean值,它至少用一个字节。当你操作大量比特时,比如逻辑运算和数字信号处理,这将是不可忍受的。另外,用位运算符来进行面向位的操作有更高的效率。显然,为处理比特经过特殊调整的Vector模板有重要的优点。下面是为了用比特而不是字节来处理bool的Vector<bool>显式特殊化的例子:



template <> class Vector <bool> //显式特殊化
{
private:
size_t sz;
unsigned char * buff;
public:
explicit Vector(size_t s = 1) : sz(s),
buff (new unsigned char [(sz+7U)/8U] ) {}
Vector<bool> (const Vector <bool> & v);
Vector<bool>& operator= (const Vector<bool>& v);
~Vector<bool>();
//其他成员函数
bool& operator [] (unsigned int index);
const bool& operator [] (unsigned int index) const;
size_t size() const;
};
void bitmanip()
{
Vector< bool> bits(8);
bits[0] = true; //赋值assign
bool seventh = bits[7]; //检索
}

template<>前缀指示这是原始模板的显式特殊化。特殊化的模板实参在尖括号种马上指定了。迄今已定义的Vector的特殊化体系如下:



template <class T> class Vector //原始模板
{};
template <class T> class Vector <T*> //部分特殊化
{};
template <> class Vector <bool> //显式特殊化
{};

幸运的是,STL已经定义了std::vector<bool>的特殊化来优化对比特的处理,你将在下一章“STL和泛型程序设计”中读到。


类模板函数的特殊化


类Vector重载运算符==执行可能的在两个Vector对象之间的比较,当两个对象的元素都是基本类型或是重载了运算符==的对象时。但是比较存储C字符串的对象时可能会产生错误。例如



#include "Vector.hpp"
extern const char msg1[] = "hello";
extern const char msg2[] = "hello";
int main()
{
Vector<const char *> v1(1), v2(1); //元素数量相同
v1[0] = msg1;
v2[0] = msg2;
bool equal = (v1 == v2); //错误,字符串相同但指针不同
return 0;
}




注意:字面上一致的字符串是否被当作不同对象依赖于具体编译器的实现。有些编译器可能将常量 msg1和 msg2存储在同一个地址(在这样的编译器上表达式 bool equal = (v1 == v2);是true)。但是这里假定 msg1和 msg2存储在两个不同的内存地址。


尽管v1和v2有相同数量的元素而且其元素存储的相同的字符串值,运算符==返回false,因为它比较字符串的地址而不是它们的内容。你可以专门为const char *类型定义特殊版本的运算符==,来比较字符串的值而不是地址。当类型Vector<const char *>进行比较时,编译器只选择特定的版本。否则就使用原始版本的运算符==。没有必要在类模板Vector中增加一种特殊的友元运算符==。但是仍然推荐你这么做,以标出特殊运算符==的存在。例如



template <class T> class Vector;
template <class T> bool operator== (const Vector<T>& v1, const Vector<T>& v2);
template <class T> class Vector
{
//...
public:
friend bool operator==<T> (const Vector<T>& v1,
const Vector<T>& v2); //原始
friend bool operator== ( //特殊版本
const Vector<const char *>& v1,
const Vector<const char *>& v2);
};

特殊版本的定义必须出现在一般版本之后。所以,你可以将它放到一个头文件中,已确保在一般版本之后。下面是特殊版本的例子:



//appended to vector.cpp
#include <cstring> //需要strcmp
using namespace std;
template <> bool operator== (
const Vector<const char *>& v1,
const Vector<const char *>& v2 )
{
if (v1.size() != v2.size()) //与以前一样
return false;
for (size_t i = 0; i<v1.size(); i++)
{
if (strcmp(v1[i], v2[i]) != 0) //比较字符串的值
return false;
}
return true;
}

这里也一样,关键字template后面的空尖括号指定特殊版本来代替以前定义的一般版本。编译器现在用特殊形式的运算符==来比较v1和v2;如期望的,现在结果是true。


特殊函数以类似于派生类虚成员函数的行为来处理。两种情况下,实际的被调用的函数确定于类型。只不过,虚函数机制依赖对象的动态类型,而函数特殊化是静态的。


函数模板


许多算法执行一系列相同的操作,而不用管所操作数据的类型。min和max,array sort和swap都是这种不依赖类型的例子。在没有模板的时候,程序员不得不使用其他技术来实现泛型算法:宏,void指针和公共根基类——所有这些都有明显的缺点。本节先举例说明这些技术的缺点,再示范如何在泛型程序设计中使用函数模板。


带参数的宏


带参数宏是在C中实现泛型算法的主要形式。例如



#define min(x,y) ((x)<(y))?(x):(y)
void f()
{
double dlower = min(5.5, 5.4);
int ilower = min(sizeof(double), sizeof(int));
char clower = min('a', 'b');
}

标准C库库一了各种带参数宏。在一定的范围内,使用他们很有效率,因为避免了完全函数调用的开销。但是宏也有明显得缺点。预处理程序对宏指示做简单的文本替换,只有非常有限的范围限制和类型检查。而且宏的难以调试是臭名昭著的,因为编译器扫描可能与看上去与原来的源文件区别很大的源文件。由于这个原因,编译器可能指出原来的源文件不包括的代码。宏可以轻易的膨胀程序的尺寸,因为每一个宏调用都被展开了。当复杂的宏被反复调用时,导致程序的尺寸意想不到的膨胀。尽管语法类似,宏与函数在语义上有很大差别——宏没有连接、地址和存储类型。由于这些原因,谨慎地是使用宏——或责不用。


void指针


近似于宏的选择是使用泛型指针void *,它可以存储任何类型的地址。C标准库定义的两个泛型函数使用了这种特性qsort和bsearch。qsort在头文件<stdlib.h>中有如下定义:



void qsort( void *,
size_t,
size_t,
int (*) (const void *, const void *)
);

这种泛型函数的类型无关性通过使用void指针和一个抽象的用户定义比较函数来实现。尽管如此,这种技术还是有值得注意的缺点。void指针不是类型安全的,而且反复的函数回调引起了远行其开销,在大多数情况下应该用内联来避免。


公共根基类


在一些面向对象语言中,每一个对象都是从一个公共基类类派生(在C++中这种设计的方法和缺点在第五章“面向对象的编程和设计”中详细讨论)。泛型算法可以依赖这种特性。例如



// 伪 C++ code
class Object //公共根类
{
public:
virtual bool operator < (const Object&) const; //多态行为
//..其他成员函数
};
const Object& min(const Object &x, const Object& y)
{
return x.operator<(y) ? x : y; //x和y可以使任何类的对象
}

可是在C++中模仿这种方法并不象在其他语言中那么有用。C++并不强制的有一个公共根基类。因此,这是程序员的——而不是编译器的——责任来保证每一个类都要从公共基类派生。更糟糕的是,公共根基类并不是标准的。结果这样的算法并不是通用的。另外这些算法不能处理基本类型,因为基本类型不是类对象。最后,对于这种算法,随着RTTI的广泛使用将带来不可接受的性能损失。



函数模板没有以上所有缺点。他们是类型安全的,他们可以是内联函数以提高性能,而且——最重要的——他们可以一致的对待基本类型和用户定义类型。


函数模板申明包括关键字template,之后是模板参数列表和函数申明。与普通函数不一样的,普通函数地申明和定义通常不再一个translation unit中,而模板的定义紧跟着其申明。例如



template <class T> T max( T t1, T t2)
{
return (t1 > t2) ? t1 : t2;
}

与类模板不同,函数模板的形参可以由实参的类型隐含的推出。


在下面的例子中,编译器实例化了三种不同的swap的特殊化,考虑每一个调用中使用实参的类型:



#include <string>
using namespace std;
int main()
{
int i = 0, j = 8;
char c = 'a', d = 'z';
string s1 = "first", s2 = "second";
int nmax = max(i, j); // int max (int, int);
char cmax = max(c, d); // char max (char, char);
string smax = max(s1, s2); // string max (string, string);
return 0;
}

可以使用同样的名字定义几个函数模板(重载)或也可以定义与普通函数同名的函数模板。例如



template <class T> T max( T t1, T t2)
{
return (t1 > t2) ? t1 : t2;
}
int max (int i, int j)
{
return (i > j) ? i : j;
}

编译器不会产生max()的int版本的代码。在用类型为int的实参调用调用max()时,编译器调用函数int max (int, int)。


性能的考虑


C++提供许多工具来控制模板的实例化,包括模板的显式实例化和引出模板。下面的章节示范怎样使用这些特性,以及其他提高性能的技术。


等价类型


当下列条件都成立时,两个模板是等价的(就是说,他们指的同一个模板id):




  • 名字相同——模板的名字相同并且指的是相同的模板。





  • 参数相同——模板的参数类型是相同的。





  • 同样的非类型参数——两个模板非类型的整数或枚举类型参数有相同的值,并且他们的非类型指针参数引用的是相同的外部对象。





  • 模板的模板参数——两个模板地模板参数指的是同一个模板。





下面是例子:



template<class T, long size> class Array
{ /* ... */ };
void func()
{
Array<char, 2*512> a;
Array<char, 1024> b;
}

编译器预先计算常量表达式象2*512,所以模板a和b是相同的类型。下面是另一个例子:



template<class T, int(*error_code_fct)()> class Buffer
{ /* ... */ };
int error_handler();
int another_error_handler();
void func()
{
Buffer<int, &error_handler> b1;
Buffer<int, &another_error_handler> b2;
Buffer<int, &another_error_handler> b3;
Buffer<unsigned int, &another_error_handler> b4;
}

b2和b3是同类型的,因为他们是同一模板的实例而且他们接受相同的类型和非类型参数。相反的,b1和b4是不同类型。


函数func()从相同类模板实例化出3个不同特殊化:一个b1、第二个b2和b3、第三个b4。与普通函数重载不同,编译器对于每一个独有类型从类模板产生一个不同类。在对long和int有表示方法的机器上,下面的代码仍然产生两个不同模板的实例:



void too_prolific()
{
Buffer<int, &error_handler> buff1;
Buffer<long, &error_handler> buff2;
}

同样的,char和unsigned char实不同类型,即使在默认char有unsigned属性的机器上。习惯普通函数重载的宽松匹配规则的程序员并不总能意识到使用有相似但不同类型模板产生的代码膨胀。


避免不必要的实例化


在下面的例子中,产生了模板min的三份拷贝——使用的所有类型:



template < class T > T min(T f, T s)
{
return f < s? f: s;
}
void use_min()
{
int n = 5, m= 10;
int j = min(n,m); //min<int> 实例化
char c = 'a', d = 'b';
char k = min(c,d); //min<char>实例化
short int u = 5, v = 10;
short int w = min(u,v); // min<short int>实例化
}

换句话说,普通函数避免了产生多余的特殊化:



int min(int f, int s)
{
return f < s? f: s;
}
void use_min()
{
//所有调用min都使用了int min (int, int);
int n = 5, m= 10;
int j = min(n,m);
char c = 'a', d = 'b';
char k = min(c,d);
short int u = 5, v = 10;
short int w = min(u,v);
}

尽管如此,min的模板版本比函数有明显的优点:它可以处理指针或其他用户定义类型。你肯定想享用模板好处的同时也避免产生不必要的特殊化的代码。怎么办到呢?简单的解决方案是在调用函数模板之前将参数转换成通用类型。例如:



void no_proliferation()
{
short n = 5, m= 10;
int j = min( static_cast<int> (n),
static_cast<int> (m) ); //min<int> 实例化
char c = 'a', d = 'b';
char k = static_cast<char> (min( static_cast<int> ,
static_cast<int> ) ); //min<int>使用
}

这种技术同样适合指针:首先必须转换成void *,其次void *也可以转换会原来的类型。当呢使用指向多态对象,派生对象的指针转换成基对象的指针。


对于象min这样非常小的模板,将参数转换成公用类型不费多少功夫。尽管如此,使用含上百行代码的大模板时,你就要考虑这个问题。


显式模板实例化


前面提到,如果程序使用了模板,模板仅实例化一次。一般的,当编译器在源文件中碰到模板时,编译器为特殊化产生必要的代码。当编译含有上百源文件的大型工程时,这将严重影响编译器速度,因为碰到特殊化时就必须打断编译过程。这种中断可能在每一个源文件中发生,因为他们都使用——除了意外情况——标准库的类模板。考虑仅包含三个源文件的简单工程:



//filename func1.cpp
#include <string>
#include <iostream>
using namespace std;
void func1()
{
string s; //产生默认构造器和销毁器
s = "hello"; //产生用于const char *的赋值运算符
string s2;
s2 = s; //产生运算符= const string&, string&
cout<<s2.size(); //产生string::size, ostream&运算符 <<(int)
}
//func1.cpp
//filename func2.cpp
#include <string>
#include <iostream>
using namespace std;
void func2()
{
string s; //产生默认构造器和销毁器
cout<<"enter a string: "<<endl; //产生ostream&运算符<<(const char *)
cin>>s //产生istream&运算符>>(string&)
}
//func2.cpp
//filename main.cpp
int main()
{
func1();
func2();
retrun 0;
}
// main.cpp

如果将所有必须的模板代码一次性的实例化,可以大大减少编译时时间,因为避免了打断编译过程。为了这个目的,你可以使用显式实例化(explicit instantiation)。通过在模板申明前面加一个关键字template(没有<>)来指定显式实例化。 这里是一些显式模板实例化的例子:



template <class T> class A{/*..*/};
template<class T> void func(T&) { }
//filename instantiations.cpp
template class Vector<short>; //类模板的显式实例化
template A<int>::A<int>(); //成员函数的显式实例化
template class
std::basic_string<char>; //命名空间成员的显式实例化
template void func<int>(int&); //函数模板的显式实例化

标准库显式实例化的例子

标准库中定义了许多类模板的特殊化。一个例子是类模板basic_string<>。这个类的两个特殊化版本是std::string和std::wstring,两者分别是特殊化basic_string<char>和basic_string<wchar_t>的typedef。通常,没有实例化他们的必要,因为头文件<string>已经实例化了特殊化:



#include <string> //td::string和std::wstring的定义
using namespace std;
bool TranslateToKorean(const string& origin,
wstring& target ); //英/韩字典
int main()
{
string EnglishMsg = "This program has performed an illegal operation";
wstring KoreanMsg;
TranslateToKorean(EnglishMsg, KoreanMsg);
}

引出模板





注意:引出模板是C++相对较新的特性;因此并不是所有的编译器都支持它。请检查你的用户手册以确定你的编译器是否支持。



可以在多个translation units中包含同一个模板定义;因此,它可以被编译多次。显然,这浪费了许多编译和连接时间。代替#including完整的模板定义,将模板定义只编译一次而在其他translation units中仅使用模板申明也是可能的。这与编译外部函数和类很相似,其定义只需编译一次之后就只需要申明了。


为了将模板的定义和申明分开,模板必须是exported。通过模板定义前加export来指定:



//filename min.cpp
export template < class T > T min (const T& a, const T& b)
{
return a > b ? b : a;
}

现在要在其他translation units中是用模板只需模板的申明。例如



//file min.c
template < class T > T min (const T & a, const T & b); //仅需申明
int main()
{
int j=0, k=1;
in smaller = min(j,k);
return 0;
}

内联函数模板不能被引出。如果模板函数即申明export又申明了inline,export申明没有效果,函数仅是内联。申明类模板为引出等价于申明类所有非内联成员函数、静态数据成员和成员类为引出。在未命名命名空间中的模板不能引出。


与语言其他特性的互相影响


模板与语言其他特性的相互影响有时可能带来令人惊讶的结果。下面的章节讨论模板和语言其他特性的各种相互影响,包括模板名字的不明确解释,继承和虚成员函数。


关键字typename


在模板中使用范围名字可能导致类型和非类型的混淆。例如



int N;
template < class T > T func()
{
T::A * N; // 混淆:乘法还是指针申明?
//
}

如果T::A是类型名,func() N的定义创建了指针。另一方面如果T::A是非类型(例如,如果A是int类型的数据类型), T::A * N是一个由范围全名T::A和全局int N相乘组成的表达式。默认的,编译器假定象T::A这样的表达式是非类型。关键字typename指示编译器替代默认的解释,以解决类型名和非类型名的混淆。换句话说,前面(可能混淆的)语句实际上被确定为乘法表达式,结果是被丢弃。为了申明一个指针,需要关键字typename :



int N;
template < class T > T func()
{
typename T::A * N; //N现在是一个指针因为T::A是一个类型名
//...
};

模板之间的继承关系


常见的错误是假定派生类的指针或引用的容器是基类的指针或引用的容器。例如



#include<vector>
using namespace std;
class Base
{
public: virtual void f() {}
};
class Derived : public Base
{
public: void f() {}
};
void func( vector<Base*>& vb);
int main()
{
Derived d;
vector<Derived*> vd;
vd.push_back(&d);
func(vd); //错误,vector<Derived*>&不是vector<Base*>
}

尽管Derived和Base之间有继承关系,但是包含有继承关系对象的指针或引用的相同的类模板没有继承种关系。


虚成员函数


成员函数模板不能是虚的。但是类模板的普通成员函数可以是虚函数。例如



template <class T> class A
{
public:
template <class S> virtual void f(S); //error
virtual int g(); // OK
};

成员函数模板的特殊化不会替代在基类中定义的虚函数。例如



class Base
{
public:
virtual void f(char);
};
class Derived : public Base
{
public:
template <class T> void f(T); //不会替代 B::f(int)
};

指向类模板成员函数的指针


指向类成员的指针可以接受类模板成员函数的地址。象普通类一样,成员指针不能指向静态成员函数。爱下面的例子中使用了std::vector<int>的特殊化:



#include<vector>
using namespace std;
//typedef用来隐藏难懂的语法
typedef void (vector< int >::*pmv) (size_t);
void func()
{
pmv reserve_ptr = &vector< int >::reserve;
//...使用reserve_ptr
}

总结


模板简化了实现泛型容器和函数。模板的好处吸引了正在从面向对象的frameworks向面向泛型frameworks转变的软件厂商。然而,参数化类型并不是C++独有的。早在1983,Ada引入了泛型包,一种类似与类模板的东西。其他语言也实现从用户写的框架代码中自动产生代码的机制。


C++中两种模板种类是类模板和函数模板。类模板封装了参数化数据成员和成员函数。函数模板是实现泛型算法的手段。在C++没有模板的时代,传统实现泛型算法的方法是有限、不安全,并不如模板有效率。模板的一个重要方面是支持对象语义。


C++使程序员能通过显式实例化来控制模板的实例化。 实例化整个类。实例化特定成员函数或者部分特殊化函数模板。都是可以的。模板的显式实例化通过不加尖括号的关键字template来指定。类模板的显式实例化忠实需要的。对于函数模板,编译器经常从实参的参数来推出特殊化。定义主模板的参数类模板的特殊化是可以的。这种特性对于修改操作指针类模板的行为是很有用的。部分特殊化通过在模板名字后面的第二参数列表来指定。显式特殊化使得程序员可以代替自动的实例化。显式特殊化通过关键字template指定,其后更着空的尖括号和模板名字后面的参数类型列表。


模板和重载运算符是泛型程序设计的基础构件。标准模板库STL是泛型程序设计的可效仿的framework,就像你将在下一章看到的。

经典点云配准算法:迭代最近点算法ICP(Iterative Closest Point) 迭代最近点算法,又名为 ICP(Iterative Closest Point)算法。 (1)ICP算法对初始值的依赖比较大,具体的配准过程中,如何选择初始值是一个问题,可以使用轮式里程计、imu预积分或其它先验的配准结果。 (2)ICP迭代次数过多,实际工程中需要考虑到其实时性的优化问题。 (3)迭代终止的阀值的设置也是需要考虑的点,可以考虑自适应阀值的方法。 (4)关于寻找最近点可以使用kdtree进行临近搜索。 阅读详情

相关推荐

ros2笔记-4.6使用launch启动脚本

ros2 4.6 使用launch启动多节点

bohu83的博客 1311

ANSI/ISO C++ Professional Programmer's Handbook 9

ANSI/ISO C++ Professional Programmers Handbook 9 Templatesby Danny Kalev 简介 类模板 类模板的申明 实例化和特殊化 模板形参

sttony的专栏 933

uses ARM-Compiler ‘Default Compiler Version 5‘ which is not available. MDK5.37安装ARM_Compiler_5

MDK5.37安装ARM_Compiler_5 最后一个AC5编译器,从MDK5.37开始,不再默认安装,需要独立安装 ARMCompiler_506_Windows_x86_b960.zip (80.94MB) 否则使用AC5编译代码报错如下: Build started: Project: Project *** Target 'STM3210C_EVAL' uses ARM-Compiler 'Default Compiler Version 5' which is not available. *

mowwwcom的专栏 2万+

ANSI/ISO C++ Professional Programmer's Handbook 14

ANSI/ISO C++ Professional Programmers Handbook 14总结和未来的方向by Danny Kalev 一些差一点被加入到标准的特性 Hashed联合容器 函数模板的默认类型参数 C++演

sttony的专栏 941

从零开始搭建GD32F103CBT6开发环境:VSCode配置与实战技巧

本文详细介绍了如何从零开始为国产MCU GD32F103CBT6搭建高效的VSCode开发环境。内容涵盖工具链安装、VSCode核心插件配置、工程目录结构规划、关键配置文件编写以及Makefile实战,旨在帮助开发者摆脱传统IDE束缚,利用现代化工具链提升嵌入式开发效率。

weixin_29158413的博客 448

c语言实现语音检测vad_壹鸽技术科普·智能语音客服系统中的前端语音处理技术...

随着智能客服机器人产业的迅速发展,尤其是伴随着人工智能AI的风潮,预计在2020年我国智能客服市场将达到万亿级别。在智能客服机器人领域中对电话信道语音进行端点检测处理的应用正在不断扩展和深化。目前语音端点检测(Voice Active Detection,简称VAD)的主要任务是准确快速判决出待处理的语音信号有话还是无话,作为自动语音识别(Automatic Speech Recognition,...

weixin_36431018的博客 508

ANSI/ISO C++ Professional Programmer's Handbook 2

ANSI/ISO C++ Professional Programmers Handbook 2标准简报:ANSI/ISO C++的最新附加部分by Danny Kalev 简介 理解 ANSI/ISO标准 本章的目的和结构 标准的术语

sttony的专栏 1234

软考嵌入式 第五讲 嵌入式Linux系统技术

5.1 inittab配置文件 Linux实时操作系统开机引导时首先启动内核,由内核检查和初始化硬件设备,载入设备的驱动程序模块,安装root文件系统,然后内核将启动一个名为init的进程,它是系统运行的第一个进程,进程号为1。在init运行完成并启动其他必要的后续进程后,系统开始运行,引导过程结束。 在任何时候,可以在文件inittab中添加新的记录项。重新检查inittab文件时,可以通过命令:init q/ Q使init进程立即重新读取并处理文件inittab。 5.1.1 inittab文件记录

Leon的博客 526

ANSI/ISO C++ Professional Programmer's Handbook 3

ANSI/ISO C++ Professional Programmers Handbook 3 运算符重载by Danny Kalev 简介 运算符重载的唯一规则 成员和非成员 运算符的接口 运算符的一致性

sttony的专栏 845

基于 STM32 的便携式肌电信号采集与疲劳度评估系统设计与实现

本系统以 STM32 单片机为核心控制器,通过肌电电极采集人体骨骼肌表面的 sEMG 信号,经仪表放大、滤波、AD 采样后送入 MCU 进行时域 / 频域特征提取,利用 RMS(均方根)、MPF(中位频率)、MF(平均频率)等指标实时评估肌肉疲劳状态,最终通过 OLED 显示疲劳等级,并支持蓝牙上传至上位机。

weixin_42300449的博客 105

Modbus通信从入门到精通_2_Modbus TCP通信详解及仿真(搭建ModbusTCP仿真环境:创建虚拟PLC并进行ModbusTCP通讯;寄存器与PLC中映射关系;适合理解如何编写上位机)

LocalPort”是PLC的端口,最终的设置如下图所示:(相关的设置都可以查看帮助文档)P#DB1.DBX0.0 BYTE28是以指针形式表示的DB1的DBX0.0开始(在使用的西门子Modbus tcp的程序块中设置的“MB_HOLD_REG”输出寄存器),其有28个字节(28个字节对应14个寄存器),4001是第一个寄存器,往后数14个即可。在上面的基础上,增加一个“DB1”的“组对象”,同时将“MW0”的组对象禁止(双击组对象,“Active”取消勾选),启动之后发现无法连接。

分享学习成长历程,Winter is coming! 1万+

ANSI/ISO C++ Professional Programmer's Handbook 4

ANSI/ISO C++ Professional Programmers Handbook 4 特殊成员函数:默认构造器,拷贝构造器,销毁器和赋值运算符by Danny Kale 简介 构造器 从类的构造器调用对象的成员函数 不需要的构造器(Trivial

sttony的专栏 601

【示波器专题】示波器探头的原理深入——有源探头之单端有源探头

由于接地环路的苛刻要求,更高带宽的单端有源探头就算设计了也不实用。因此单端有源探头的带宽一般在6GHz以内,更高带宽的信号测量也只能使用差分有源探头(市面上有些型号的差分探头可同时兼顾单端测试)。......

一点硬件 2145

ANSI/ISO C++ Professional Programmer's Handbook(12)

  摘自:http://sttony.blogspot.com/search/label/C%2B%2B12优化你的代码by Danny Kalev 简介 本章的范围 在优化你的软件之前 申明的布局 初始化比赋值好 重新放置申明 成员初始化列表 前缀运算符VS后缀运算符 内联函数 函数调用的开销 内联函数的好处 被申明为内

SearchLife的专栏 1209

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

ANSI/ISO C++ Professional Programmer's Handbook

[在线阅读 或 下载] ANSI/ISO C++ Professional Programmers Handbook 文件格式: PDF/Adobe Acrobat  

Mobidogs Blog 1120

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

ANSI/ISO C++ Professional Programmer's Handbook(14)

摘自:http://sttony.blogspot.com/search/label/C%2B%2B14总结和未来的方向by Danny Kalev一些差一点被加入到标准的特性Hashed联合容器函数模板的默认类型参数C++演化过程与其他语言的比较用户的利益"Pay as You Go"将来可能加入C++的特性自动垃圾收集器对象永久化

SearchLife的专栏 1352

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

ANSI/ISO C++ Professional Programmer's Handbook 13

ANSI/ISO C++ Professional Programmers Handbook 13与C语言的兼容性问题by Danny Kalev 简介 ISO C 和ANSI/ISO C++的C子集之间的区别 函数形参列表 函数申明 空的形参列

sttony的专栏 1259

ANSI/ISO C++ Professional Programmer's Handbook 10

ANSI/ISO C++ Professional Programmers Handbook 10STL和泛型程序设计by Danny Kalev 简介 泛型程序设计 STL头文件的组织 容器 算法 迭代器

sttony的专栏 1218

ANSI/ISO C++ Professional Programmer's Handbook(1)

  摘自:http://sttony.blogspot.com/search/label/C%2B%2B1简介by Danny KalevC++的起源 带类的C 进入C++ 80年代后期:开放导致的泛滥 ANSI委员会标准的建立 成熟 国际性的标准 委员会的草案和公众的评论 标准的锁定和最终化(Feature Freeze and Fin

SearchLife的专栏 1121

ANSI/ISO C++ Professional Programmer's Handbook 7

ANSI/ISO C++ Professional Programmers Handbook 7运行期类型识别by Danny Kalev 简介 本章的结构 不用RTTI 不要RTTI的支持,虚成员函数也能提供对动态类型的合理层次。一个良好设计的类层次可以为在基类中申明的每

sttony的专栏 1294

ANSI/ISO C++ Professional Programmer's Handbook(13)

 摘自:http://sttony.blogspot.com/search/label/C%2B%2B 13与C语言的兼容性问题by Danny Kalev简介ISO C 和ANSI/ISO C++的C子集之间的区别函数形参列表函数申明空的形参列表隐含的int 申明现在ISO C 做了修订,禁止了隐含int 申明和重复申明全局变量隐含的转换v

SearchLife的专栏 1072

ANSI/ISO C++ Professional Programmer's Handbook 11

ANSI/ISO C++ Professional Programmers Handbook 11内存管理by Danny Kalev 简介 存储的类型 自动存储 静态存储 自由存储

sttony的专栏 881

ANSI/ISO C++ Professional Programmer's Handbook 8

ANSI/ISO C++ Professional Programmers Handbook 8命名空间by Danny Kalev 命名空间后面的基本原理 简要的历史背景 大型工程更容易受命名冲突的影响 命名空间的属性

sttony的专栏 798

ANSI/ISO C++ Professional Programmer's Handbook 12

ANSI/ISO C++ Professional Programmers Handbook 12优化你的代码by Danny Kalev 简介 本章的范围 在优化你的软件之前 申明的布局 初始化比赋值好

sttony的专栏 964

ANSI/ISO C++ Professional Programmer's Handbook 5

ANSI/ISO C++ Professional Programmers Handbook5面向对象的编程和设计by Danny Kalev 简介 程序设计范例 过程化的程序设计 基于对象的程序设计 面向对象的程序设计

sttony的专栏 893

ANSI/ISO C++ Professional Programmer's Handbook 6

ANSI/ISO C++ Professional Programmers Handbook 6异常处理by Danny Kalev 简介 传统的错误处理方法 返回错误码 改变一个全局标志 终止程序运行

sttony的专栏 806
上一篇: ANSI/ISO C++ Professional Programmer's Handbook(10)
下一篇: ANSI/ISO C++ Professional Programmer's Handbook(8)
SearchLife
博客等级 码龄20年 51粉丝 22原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值