首頁 > 軟體

淺析C++可變引數模板的展開方式

2022-04-06 19:04:04

前言

可變引數模板(variadic templates)是C++11新增的強大的特性之一,它對模板引數進行了高度泛化,能表示0到任意個數、任意型別的引數。相比C++98/03這些類模版和函數模版中只能含固定數量模版引數的“老古董”,可變模版引數無疑是一個巨大的進步。

如果是剛接觸可變引數模板可能會覺得比較抽象,使用起來會不太順手,使用可變引數模板時通常離不開模板引數的展開,所以本文來列舉一些常用的模板展開方式,幫助我們來對可變引數模板有一個初步的瞭解。

可變引數模板的定義

可變引數模板和普通模板的定義類似,在寫法上需要在 typenameclass 後面帶上省略號...,以下為一個常見的可變引數函數模板:

template <class... T>
void func(T... args)
{
    //...
}

上面這個函數模板的引數 args 前面有省略號,所以它就是一個被稱為模板引數包(template parameter pack)的可變模版引數,它裡面包含了0到N個模版引數,而我們是無法直接獲取 args 中的每個引數的,只能通過展開引數包的方式來獲取引數包中的每個引數,這也是本文要重點總結的內容。

引數包的展開

引數包展開的方式隨著c++語言的發展也在與時俱進,我們以實現一個可變參格式化列印函數為例,列舉一些常用的方式:

遞迴函數方式展開

#include <iostream>
void FormatPrint()
{
    std::cout << std::endl;
}
template <class T, class ...Args>
void FormatPrint(T first, Args... args)
{
   std::cout << "[" << first << "]";
   FormatPrint(args...);
}
int main(void)
{
   FormatPrint(1, 2, 3, 4);
   FormatPrint("good", 2, "hello", 4, 110);
   return 0;
}

這種遞迴展開的方式與遞迴函數的定義是一樣的,需要遞迴出口和不斷呼叫自身,仔細看看這個函數模板是不是都滿足啦?遞迴出口就是這個無模板引數的 FormatPrint,並且在有參模板中一直在呼叫自身,遞迴呼叫的過程時這樣的 FormatPrint(4,3,2,1) -> FormatPrint(3,2,1) -> FormatPrint(2,1) -> FormatPrint(1) -> FormatPrint(),輸出內容如下:

>albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++11
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]

逗號表示式展開

#include <iostream>
template <class ...Args>
void FormatPrint(Args... args)
{
   (void)std::initializer_list<int>{ (std::cout << "[" << args << "]", 0)... };
   std::cout << std::endl;
}

int main(void)
{
   FormatPrint(1, 2, 3, 4);
   FormatPrint("good", 2, "hello", 4, 110);
   return 0;
}

這種方式用到了C++11的新特性初始化列表(Initializer lists)以及很傳統的逗號表示式,我們知道逗號表示式的優先順序最低,(a, b) 這個表示式的值就是 b,那麼上述程式碼中(std::cout << "[" << args << "]", 0)這個表示式的值就是0,初始化列表保證其中的內容從左往右執行,args引數包會被逐步展開,表示式前的(void)是為了防止變數未使用的警告,執行過後我們就得到了一個N個元素為0的初始化列表,內容也被格式化輸出了:

albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++11
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]

說到這順便提一下,可以使用sizeof...(args)得到引數包中引數個數。

enable_if方式展開

#include <iostream>
#include <tuple>
#include <type_traits>
template<std::size_t k = 0, typename tup>
typename std::enable_if<k == std::tuple_size<tup>::value>::type FormatTuple(const tup& t)
{
    std::cout << std::endl;
}
template<std::size_t k = 0, typename tup>
typename std::enable_if<k < std::tuple_size<tup>::value>::type FormatTuple(const tup& t){
    std::cout << "[" << std::get<k>(t) << "]";
    FormatTuple<k + 1>(t);
}
template<typename... Args>
void FormatPrint(Args... args)
{
    FormatTuple(std::make_tuple(args...));
}
int main(void)
{
   FormatPrint(1, 2, 3, 4);
   FormatPrint("good", 2, "hello", 4, 110);
   return 0;
}

C++11的enable_if常用於構建需要根據不同的型別的條件範例化不同模板的時候。顧名思義,當滿足條件時型別有效。可作為選擇型別的小工具,其廣泛的應用在 C++ 的模板超程式設計(meta programming)之中,利用的就是SFINAE原則,英文全稱為Substitution failure is not an error,意思就是匹配失敗不是錯誤,假如有一個特化會導致編譯時錯誤,只要還有別的選擇,那麼就無視這個特化錯誤而去選擇另外的實現,這裡的特化概念不再展開,感興趣可以自行了解,後續可以單獨總結一下。

在上面的程式碼實現中,基本思路是先將可變模版引數轉換為std::tuple,然後通過遞增引數的索引來選擇恰當的FormatTuple函數,當引數的索引小於tuple元素個數時,會不斷取出當前索引位置的引數並輸出,當引數索引等於總的引數個數時呼叫另一個模板過載函數終止遞迴,編譯執行輸入以下內容:

albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++11
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]

摺疊表示式展開(c++17)

#include <iostream>
template<typename... Args>
void FormatPrint(Args... args)
{
    (std::cout << ... << args) << std::endl;
}
int main(void)
{
   FormatPrint(1, 2, 3, 4);
   FormatPrint("good", 2, "hello", 4, 110);
   return 0;
}

摺疊表示式(Fold Expressions)是C++17新引進的語法特性,使用摺疊表示式可以簡化對C++11中引入的引數包的處理,可以在某些情況下避免使用遞迴,更加方便的展開引數,如上述程式碼中展示的這樣可以方便的展開引數包,不過輸出的內容和之前的有些不一樣:

albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++17
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
1234
good2hello4110

對比結果發現缺少了格式化的資訊,需要以輔助函數的方式來格式化:

#include <iostream>
template<typename T>
string format(T t) {
    std::stringstream ss;
    ss << "[" << t << "]";
    return ss.str();
}
template<typename... Args>
void FormatPrint(Args... args)
{
    (std::cout << ... << format(args)) << std::endl;
}
int main(void)
{
   FormatPrint(1, 2, 3, 4);
   FormatPrint("good", 2, "hello", 4, 110);
   return 0;
}

這次格式化內容就被加進來了:

albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++17
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]

這樣好像還是有點麻煩,我們可以把摺疊表示式和逗號表示式組合使用,這樣得到的程式碼就簡單多啦,也能完成格式化輸出的任務:

#include <iostream>
template<typename... Args>
void FormatPrint(Args... args)
{
    (std::cout << ... << (std::cout << "[" << args, "]")) << std::endl;
}
int main(void)
{
   FormatPrint(1, 2, 3, 4);
   FormatPrint("good", 2, "hello", 4, 110);
   return 0;
}

總結

Variadic templates 是C++11新增的強大的特性之一,它對模板引數進行了高度泛化Initializer lists 是C++11新加的特性,可以作為函數引數和返回值,長度不受限制比較方便Fold Expressions 是C++17新引進的語法特性,可以方便的展開可變引數模板的引數包可變引數模板的引數包在C++11的環境下,可以利用遞迴、逗號表示式、enable_if等方式進行展開

==>> 反爬連結,請勿點選,原地爆炸,概不負責!<<==

到此這篇關於C++可變引數模板的展開方式的文章就介紹到這了,更多相關C++模板展開方式內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com