C++基礎——非型別模板引數
非型別模板參看,顧名思義,模板引數不限定於型別,普通值也可作為模板引數。在基於型別的模板中,模板例項化時所依賴的是某一型別的模板引數,你定義了一些模板引數(
template<typename T>
)未加確定的程式碼,直到模板被例項化這些引數細節才真正被確定。而非型別模板引數,面對的未加確定的引數細節是指(value),而非型別。當要使用基於值的模板時,你必須顯式地指定這些值,模板方可被例項化。
非型別類模板引數
這裡我們使用一個新版本的Stack類模板,這類模板的底層容器是一個一維陣列,陣列的元素型別由模板型別引數typename T
指定,而一位陣列在初始化時必須指定其大小,這個大小便可通過一個非型別的模板引數int MAXSIZE
template<typename T, int MAXSIZE>
class Stack
{
public:
Stack():idx(0){}
bool empty() const { return idx == 0;}
bool full() const { return idx == MAXSIZE;}
void push(const T&);
void pop();
T& top();
const T& top() const;
private:
int idx;
T elems[MAXSIZE];
}
template<typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(const T& elem)
{
if (full())
throw std::out_of_range("Stack<>::push(): full stack");
elems[idx++] = elem;
}
template<typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::pop()
{
if (!empty())
idx--;
else
throw std::out_of_range("Stack<>::pop(): empty stack")
}
template<typename T, int MAXSIZE>
T& Stack<T, MAXSIZE>::top()
{
if (empty())
throw std::out_of_range("Stack<>::top(): empty stack");
return elems[idx-1];
}
template<typename T, int MAXSIZE>
const T& Stack<T, MAXSIZE>::top() const
{
if (empty())
throw std::out_of_range("Stack<>::top(): empty stack");
return elems[idx-1];
}
客戶端程式:
try
{
Stack<int, 10> int10Stack;
Stack<int, 20> int20Stack;
int20Stack.push(7);
...
}
catch(std::exception& ex)
{
cout << ex.what() << endl;
return EXIT_FAILURE;
}
每個模板例項都有自己的型別,
int10Stack
和int20Stack
屬於不同的型別,這兩種型別之間也不存在顯示或隱式的型別轉換。
同樣地,也可以為非型別模板引數指定預設值:
template<typename T, int MAXSIZE = 20>
class Stack
{
...
}
這樣在呼叫時:
Stack<int> intStack; // == Stack<int, 20>
非型別函式模板引數
同樣地也可以為函式模板定義為非型別引數,如下的函式模板定義一組用於增加特定值的函式:
template<typename T, int VAL>
T addValue(const T& x)
{
return x + VAL;
}
當然這樣做純粹為了演示,意義不大,我們可以將非型別引數的型別(這話有點繞)定義為型別引數:
template<typename T, T VAL>
T addValue(const T& x)
{
return x+VAL;
}
千萬不要小瞧這樣的簡單機制(整個計算機理論的恢弘大廈的本質也還是0/1呢),如果把函式(仿函式)或者某一操作當做引數傳遞給某一函式,這些實現了一些簡單的功能的函式將產生巨大的作用。例如,藉助標準模板庫(STL),可以將這個函式模板的一個例項傳遞給集合中的每一個元素,將集合中的每一個值都增加一個預設的值。
#include <algorithm>
std::vector<int> ivec;
std::transform(src.begin(), src.end(), // 原容器(待轉換)的起點和終點
dst.begin(), // 目標容器的起點
addValue<std::vector<int>::value_type, 10>); // 操作或者函式(也可以是仿函式)
這裡要想成為STL <algorithm>
演算法中某一函式的引數(尤其是在內部作為函式使用的),需要滿足一定的要求,比如本例中的std::transform()
,傳遞進來的在內部作為函式使用的第四個引數,只能接受一個引數,且需返回一個值(返回值的型別就是目標容器的元素型別),這是基本要求。
另外還有一個問題需要注意,addValue<int, 5>
是一個函式模板的例項化版本,而函式模板的例項化通常被視為用來命名一組過載函式的集合(即使該集合中只有一個元素)。在一些較老的C++標準裡,過載函式的集合不能用於模板引數(如本例的transform()
)的演繹。於是,必須顯式地將函式模板的實參強制轉換為具體的型別:
std::transform(ivec.begin(), ivec.end(), dst.begin(),
(int(*)(const int& ))addValue<int, 5>);
一個完整的演示程式即是:
int arr[] = {1, 2, 3, 4, 5};
vector<int> src(arr, arr+5), dst(5);
typedef vector<int>::value_type value_type;
transform(src.begin(), src.end(), dst.begin(),
(value_type (*)(const value_type&)addValue<value_type, 5>);
copy(dst.begin(), dst.end(), ostream_iterator<value_type>(cout, " "));
// ostream_iterator 在<iterator>的std名稱空間中
非型別模板引數的限制
非型別模板引數是有型別限制的。一般而言,它可以是常整數(包括enum列舉型別)或者指向外部連結物件的指標。
浮點數和類物件(class-type)不允許作為非型別模板引數:
template<double VAL> // ERROR: 浮點數不可作為非型別模板引數
double process(double v)
{
return v * VAL;
}
template<std::string name> // ERROR:類物件不能作為非型別模板引數
class MyClass
{}
稍作變通,我們即可使編譯通過:
template<double* PVAL>
double process(const double& x)
{
return x * (*PVAL);
}
template<const char* name>
class MyClass
{
...
}
這樣可順利通過編譯,但如果想在當前檔案中使用這兩個模板,還需要動一些手腳:
double val = 10;
double res = process<&val>(20); // ERROR: 表示式必須含有常量值
MyClass<"hello"> x; // ERROR: 模板引數不能引用非外部實體
const char* s = "hello";
MyClass<s> x; // ERROR: 表示式必須含有常量值
這裡就點出另外一點注意事項,也就是非型別模板引數的限制,非型別模板引數可以是指標,但該指標必須指向外部連結物件,還記得在A.cpp
中如何引用B.cpp
中的全域性變數嗎,在A.hpp
中使用extern
關鍵字對外部變數加以引用。
// B.cpp
double val = 3.14159265;
char str[] = "hello";
// A.hpp
extern double val;
extern char str[];
// A.cpp
#include "A.hpp"
double res = process<&val>(10);
MyClass<str> x;