概述
大家在C++中應該見過不少函數,它們既沒有限制參數的類型,也沒有限制參數的個數,比如vector<T>::emplace()
,make_unique<T>()
。它們都是利用C++11中的可變模板參數來實現的。對於這一新特性,需要掌握以下三點
- 可變模板參數的語法
- 參數包的展開
- 實踐
前言
在講可變模板參數之前,需要先講C語言中的變長參數
#include<stdarg.h>
#define END (-1)
int sum(int num, ...)
{
va_list list;
va_start(list, num);
int sum = 0;
for (int cur = num; cur != END; cur = va_arg(list, int))
sum += cur;
va_end(list);
return sum;
}
int main()
{
// 輸出15
cout << sum(1, 2, 3, 4, 5, END) << endl;
}
C語言中的va_arg
宏並不能判斷出哪個參數參數包的末尾,所以只能通過自己設定結束位,並通過顯式判斷來截取有效的參數
而在C++11中,使用變長參數更簡單了(好吧其實也不怎么算變長)
int sum(std::initializer_list<int> initializerList)
{
int sum = 0;
for (const int& i : initializerList)
sum += i;
return sum;
}
int main()
{
std::cout << sum({1, 2, 3, 4, 5}) << std::endl;
}
可變模板參數的語法
函數
以C++11的標准來看,聲明一個可變參數的模板函數有兩種方法
template<typename... Ts>
void AnyNumberOfParam(Ts... ars) {}
int main()
{
// 可以接受0及以上個參數
AnyNumberOfParam();
AnyNumberOfParam(1, 2);
AnyNumberOfParam("824", std::vector<int>(), 3);
}
template<typename T, typename... Ts>
void AnyNumberOfParam(T least, Ts... ars) {}
int main()
{
// 至少需要有一個參數
// AnyNumberOfParam();
AnyNumberOfParam(1, 2);
AnyNumberOfParam("824", std::vector<int>(), 3);
}
類
template<typename T, typename... Ts>
class TestTemplateClass {};
int main()
{
TestTemplateClass<int, std::string> t;
}
template<typename... Ts>
class TestTemplateClass {};
int main()
{
// 因為支持0及以上的參數 所以這么寫是合法的
TestTemplateClass t;
}
函數參數包的展開
C++11中的展開方式
遞歸展開
假設我們通過設計一個函數,能逐個輸出它的參數
void print()
{
std::cout << "empty" << std::endl;
}
template<typename T, typename... Ts>
void print(T first, Ts... args)
{
std::cout << first << std::endl;
print(args...);
}
int main()
{
print(1, 2, "Mike", 3.21);
std::cout << std::endl;
print();
}
以上代碼的遞歸過程為
print(1, 2, "Mike", 3.21);
print(2, "Mike", 3.21);
print("Mike", 3.21);
print(3.21);
print();

通過遞歸方式展開參數包,當所有參數包展開完畢后,自然為空,所以調用到非模板的遞歸中止函數
當然還可以使用模板遞歸中止函數,這種情況就不支持空參數包了
template<typename T>
void print(T end)
{
std::cout << end << std::endl;
}
template<typename T, typename... Ts>
void print(T first, Ts... args)
{
std::cout << first << std::endl;
print(args...);
}
int main()
{
print(1, 2, "Mike", 3.21);
// print();
}
以上代碼的遞歸過程為
print(1, 2, "Mike", 3.21);
print(2, "Mike", 3.21);
print("Mike", 3.21);
print(3.21);
而在C++17中,對遞歸展開法進行了優化(前提是將if
語句聲明為常量表達式)
template<typename T, typename... Ts>
void print(T first, Ts... args)
{
std::cout << first << std::endl;
if constexpr(sizeof...(args) > 0)
print(args...);
}
逗號表達式搭配initializer_list展開
在C++11中通過遞歸展開參數包的缺點很明顯,需要重載一個遞歸終止函數,同時還需要判定終止函數是否需要使用到模板;不僅如此,還需要確保帶參數包版本的函數至少包含一個類型(T fistst
),可以說是很不便了。下面介紹逗號表達式結合initializer_list
的展開方法
template<typename... Ts>
void print(Ts&&... args)
{
auto lambda = [](auto&& data) { std::cout << data << std::endl; };
std::initializer_list<int> il = { (lambda(std::forward<Ts>(args)), 0)... };
}
// C++現代教程上的做法
template<typename... Ts>
void print(Ts... args)
{
std::initializer_list<int>{([&args]() { std::cout << args << std::endl; }(), 0)... };
}
int main()
{
print(1, 2, "Mike", 3.21);
print();
}
這種搭配initializer_list
的解法我願稱之為黑魔法,在構造初始化列表的同時完成了參數包的展開
以下示范一個求和模板函數,如果我們直接使用參數包進行操作而不展開它,那么我們將會得到報錯

C++17中的折疊表達式
一元折疊表達式
先來看看如何在C++17中利用折疊表達式(...)
展開參數包實現一個求平均數的函數
template<typename... T>
int avg(T... args) {
// 右折疊
return (args + ...) / sizeof...(args);
}
int main()
{
// 輸出26
cout << avg(1, 2, 5, 'a') << endl;
// 編譯出錯
// cout << avg() << endl;
}
對於+運算符,符合交換律,所以左折疊和右折疊的結果相同
對於-
運算符,不符合交換律,因此左右折疊的結果不同
template<typename... Ts>
decltype(auto) sub_right(Ts... args) { return (args - ...); }
template<typename... Ts>
decltype(auto) sub_left(Ts... args) { return (... - args); }
int main()
{
// (((10 - 5) - 2) - 8)
std::cout << sub_left(10, 5, 2, 8) << std::endl;
// (10 - (5 - (2 - 8)))
std::cout << sub_right(10, 5, 2, 8) << std::endl;
}
對於一元折疊表達式而言,只有,
,&&
和||
操作允許空包,其它的如果出現空包則會編譯出錯
When a unary fold is used with a pack expansion of length zero, only the following operators are allowed:
Logical AND (&&). The value for the empty pack is true
Logical OR (||). The value for the empty pack is false
The comma operator (,). The value for the empty pack is void()
// 一元折疊逗號表達式
template<typename ...Args>
void printer(Args&&... args)
{
(..., (std::cout << args << std::endl)); // 左折疊 且每個參數間隔輸出std::endl
// ((std::cout << args), ...); // 右折疊
}
int main()
{
// (((1 << 2) << Mike) << 3.21) 左折疊
// 1 \n 2 \n Mike \n 3.21
printer(1, 2, "Mike", 3.21);
}
二元折疊表達式
二元折疊表達式,支持空包操作。二元折疊表達式的省略號(...
)永遠在中間,特例在左還是在右決定了是左折疊還是右折疊
對於std::cout
的二元表達式而言,只能使用左折疊(因為輸出必須以std::cout
開頭,而這也就代表了它是左折疊)
template<typename ...Args>
void printer(Args&&... args)
{
// 二元左折疊
(std::cout << ... << args);
}
template<typename ... Ts>
void printer_space(Ts&&... args)
{
auto lambda = [] (auto params)
{
cout << ends;
return params;
};
(std::cout << ... << lambda(args));
}
int main()
{
// 12Mike3.21
printer(1, 2, "Mike", 3.21);
// nothing...
printer();
// 1 2 Mike 3.21
printer_space(1, 2, "Mike", 3.21);
}
拓展:以下代碼是在參數包展開完畢之后再輸出std::endl
,而不是每拆一次就輸出一次
template<typename ...Args>
void printer(Args&&... args)
{
(std::cout << ... << args) << std::endl;
}
如何評價

類參數包的展開
C++11中的函數參數包可以使用遞歸或逗號表達式來展開,C++17中則可以使用優化的遞歸或折疊表達式來展開
C++11中的類參數包的展開需要運用到類模板的特化(因為筆者也搞不清楚以下代碼是屬於偏特化還是全特化,所以統一寫成特化)
遞歸展開
// 一個支持1及多個類型的類
template<typename T, typename... Ts>
class TestClass
{
public:
// 匿名枚舉 遞歸展開
enum { value = TestClass<T>::value + TestClass<Ts...>::value };
};
// 對1個類型的情況進行特化 遞歸中止類
template<typename lastT>
class TestClass<lastT>
{
public:
enum { value = sizeof(lastT) };
};
int main()
{
// 4 + 8 + 1 = 13
std::cout << TestClass<int, double, char>::value << std::endl;
// 在64位環境下大小為32
std::cout << TestClass<std::vector<int>>::value << std::endl;
// TestClass<>::value;
}
此展開方式不支持0參數包,因此可以改寫為以下方式
// 只聲明一個支持0及以上個類型的類
template<typename... Args>
class TestClass;
// 對1及以上個類型進行特化
template<typename First, typename... Rest>
class TestClass<First, Rest...>
{
public:
// 遞歸展開
enum { value = TestClass<First>::value + TestClass<Rest...>::value };
};
// 對1個類型進行特化 即遞歸終止類
template<typename First>
class TestClass<First>
{
public:
enum { value = sizeof(First) };
};
// 對0個類型進行特化
template<>
class TestClass<>
{
public:
enum { value = 0 };
};
對上面的代碼可能會產生以下幾點疑問
-
為什么只需要聲明
TestClass
主體類而不用實現它,看看以下一個簡單的實例你就明白了template<typename T> class TestTemplate; template<> class TestTemplate<int> { public: int data; }; template<> class TestTemplate<std::string> { public: std::string name; }; int main() { // 只能實例化出特化的int和string類型 TestTemplate<int> it{}; // TestTemplate<char> ct{}; }
-
為什么感覺這個特化並不是很特化的樣子
template<typename First, typename... Rest> class TestClass<First, Rest...> { // codes... }
因為這里是根據參數的個數進行特化,而不是根據類型進行特化
你可能會覺得用匿名enum
來扮演一個編譯期常量有點撈,那么接下來使用同樣具有編譯期常量特性的std::integral_constant
注意是
integral
不是interger
template<typename T, typename... Ts>
class TestTemplate : public std::integral_constant<int, TestTemplate<T>::value + TestTemplate<Ts...>::value>
{
};
template<typename T>
class TestTemplate<T> : public std::integral_constant<int, sizeof(T)>
{
};
int main()
{
cout << TestTemplate<int, double, char>::value << endl;
// TestTemplate<>::value;
}
有人可能會說了,那你加一個無類型的特化不就得了嗎,其實不是的。因為我們模板類的主體是template<typename T, typename... Ts>
,這也就意味着需要1及以上個的類型,那么此時針對一個不滿足主體的特化肯定是不正確的。至於如何實現0個類型,上文中給出了匿名enum
版本的實現,這里不再贅述
// 錯誤的特化方式 編譯將出錯
template<>
class TestTemplate<> : public std::integral_constant<int, 0>
{
};
繼承展開
C++11中的std::tuple
就是使用繼承展開參數包的
std::tuple
可以看作是std::pair
的升級版,它支持0-多個類型參數
std::tuple<int, double, std::string> myTuple{1, 3.14, "pi"};
auto& [intX, doubleY, strZ] = myTuple;
intX = 200;
// 200
std::cout << std::get<0>(myTuple) << std::endl;
手撕std::tuple
的代碼將放在后文的實踐部分
實踐
手撕std::tuple
本文實現的MyTuple
的功能十分有限,在實際應用中可能會出現各種不必要的拷貝,以及編譯無法通過等問題。以下代碼只當作核心功能的剖析,應當作練習看待
tuple
// 聲明一個支持0及以上個參數包的模板類
template<typename... Ts>
class MyTuple;
// 空MyTuple 偷懶不做實現
template<>
class MyTuple<> {};
// MyTuple主要實現 繼承展開參數包
template<typename T, typename... Ts>
class MyTuple<T, Ts...> : public MyTuple<Ts...>
{
private:
T data;
using TopTuple = MyTuple<Ts...>;
public:
MyTuple() = default;
// 通用引用有參構造函數
template<typename ThisType, typename... RestTypes>
MyTuple(ThisType&& _data, RestTypes&&... _args) : data(std::forward<ThisType>(_data)), TopTuple(std::forward<RestTypes>(_args)...) {}
// 常函數版本get<>
template<std::size_t index>
constexpr auto& get() const
{
// 靜態斷言防止越界訪問
static_assert(index <= sizeof...(Ts), "out of range");
if constexpr (index == 0)
return data;
else
return TopTuple::template get<index - 1>();
}
template<std::size_t index>
constexpr auto& get()
{
// 調用常函數版本的get<>
using element_type = my_tuple_element_type<index, MyTuple<T, Ts...>>;
return const_cast<element_type&>((static_cast<const MyTuple<T, Ts...>&>(*this)).template get<index>());
}
};
int main()
{
MyTuple<int, std::string, double> t(1, "Jelly", 3.14);
// 輸出Jelly
std::cout << t.get<1>() << std::endl;
}
實現了一個非常簡單的MyTuple
,沒有考慮復雜的拷貝,移動或賦值等問題。和庫中的版本一樣,都使用了繼承的方式展開參數包,每一層MyTuple
儲存一個類型的數據。
tuple_size
下面實現一個個數萃取機,能獲得MyTuple
中存儲的類型個數
template<typename>
struct my_tuple_size;
// 對MyTuple格式的類型進行特化
template<template<typename...> typename TupleType, typename... Ts>
struct my_tuple_size<TupleType<Ts...>> : std::integral_constant<std::size_t, sizeof...(Ts)> {};
template<typename TupleType>
inline constexpr std::size_t my_tuple_size_value = my_tuple_size<TupleType>::value;
tuple_element
再來實現一個類型萃取機,能獲得第幾號元素是什么類型
template<std::size_t, typename>
struct my_tuple_element;
// 對MyTuple格式的類型進行特化
template<std::size_t index, template<typename...> typename TupleType, typename T, typename... Ts>
struct my_tuple_element<index, TupleType<T, Ts...>> : my_tuple_element<index - 1, TupleType<Ts...>> {};
// 特化出一個繼承終止類
template<template<typename...> typename TupleType, typename T, typename... Ts>
struct my_tuple_element<0, TupleType<T, Ts...>> {
using type = T;
};
template<std::size_t index, typename TupleType>
using my_tuple_element_type = typename my_tuple_element<index, TupleType>::type;
make_tuple
再來實現一個my_make_tuple
,下面給出一個錯誤實現
template<typename... Ts>
MyTuple<Ts...> my_make_tuple(Ts&&... args) {
return MyTuple<Ts...>(std::forward<Ts>(args)...);
}
乍一看好像很對,對參數包進行展開然后完美轉發。這么想就忽略了通用引用的特性,對於左值類型,通用引用會推導出T&
;對於右值類型會推導出T
那么我們使用左值類型進行my_make_tuple
時,將會推導出錯誤的類型
int main()
{
int a = 10;
const int b = 20;
string name = "Jelly";
MyTuple<int&, const int&, std::string&, double> t = my_make_tuple(a, b, name, 3.14);
}
那么關鍵就是:用什么辦法能去除掉類型的const
,volatile
,reference
屬性等等呢,答案是退化
std::decay
在C++ 11中的右值引用中,我提到了std::remove_reference_t
,用於去除類型的引用屬性,但是std::decay
要更猛一些
- 如果是引用類型,會將其消除
- 如果又是數組類型,會退化為指針
- 如果又是函數,會退化為函數指針
- 同時會消除對象的cv屬性
std::ref
但是問題又來了,這樣不分青紅皂白的退化一個類型,也使得我們沒有辦法創建一個記錄引用類型變量的元組。std::ref
就是為此而生的,它會將參數包裝成std::reference_wrapper
對象
最終實現
因此還需要進一步特化,正確的實現為:
template<typename T>
struct remove_ref_wrap {
using type = T;
};
template<typename T>
struct remove_ref_wrap<std::reference_wrapper<T>> {
using type = T&;
};
template<typename T>
using with_ref_decay_t = typename remove_ref_wrap<std::decay_t<T>>::type;
template<typename... Ts>
constexpr MyTuple<with_ref_decay_t<Ts>...> my_make_tuple(Ts&&... args) {
return MyTuple<with_ref_decay_t<Ts>...>(std::forward<Ts>(args)...);
}
先執行一次std::decay_t<T>
,然后再萃取出std::reference_wrapper
對象,為其施加引用。std::decay
不會去掉std::reference_wrapper
,因為它是經過包裝的對象而不是類型
int main()
{
int a = 10;
float height = 1.7;
MyTuple<int&, double, float> t = my_make_tuple(std::ref(a), 3.14, height);
}
不足之處
對於有隱式轉換的類型,需要顯式指明my_make_tuple
的類型
MyTuple<std::string> t = my_make_tuple<std::string>("Jelly");
traverse_tuple
通過傳入函數對象進行對MyTuple
的遍歷
// 利用折疊表達式執行回調
template<typename TupleType, typename FuncType, std::size_t... Index>
void call_tuple(const TupleType& t, const FuncType& f, std::index_sequence<Index...>) {
(f(t.template get<Index>()), ...);
}
template<template<typename...> typename TupleType, typename... Ts, typename FuncType>
void my_traverse_tuple(const TupleType<Ts...>& t, const FuncType& f) {
call_tuple(t, f, std::make_index_sequence<my_tuple_size_value<TupleType<Ts...>>>{});
}
測試代碼為
int main()
{
MyTuple<int, std::string> t = my_make_tuple(1, "123");
my_traverse_tuple(t, [](auto&& data) { std::cout << data << std::endl; });
}
std::apply
c++17中引入的對std::tuple
的一種遍歷方式,傳入的回調函數的參數為參數包
int main()
{
std::tuple<int, std::string, float> t = std::make_tuple(1, "Jelly", 3.14);
// 1Jelly3.14
std::apply([](auto&&... params) { (std::cout << ... << params); }, t);
}
完善
經過不懈的努力,我終於掌握了如何解決上面中提到的不足之處
template<typename... Ts>
class MyTuple;
template<>
class MyTuple<> {};
template<typename T, typename... Ts>
class MyTuple<T, Ts...> : public MyTuple<Ts...>
{
private:
T data;
using TopTuple = MyTuple<Ts...>;
template<typename TupleType, std::size_t... Indices>
MyTuple(TupleType&& _copy, std::index_sequence<Indices...>) : MyTuple(_copy.template get<Indices>()...) {}
public:
MyTuple() = default;
template<typename ThisType, typename... RestTypes>
MyTuple(ThisType&& _data, RestTypes&&... _args) : data(std::forward<ThisType>(_data)), TopTuple(std::forward<RestTypes>(_args)...) {}
template<template<typename...> typename TupleType, typename... RestTypes>
MyTuple(TupleType<RestTypes...>&& _copy) : MyTuple(std::forward<TupleType<RestTypes...>>(_copy), std::make_index_sequence<my_tuple_size_value<TupleType<RestTypes...>>>{}) {}
// const MyTuple調用
template<std::size_t index>
constexpr auto& get() const
{
static_assert(index <= sizeof...(Ts), "out of range");
if constexpr (index == 0)
return data;
else
return TopTuple::template get<index - 1>();
}
// non-const MyTuple調用
template<std::size_t index>
constexpr auto& get()
{
using element_type = my_tuple_element_type<index, MyTuple<T, Ts...>>;
return const_cast<element_type&>((static_cast<const MyTuple<T, Ts...>&>(*this)).template get<index>());
}
};
太棒了,解決了一個問題之后又出現一個問題
int main()
{
MyTuple<int, std::string, float> t1 = my_make_tuple(12, "24142", 3.14);
cout << t1.template get<0>() << endl;
cout << t1.template get<1>() << endl;
cout << t1.template get<2>() << endl;
// 一個我暫時還解決不掉的問題
// auto temp = my_make_tuple(12, "24142", 3.14);
// MyTuple<int, std::string, float> t2 = temp;
}
實現一個泛型delegate
眾所周知,在C#中有一個關鍵字是delegate
delegate int AddNumDelegate(int x, int y);
int Sum(int x, int y) { return x + y; }
AddNumDelegate addNum = Sum;
int z = addNum(10, 30);
如果你看不懂的話,翻譯到C++中大概是這么個玩意(圖一樂就行了)
using AddNumDelegate = std::function<int(int, int)>;
int Sum(int x, int y) { return x + y; }
int main()
{
AddNumDelegate addSum = Sum;
std::cout << addSum(10, 20) << std::endl;
}
上述例子中都顯式指定了函數的參數類型,那我們在C++中實現一個泛型的delegate
。因為C#中的函數都是成員函數,所以只實現類成員函數的版本
template<typename T, typename ReturnType, typename... Params>
class MyDelegate
{
private:
using MemberFuncType = ReturnType (T::*)(Params...);
T* pClass;
MemberFuncType pMemberFunc;
public:
MyDelegate(T* _t, MemberFuncType _f) : pClass(_t), pMemberFunc(_f) {}
template<typename... Ts>
ReturnType operator()(Ts&&... _args) {
return (pClass->*pMemberFunc)(std::forward<Ts>(_args)...);
}
};
struct TestStruct
{
template<typename T>
int test_func(T&& x) { return std::forward<T>(x); }
};
int main()
{
TestStruct t;
MyDelegate d(&t, &TestStruct::test_func<int>);
// true
std::cout << std::boolalpha << is_rvalue_reference_v<decltype(d(10))> << std::endl;
}
模板題外話
template<auto>
這是C++17對非類型模板參數的自動類型推導
// C++17之前
template<std::size_t num>
void print() { std::cout << num << std::endl; }
// C++17之后
template<auto num>
void print() { std::cout << num << std::endl; }
而C++20又放寬了非類型模板參數的限制,C++20之前的非類型模板參數不能為浮點數,只能為int
, unsigned int
, long long
,char
以及指針類型等等,而C++20則支持double
和float
// C++20
template<double num>
void print() { std::cout << num << std::endl; }
總結
