可變參數模板示例
void print() {} // 沒有參數時將調用此函數
template<typename T, typename... Types>
void print(T firstArg, Types... args)
{
std::cout << firstArg << ' '; // 打印第一個實參,無參數時將調用此函數
print(args...); // 調用print()打印其余實參
}
int main()
{
std::string s("world");
print(3.14, "hello", s); // 3.14 hello world
}
重載可變參數和非可變參數模板
- 前一例子也可以如下實現,如果兩個函數模板只有尾置參數包不同,會優(yōu)先匹配沒有尾置參數包的版本
template<typename T>
void print(T x)
{
std::cout << x << ' ';
}
template<typename T, typename... Types>
void print(T firstArg, Types... args)
{
print(firstArg);
print(args...);
}
sizeof...運算符
- sizeof...用于計算參數包的元素數
template<typename T, typename... Types>
void print(T firstArg, Types... args)
{
std::cout << sizeof...(Types) << '\n'; // print number of remaining types
std::cout << sizeof...(args) << '\n'; // print number of remaining args
}
- 可能會想到將其用于跳過遞歸結尾,以防缺少實參,但這是錯誤的
template<typename T, typename... Types>
void print(T firstArg, Types... args)
{
std::cout << firstArg << '\n';
if (sizeof...(args) > 0) // sizeof...(args)==0時會出錯
{
print(args...); // 因為print(args...)仍將被初始化,而此時沒有實參
}
}
- 函數模板中所有的if語句分支都會被實例化,當對最后一個實參調用print()時,打印了實參后,
sizeof...(args)
為0,但沒有實參時print(args...)仍然會初始化,結果就會出錯。C++17中引入了編譯期if來解決這個問題
template<typename T, typename...Types>
void print(const T& firstArg, const Types&... args)
{
std::cout << firstArg << '\n';
if constexpr (sizeof...(args) > 0)
{
print(args...); // 只在sizeof...(args) > 0時實例化
}
}
折疊表達式
- C++17引入了折疊表達式,用于獲取對所有參數包實參使用二元運算符的計算結果。如下模板將返回所有實參的和
template<typename... T>
auto foldSum(T... s)
{
return (... + s); // ((s1 + s2) + s3) ...
}
-
如果參數包為空,表達式通常是非法的(對空參數包例外的是:&&視為true,||視為false,逗號運算符視為void())
- 上例中的折疊表達式還可以有如下形式
foldSum(1, 2, 3, 4, 5); // 假如實參是12345
// 左邊是返回值,右邊是計算時的內部展開方式
(... + s):((((1 + 2) + 3) + 4) + 5)
(s + ...):(1 + (2 + (3 + (4 + 5))))
(0 + ... + s):(((((0 + 1) + 2) + 3) + 4) + 5)
(s + ... + 0):(1 + (2 + (3 + (4 + (5 + 0)))))
- 折疊表達式幾乎可以使用所有二元運算符
struct Node {
int val;
Node* left;
Node* right;
Node(int i = 0) : val(i), left(nullptr), right(nullptr) {}
};
// 使用operator->*的折疊表達式,用于遍歷指定的二叉樹路徑
template<typename T, typename... Ts>
Node* traverse(T root, Ts... paths)
{
return (root ->* ... ->* paths); // np ->* paths1 ->* paths2 ...
}
int main()
{
Node* root = new Node{ 0 };
root->left = new Node{ 1 };
root->left->right = new Node{ 2 };
root->left->right->left = new Node{ 3 };
auto left = &Node::left;
auto right = &Node::right;
Node* node1 = traverse(root, left);
std::cout << node1->val; // 1
Node* node2 = traverse(root, left, right);
std::cout << node2->val; // 2
Node* node3 = traverse(node2, left);
std::cout << node3->val; // 3
}
- 使用折疊表達式簡化打印所有參數的可變參數模板
template<typename... Ts>
void print(const Ts&... args)
{
(std::cout << ... << args) << '\n';
}
- 如果想用空格分隔參數包元素,需要使用一個包裹類來提供此功能
template<typename T>
class AddSpace {
public:
AddSpace(const T& r): ref(r) {}
friend std::ostream& operator<<(std::ostream& os, AddSpace<T> s)
{
return os << s.ref << ' '; // 輸出傳遞的實參和一個空格
}
private:
const T& ref; // 構造函數中的實參的引用
};
template<typename... Args>
void print(Args... args)
{
(std::cout << ... << AddSpace(args)) << '\n';
}
可變參數模板的應用
- 可變參數模板的典型應用是轉發(fā)任意數量任意類型的實參,比如std::make_shared就是用它實現的
auto p = std::make_shared<std::complex<double>>(3.14, 4.2);
void f(int, std::string);
std::thread t(f, 42, "hi");
struct A {
A(int _i, std::string _s) : i(_i), s(_s) {}
int i;
std::string s;
};
std::vector<A> v;
v.emplace_back(1, "hi");
- 通常這類實參會使用移動語義進行完美轉發(fā),上述例子在標準庫中對應的聲明如下
namespace std {
template<typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args);
class thread {
public:
template<typename F, typename... Args>
explicit thread(F&& f, Args&&... args);
...
};
template<typename T, typename Allocator = allocator<T>>
class vector {
public:
template<typename... Args>
reference emplace_back(Args&&... args);
...
};
}
- 除了上述例子,參數包還能用于其他地方,如表達式、類模板、using聲明、deduction guide
可變參數表達式(Variadic Expression)
- 可以對參數包中的參數進行運算,比如讓每個元素翻倍后傳遞給再打印
template<typename... Args>
void print(const Args&... args)
{
(std::cout << ... << args);
}
template<typename... T>
void printDoubled(const T&... args)
{
print(args + args...);
}
int main()
{
printDoubled(3.14, std::string("hi"), std::complex<double>(4, 2));
// 等價于
print(3.14 + 3.14, std::string("hi") + std::string("hi"),
std::complex<double>(4, 2) + std::complex<double>(4, 2));
}
- 注意參數包的省略號不能直接接在數值字面值后
template<typename... T>
void addOne(const T&... args)
{
print(args + 1...); // 錯誤 1...是帶多個小數點的字面值,不合法
print(args + 1 ...); // OK
print((args + 1)...); // OK
}
- 編譯期表達式能以同樣的方式包含模板參數包
template<typename T1, typename... TN>
constexpr bool isHomogeneous(T1, TN...)
{ // 判斷是否所有實參類型相同
return (std::is_same_v<T1, TN> && ...); // since C++17
}
isHomogeneous(1, 2, "hi"); // 結果為false
// 擴展為std::is_same_v<int, int> && std::is_same_v<int, const char*>
isHomogeneous("hello", "", "world", "!") // 結果為true:所有實參都為const char*
可變參數索引(Variadic Index)
- 下面函數使用一個可變索引列表訪問傳遞的第一個實參對應的元素
template<typename... Args>
void print(const Args&... args)
{
(std::cout << ... << args);
}
template<typename C, typename... N>
void printElems(const C& c, N... n)
{
print(c[n]...);
}
int main()
{
std::vector<std::string> v{ "good", "times", "say", "bye" };
printElems(v, 2, 0, 3); // say good bye:等價于print(v[2], v[0], v[3]);
}
- 非類型模板參數也可以聲明為參數包
template<std::size_t... N, typename C>
void printIdx(const C& c)
{
print(c[N]...);
}
std::vector<std::string> v{ "good", "times", "say", "bye" };
printIdx<2, 0, 3>(v);
可變參數類模板(Variadic Class Template)
- 可變參數類模板的一個重要例子是std::tuple
template<class... Types>
class tuple;
tuple<int, std::string, char> t;
- 另一個例子是std::variant
template<class... Types>
class variant;
variant<int, std::string, char> v;
- 也能定義一個類作為表示一個索引列表的類型
template<std::size_t...>
struct Indices
{};
template<typename... Args>
void print(const Args&... args)
{
(std::cout << ... << args);
}
template<typename T, std::size_t... N>
void printByIdx(T t, Indices<N...>)
{
print(std::get<N>(t)...);
}
int main()
{
std::array<std::string, 5> arr{ "Hello", "my", "new", "!", "World" };
printByIdx(arr, Indices<0, 4, 3>()); // HelloWorld!
auto t = std::make_tuple(12, "monkeys", 2.0);
printByIdx(t, Indices<0, 1, 2>()); // 12monkeys2
}
可變參數推斷指南(Variadic Deduction Guide)
- C++17的標準庫中對std::array定義了如下deduction guide
namespace std {
template<typename T, typename... U> array(T, U...)
-> array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>;
}
可變參數基類(Variadic Base Class)與using
class A {
public:
A(const std::string& x) : s(x) {}
auto f() const { return s; }
private:
std::string s;
};
struct A_EQ {
bool operator() (const A& lhs, const A& rhs) const
{
return lhs.f() == rhs.f();
}
};
struct A_Hash {
std::size_t operator() (const A& a) const
{
return std::hash<std::string>{}(a.f());
}
};
// 定義一個組合所有基類的operator()的派生類
template<typename... Bases>
struct Overloader : Bases...
{
using Bases::operator()...; // OK since C++17
};
int main()
{
// 將A_EQ和A_Hash組合到一個類型中
using A_OP = Overloader<A_Hash, A_EQ>;
/* unordered_set的聲明
template<
class Key,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<Key>
> class unordered_set;
*/
std::unordered_set<A, A_Hash, A_EQ> s1;
std::unordered_set<A, A_OP, A_OP> s2;
}