非类型模板参数如何使用&非类型模板参数使用时的注意事项&如何控制模板的实例化以节省内存空间
非类型模板参数
含有非类型模板参数的函数在重载时的注意事项
形式一:
#include <iostream> using namespace std; #include <vector> #include <algorithm> template <typename T, int val> T AddValue(T const& obj) { return obj val; } int main() { vector<int> Vector_Obj1{ 1,2,3,4,5,6 }, Vector_Obj2; Vector_Obj2.resize(Vector_Obj1.size()); transform(Vector_Obj1.begin(), Vector_Obj1.end(), Vector_Obj2.begin(), (int(*)(int const&))AddValue<int, 5>); for_each(Vector_Obj2.begin(), Vector_Obj2.end(), [](const int& obj) {cout << obj << " "; }); }
形式二:
#include <iostream> using namespace std; #include <vector> #include <algorithm> template <typename T, int val> T AddValue(T const& obj) { return obj val; } int main() { vector<int> Vector_Obj1{ 1,2,3,4,5,6 }, Vector_Obj2; Vector_Obj2.resize(Vector_Obj1.size()); transform(Vector_Obj1.begin(), Vector_Obj1.end(), Vector_Obj2.begin(), AddValue<int, 5>); for_each(Vector_Obj2.begin(), Vector_Obj2.end(), [](const int& obj) {cout << obj << " "; }); }
我们看到上述两种形式,最大的区别就在于:
①transform(Vector_Obj1.begin(), Vector_Obj1.end(), Vector_Obj2.begin(), (int(*)(int const&))AddValue<int, 5>);
② transform(Vector_Obj1.begin(), Vector_Obj1.end(), Vector_Obj2.begin(), AddValue<int, 5>);
我们有时就会产生疑问:这两种代码输出结果相同,但为什么形式一中要进行函数指针的强制类型转换呢?
在《C templates》中有这样一段话,引人深思:
这段话的意思是说:
AddValue<int,5>相当于一个函数指针,对于C 重载出的很多同名但是参数不同的函数来说,单纯的看AddValue这个独立的函数指针不看参数类型,我根本不知道AddValue这个函数指针指向函数的那个重载版本。虽然编译器可以在Transform中通过观察判断传递给AddValue函数指针的参数可以判断出是调用了那个函数重载版本,但是我们不清楚呀!因此我们要进行强制类型转换,虽然强转与否没啥卵用,但是至少可以使我们思路清晰,直到这个函数的到底调用了那个函数重载版本去操作的这一堆数据。
举例说明:
#include <iostream> using namespace std; #include <vector> #include <algorithm> template <typename T, int val> T AddValue(T const& obj) { return obj val; } template <typename T, int val> double AddValue(double const& obj) { return (obj (double)val) / 2; } int main() { vector<int> Vector_Obj1{ 1,2,3,4,5,6 }; vector<double> Vector_Obj2; Vector_Obj2.resize(Vector_Obj1.size()); transform(Vector_Obj1.begin(), Vector_Obj1.end(), Vector_Obj2.begin(), (double(*)(double const&))AddValue<int, 5>); for_each(Vector_Obj2.begin(), Vector_Obj2.end(), [](const double& obj) {cout << obj << " "; }); }
这个例子中,强制类型转换就很有必要了,没有了强制类型转换,编译器根本仅仅根据一个光溜溜的函数指针根本识别不出到底要执行那个函数重载版本,当编译器一出现徘徊不定的现象时,错误警告就接踵而至:
非类型模板的参数限制
① 不可以是浮点型类型的量(变量/常量):
匹配模板特化时,编译器会匹配模板参数,包括非类型参数。
就其本质而言,浮点值并不精确,并且C 标准未指定它们的实现。因此,很难确定两个浮点非类型参数何时真正匹配:
template <float f> void foo () ;void bar () { foo< (1.0/3.0) > ();foo< (7.0/21.0) > ();}
这些表达式不一定产生相同的“位模式”,因此不可能保证它们使用相同的特化 - 没有特殊的措辞来涵盖这一点。
② 非类型模板参数不能使用内部链接对象(例如string)作为实参
当我们在一个项目的多个源文件中使用相同的string字符串实例化非类型参数模板时,按理说相同的string字符串实例化出来的非类型参数模板应该是相同的,但是别忘了,我们实例化非类型参数模板是在不同的源文件中,不同源文件中的string字符串常量被存储在不同区域中,不同区域的数据实例化出来的非类型参数模板对于编译器来说是不相同的,这就有些可怕了,在我们看来相同的string字符串应该实例化出相同的模板才对,但是由于相同的string字符串定义文件不同,导致最终实例化出的模板不相同,针对于这种情况,编译器才给出了“非类型模板参数的形参不可以使用内部链接对象”。
如何使用string类型作为非类型模板参数呢?(改变链接属性)
C 规定,有const修饰的变量,不但不可修改,还都将具有内部链接属性,也就是只在本文件可见。(这是原来C语言的static修饰字的功能,现在const也有这个功能了。)又补充规定,extern const联合修饰时,extern将压制const这个内部链接属性。于是,extern const string将仍然有外部链接属性,但是还是不可修改的。
showing.hpp
#include <iostream> #include <string> using namespace std; template <const string& obj> void ShowInf() { cout << obj << endl; }
main.cpp
#include <iostream> #include "ShowInf.hpp" using namespace std; extern const string str = "hello world!"; int main() { ShowInf<str>(); }
输出结果:
注意:extern只能存在于main.cpp中函数体的外部,以下情形是不对的:
#include <iostream> #include "ShowInf.hpp" using namespace std; int main() { extern const string str = "hello world!"; ShowInf<str>(); }
Const char*变量为什么不可以呢?
你得用下述的方式传递const char数组:
Main.cpp
#include <iostream> #include <string> #include "Person.hpp" #include "ShowInf.hpp" using namespace std; const char s[] = "hello"; // 加不加extern都OK int main() { ShowInf<s>(); }
ShowInf.hpp
#include <iostream> #include <string> using namespace std; template <const char* obj> void ShowInf() { cout << obj << endl; }
注意:变量s一定要声明为:const char s[] = "hello",不要声明为const char* s = “hello”!
那用上述方式可不可以使用类类型作为非类型模板的实参呢?
我做了如下尝试,得出的答案是:不可以。
因为类类型是不可以作为编译期常量的。其实,你可以这样的巧记:类类型是多个基本数据类型的有机结合,float,double浮点型况且都不可以作为非类型模板的参数,类类型怎么又可以担当这个大任呢!
Person.hpp
#include <iostream> #include <string> using namespace std; class Person { private: int age; string name; public: Person(int age, string name) { this->age = age; this->name = name; } void Define(int age, string name) { this->age = age; this->name = name; } friend ostream& operator << (ostream& ostr, Person& obj); }; ostream& operator << (ostream& ostr, Person& obj) { ostr << obj.name << "的姓名为" << obj.age; return ostr; }
Showing.hpp
#include <iostream> #include <string> using namespace std; template <const Person& obj> void ShowInf() { cout << obj << endl; }
Main.cpp
#include <iostream> #include <string> #include "Person.hpp" #include "ShowInf.hpp" using namespace std; extern Person& obj; int main() { obj.Define(19, "张三"); ShowInf<obj>(); }
输出错误
什么是内部链接和外部链接?
内部链接就是该符号只在编译单元内有效,其他编译单元看不到。所以 多个编译单元中可有相同符号。const变量可以出现在多个.cpp文件中 而不会冲突就是因为是内部链接。
外部链接就是其他编译单元能看到当前编译单元的符号。如果有相同的外部链接符号,就会在链接时报重定义符号的错误。
对于参数为类型的模板,相同类型实例化出的模板会由于所属文件不同而被存储在不同存储区域,这对于我们宝贵的内存来说是一种非常嚣张的浪费,我们应该如何避免这种铺张浪费的情况呢?
模板实例化:
当模板被使用时才会实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。举个例子就是说,当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板适用该参数的一个实例。
上述的问题在小程序里不算什么,但是在一个大的程序中,在多个文件中实例化相同模板的额外开销会非常严重。在新标准中,我们可以通过控制显式实例化来避免这种开销。
控制实例化
与多文件中声明一个变量相同,可以使用关键字extern来承诺在程序其他位置会有一个该实例化的非extern声明,只需要在链接成可执行程序时将含有实例化的.o文件链接上就可以了。因此使用关键字extern进行一个模板的声明时不会在本文件中生成实例化代码。
如下演示一下具体如何操作:
第一个文件主要用来定义模板(当然这里的第一个文件代指了我们在项目中定义的许多模板文件):
// Stack.hpp#include <iostream> using namespace std; #include <vector> // 模板的缺省值 template <class T, class CONT = vector<T> > class Stack { private: CONT element; int Size; public: Stack(); Stack(T obj); Stack(vector<T> obj); void Push(T obj); void Pop(); T& Top(); bool Empty(); int PrintSize(); T& At(int order); }; template <class T, class CONT /*= vector<T> */> T& Stack<T, CONT>::At(int order) { if (order >= this->Size) { throw out_of_range("over array range!"); } return this->element.at(order); } template <class T, class CONT /*= vector<T> */> int Stack<T, CONT>::PrintSize() { return this->Size; } template <class T, class CONT /*= vector<T> */> bool Stack<T, CONT>::Empty() { return this->element.empty(); } template <class T, class CONT /*= vector<T> */> T& Stack<T, CONT>::Top() { if (this->Size == 0) { throw out_of_range("Stack is empty!"); } return *(this->element.end()); } template <class T, class CONT /*= vector<T> */> void Stack<T, CONT>::Pop() { if (this->Size == 0) { throw out_of_range("Stack is empty!"); } this->element.pop_back(); this->Size--; } template <class T, class CONT /*= vector<T> */> void Stack<T, CONT>::Push(T obj) { this->element.push_back(obj); this->Size ; } template <class T, class CONT /*= vector<T> */> Stack<T, CONT>::Stack(vector<T> obj) { this->Size = 0; this->element.clear(); this->element = obj; this->Size = obj.size(); } template <class T, class CONT /*= vector<T> */> Stack<T, CONT>::Stack(T obj) { this->Size = 0; this->element.clear(); this->element.push_back(obj); this->Size ; } template <class T, class CONT /*= vector<T> */> Stack<T, CONT>::Stack() { this->Size = 0; // 赋值前一定要初始化 this->element.clear(); }
在第二个文件中主要定义我们要在项目中使用到的所有实例化模板:
// TemplateBuild.cpp#include "Stack.hpp" #include <iostream> #include <string> using namespace std; template Stack<string>; // 加不加template关键字都OK template Stack<int>;
第三个文件中通过外部链接我们可以使用之前已经定义好的实例化模板:
// TemplateApplication.cpp#include "Stack.hpp" #include <iostream> #include <string> using namespace std; extern template Stack<string>; int main() { Stack<string> Stack_Obj1("张三"); cout << Stack_Obj1.At(0) << endl; }
在编译时,一个项目中的每个.cpp文件(TemplateApplication.cpp & TemplateBuild.cpp)都将会编译成相应的.o文件,编译器编译完后的下一步工作就是根据“外部链接关键字extern”将项目中得到的每个.o文件(TemplateApplication.o & TemplateBuild.o)链接到一起,生成可执行文件。
注意:由于我们使用外部链接,链接外部的已经实例化的模板并且使用,因此编译器需要实例化模板的所有成员,这与我们普通的模板实例化不同(不同模板实例化中,用到啥成员就实例化啥成员),这种实例化模板的方式会实例化模板的所有成员。
非类型参数模板的形参要求
非类型模板参数是有限制的,通常是常整数(包括枚举值)或者指向外部链接对象的指针/引用。
模板参数:常整数(int,short,long……可在编译器确定的整数数据类型)
#include <iostream> using namespace std; template <unsigned int N> void ShowInf() { cout << N << endl; } int main() { const int a = 10; ShowInf<a>(); }
模板参数:指向外部链接对象的指针/引用
#include <iostream> using namespace std; template <typename T, T(*Func)(const T& val1,const T& val2) > T Operator(T para1, T para2) { return Func(para1, para2); } int Add(const int& para1, const int& para2) { return para1 para2; } int main() { int para1 = 9, para2 = 10; cout << Operator<int, Add>(para1, para2) << endl; }
上述代码展示了,模板参数为函数指针时的代码书写格式。
模板参数:模板
#include <iostream> using namespace std; template<typename T> class Print { public: void operator()(const T& val) { cout << val << " "; } }; template<typename T> class Inc { public: void operator()(T& val) { val ; } }; template<typename T> class Dec { public: void operator()(T& val) { val--; } }; template< typename T, template<typename T> class Func> void Foreach(T Array[], unsigned size) { Func<T> FuncA; for (int i = 0; i < size; i ) { FuncA(Array[i]); // []为数组索引符号 } cout << endl; } int main() { int Array[] = { 1,2,3,4,5,6,7,8,9,10 }; Foreach<int, Inc>(Array, 10); Foreach<int, Print>(Array, 10); Foreach<int, Dec>(Array, 10); Foreach<int, Print>(Array, 10); }
运行结果: