在C++11之前,類模板和函數模板只能含有固定數量的模板參數。C++11增強了模板功能,允許模板定義中包含0到任意個模板參數,這就是可變參數模板。可變參數模板的加入使得C++11的功能變得更加強大,而由此也帶來了許多神奇的用法。
本文實例源碼github地址:https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2020Q2/20200401。
可變參數模板
可變參數模板和普通模板的語義是一樣的,只是寫法上稍有區別,聲明可變參數模板時需要在typename
或class
後面帶上省略號...
:
template<typename... Types>
其中,...
可接納的模板參數個數是0個及以上的任意數量,需要注意包括0個。
若不希望產生模板參數個數爲0的變長參數模板,則可以採用以下的定義:
template<typename Head, typename... Tail>
本質上,...
可接納的模板參數個數仍然是0個及以上的任意數量,但由於多了一個Head類型,由此該模板可以接納1個及其以上的模板參數。
函數模板的使用
在函數模板中,可變參數模板最常見的使用場景是以遞歸的方法取出可用參數:
void print() {}
template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
std::cout << firstArg << " " << sizeof...(args) << std::endl;
print(args...);
}
通過設置...
,可以向print函數傳遞任意個數的參數,並且各個參數的類型也是任意。也就是說,可以允許模板參數接受任意多個不同類型的不同參數。這就是不定參數的模板,格外需要關注的是,...
三次出現的位置。
如果如下調用print函數:
print(2, "hello", 1);
如此調用會遞歸將3個參數全部打印。細心的話會發現定義了一個空的print函數,這是因爲當使用可變參數的模板,需要定義一個處理最後情況的函數,如果不寫,會編譯錯誤。這種遞歸的方式,是不是覺得很驚豔!
在不定參數的模板函數中,還可以通過如下方式獲得args的參數個數:
sizeof...(args)
假設,在上面代碼的基礎上再加上一個模板函數如下,那麼運行的結果是什麼呢?
#include <iostream>
void print() {}
template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
std::cout << firstArg << " " << sizeof...(args) << std::endl;
print(args...);
}
template <typename... Types>
void print(const Types&... args) {
std::cout << "print(...)" << std::endl;
}
int main(int argc, char *argv[]) {
print(2, "hello", 1);
return 0;
}
現在有一個模板函數接納一個參數加上可變參數,還有一個模板函數直接接納可變參數,如果調用print(2, “hello”, 1),會發現這兩個模板函數的參數格式都符合。是否會出現衝突、不衝突的話會執行哪一個呢?
運行代碼後的結果爲:
yngzmiao@yngzmiao-virtual-machine:~/test/$ ./main
2 2
hello 1
1 0
從結果上可以看出,程序最終選擇了一個參數加上不定參數的模板函數。也就是說,當較泛化和較特化的模板函數同時存在的時候,最終程序會執行較特化的那一個。
再比如一個例子,std::max函數只可以返回兩個數的較大者,如果多個數,就可以通過不定參數的模板來實現:
#include <iostream>
template <typename T>
T my_max(T value) {
return value;
}
template <typename T, typename... Types>
T my_max(T value, Types... args) {
return std::max(value, my_max(args...));
}
int main(int argc, char *argv[]) {
std::cout << my_max(1, 5, 8, 4, 6) << std::endl;
return 0;
}
類模板的使用
除了函數模板的使用外,類模板也可以使用不定參數的模板參數,最典型的就是tuple
類了。其大致代碼如下:
#include <iostream>
template<typename... Values> class tuple;
template<> class tuple<> {};
template<typename Head, typename... Tail>
class tuple<Head, Tail...>
: private tuple<Tail...>
{
typedef tuple<Tail...> inherited;
public:
tuple() {}
tuple(Head v, Tail... vtail) : m_head(v), inherited(vtail...) {}
Head& head() {return m_head;}
inherited& tail() {return *this;}
protected:
Head m_head;
};
int main(int argc, char *argv[]) {
tuple<int, float, std::string> t(1, 2.3, "hello");
std::cout << t.head() << " " << t.tail().head() << " " << t.tail().tail().head() << std::endl;
return 0;
}
根據代碼可以知道,tuple類繼承除首之外的其他參數的子tuple類,以此類推,最終繼承空參數的tuple類。繼承關係可以表述爲:
tuple<>
↑
tuple<std::string>
string "hello"
↑
tuple<float, std::string>
float 2.3
↑
tuple<int, float, std::string>
int 1
接下來考慮在內存中的分佈,內存中先存儲父類的變量成員,再保存子類的變量成員,也就是說,對象t按照內存分佈來說;
┌─────────┐<---- 對象指針
| hello |
|─────────|
| 2.3 |
|─────────|
| 1 |
└─────────┘
這時候就可以知道下一句代碼的含義了:
inherited& tail() {return *this;}
tail()函數返回的是父類對象,父類對象和子類對象的內存起始地址其實是一樣的,因此返回*this,再強行轉化爲inherited類型。
當然,上面採用的是遞歸繼承的方式,除此之外,還可以採用遞歸複合的方式:
template<typename... Values> class tup;
template<> class tup<> {};
template<typename Head, typename... Tail>
class tup<Head, Tail...>
{
typedef tup<Tail...> composited;
public:
tup() {}
tup(Head v, Tail... vtail) : m_head(v), m_tail(vtail...) {}
Head& head() {return m_head;}
composited& tail() {return m_tail;}
protected:
Head m_head;
composited m_tail;
};
兩種方式在使用的過程中沒有什麼區別,但C++11中採用的是第一種方式(遞歸繼承)。
在上面的例子中,取出tuple中的元素是一個比較複雜的操作,需要不斷地取tail,最終取head才能獲得。標準庫的std::tuple,對此進行簡化,還提供了一些其他的函數來進行對tuple的訪問。例如:
#include <iostream>
#include <tuple>
int main(int argc, char *argv[]) {
std::tuple<int, float, std::string> t2(1, 2.3, "hello");
std::get<0>(t2) = 4; // 修改tuple內的元素
std::cout << std::get<0>(t2) << " " << std::get<1>(t2) << " " << std::get<2>(t2) << std::endl; // 獲取tuple內的元素
auto t3 = std::make_tuple(2, 3.4, "World"); // make方法生成tuple對象
std::cout << std::tuple_size<decltype(t3)>::value << std::endl; // 獲取tuple對象元素的個數
std::tuple_element<1, decltype(t3)>::type f = 1.2; // 獲取tuple對象某元素的類型
return 0;
}