可变参数模板 Variadic Templates

从 C++11 开始,模板可以有任意个模板参数,也即所谓的可变参数模板。

可变参数模板

可变参数模板的例子

定义一个函数 print() 如下,用于接收不同类型的不同参数。

#include <iostream>

void print ()
{
}

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
  std::cout << firstArg << '\n'; // print first argument
  print(args...); // call print() for remaining arguments
}

typename... Args 被称为模板参数包(template parameter pack),Args... args 被称为函数参数包(function parameter pack)。模板函数 void print (T firstArg, Types... args) 首先打印第一个参数,然后递归调用自己打印剩余参数。为了结束递归,需要提供一个非模板类型重载函数,用于处理最后的空参数包。

为了更直观地感受可变参数模板地递归处理过程,可以在以上 print 中插入 __PRETTY_FUNCTION__,打印出函数的调用。

#include <iostream>

void print ()
{
  std::cout << __PRETTY_FUNCTION__ << "\n";
}

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
  std::cout << firstArg << '\n'; // print first argument
  std::cout << __PRETTY_FUNCTION__ << "\n";
  print(args...); // call print() for remaining arguments
}

当发生 print (7.5, "hello", 5) 调用时,输出如下:

7.5
void print(T, Types ...) [with T = double; Types = {const char*, int}]
hello
void print(T, Types ...) [with T = const char*; Types = {int}]
5
void print(T, Types ...) [with T = int; Types = {}]
void print()

重载可变和非可变参数模板

也可以实现上述例子如下:

#include <iostream>
template<typename T>
void print (T arg)
{
  std::cout << arg << '\n'; // print passed argument
}
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
  print(firstArg); // call print() for the first argument
  print(args...);  // call print() for remaining arguments
}

两个 ptint 的区别在于拖尾参数包,没有拖尾参数包的函数会被更优先匹配。

sizeof… 操作符

C++ 引入操作 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
  ...
}

添加以上两次 sizeof... 的调用,可以打印每次调用 print 时模板参数包和函数参数包元素的个数。

你可能会认为,借助 sizeof... ,可以省去一个空参数的 print

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
  std::cout << firstArg << '\n';
  if (sizeof...(args) > 0) { // error if sizeof...(args)==0
    print(args...);          // and no print() for no arguments declared
  }
}

但是,上面的代码无法工作。模板代码的实例是编译期决定的,而实例化的代码是否被执行是运行期决定的。也即 if 语句在编译期会生成,由于没有空参数的 print() 而导致编译失败。

在 C++17 开始支持编译期的 if 语句:

template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
  std::cout << firstArg << '\n';
  if constexpr (sizeof...(args) > 0) { // error if sizeof...(args)==0
    print(args...);          // and no print() for no arguments declared
  }
}

折叠表达式

C++17 引入了折叠表达式(Fold Expressions),用于对参数包中参数进行二元计算。例如:

template<typename... T>
auto foldSum (T... s) {
  return (... + s); // ((s1 + s2) + s3) ...
}

auto ret = foldSum(3, 4.5, 9.0); 得到 16.5

如果参数包为空,表达式通常是非法的。但是空参数包二元计算的例外情况:&& 视为 true|| 视为 false,逗号运算符视为 void()

可能的折叠表达式如下表:

折叠表达式计算结果
(… op pack)(((pack1 op pack2) op pack3) … op PackN)
(pack op …)(pack1 op (… (packN-1 op packN)))
(init op … op pack)(((init op pack1) op pack2) … op PackN)
(pack op … op init)(pack1 op (… (packN op init)))

折叠表达式几乎可以使用所有二元运算符。例如,可以使用运算符 ->* 遍历一个二叉树的路径:

#include <cassert>

struct Node {
  int val;
  Node* left;
  Node* right;
  Node(int i = 0) : val(i), left(nullptr), right(nullptr) {}
};

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);
  assert(node1->val == 1);
  Node* node2 = traverse(root, left, right);
  assert(node2->val == 2);
  Node* node3 = traverse(node2, left);
  assert(node3->val == 3);
}

使用折叠表达式,我们可以简化前面的可变参数模板 print 的实现:

#include <iostream>

template<typename... Types>
void print (Types const&... args)
{
  (std::cout << ... << args) << '\n';
}

int main() {
  print(3, 4.5, 5);  //34.55
}

但是,上面的打印没有空白分割符来分割不同的参数。为了增加空白分隔符,需要增加一个类模板来辅助实现:

#include <iostream>

template<typename T>
class AddSpace
{
  private:
    T const& ref; // refer to argument passed in constructor
  public:
    AddSpace(T const& r): ref(r) {
    }
    friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {
      return os << s.ref << ' '; // output passed argument and a space
    }
};

template<typename... Types>
void print (Types const&... args)
{
  (std::cout << ... << AddSpace(args)) << '\n';
}

int main() {
  print(3, 4.5, 5); // 3 4.5 5
}

可变参数模板的应用

可变参数模板在 C++ 标准库中扮演重要角色。可变参数模板的典型应用是转发任意数量、任意类型的实参。例如:

// create shared pointer to complex<float> initialized by 4.2 and 7.7:
auto sp = std::make_shared<std::complex<float>>(4.2, 7.7);

std::thread t (foo, 42, "hello"); // call foo(42,"hello") in a separate thread

std::vector<Customer> v;
...
v.emplace("Tim", "Jovi", 1962); // insert a Customer initialized by three arguments

通常,这些参数会进行完美转发,在标准库中申明如下:

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);
  ...
};
}

需要注意的是:和普通函数一样,可变参函数模板的参数有一样应用的规则。例如,值传参时,参数会被拷贝和退化(例如,数组会退化成指针);引用传参时,入参指向原始的参数并且不会退化:

// args are copies with decayed types:
template<typename... Args> void foo (Args... args);
// args are nondecayed references to passed objects:
template<typename... Args> void bar (Args const&... args);

可变参数类模板和可变表达式

除了上述例子,参数包还可以出现在其他地方,例如表达式、类模板、使用申明、类型推导指南。

可变表达式

不仅可以转发参数,还可以计算参数。例如:

template<typename... T>
void printDoubled (T const&... args)
{
  print (args + args...);
}

例如,调用如下:

printDoubled(7.5, std::string("hello"), std::complex<float>(4,2));

相当于如下效果:

print(7.5 + 7.5,
      std::string("hello") + std::string("hello"),
      std::complex<float>(4,2) + std::complex<float>(4,2);

如果只想给每个参数加1,请注意省略号中的点可能不会直接跟在数字文字后面。

template<typename... T>
void addOne (T const&... args)
{
  print (args + 1...);   // ERROR: 1... is a literal with too many decimal points
  print (args + 1 ...);  // OK
  print ((args + 1)...); // OK
}

编译期的表达式也可以包含在模板参数中。例如下面的函数模板用于返回所以参数类型是否相同:

template<typename T1, typename... TN>
constexpr bool isHomogeneous (T1, TN...)
{
  return (std::is_same<T1,TN>::value && ...); // since C++17
}

这是折叠表达式的应用。对于:

isHomogeneous(43, -1, "hello")

会被扩展为

std::is_same<int,int>::value && std::is_same<int,char const*>::value

并且返回 false。而

isHomogeneous("hello", " ", "world", "!")

则返回 true。因为所有的参数被推导为 char*(值传参,参数类型退化)。

可变下标

另一个应用:下面的函数使用可变的下标列表访问第一个参数的相应元素。

template<typename C, typename... Idx>
void printElems (C const& coll, Idx... idx)
{
  print (coll[idx]...);
}

例如:

std::vector<std::string> coll = {"good", "times", "say", "bye"};
printElems(coll,2,0,3);

效果相当于调用

print (coll[2], coll[0], coll[3]);

还可以将非类型模板参数申明为参数包。例如:

template<std::size_t... Idx, typename C>
void printIdx (C const& coll)
{
  print(coll[Idx]...);
}

例如:

std::vector<std::string> coll = {"good", "times", "say", "bye"};
printIdx<2,0,3>(coll);

可变参数类模板

可变参数模板也可以是类模板。一个重要的例子就是任意参数、任意类型的类 tuple

template<typename... Elements>
class Tuple;
Tuple<int, std::string, char> t; // t can hold integer, string, and character

std::tuple 将在后续章节继续讨论。另一个例子是 std::variant

template<typename... Types>
class Variant;
Variant<int, std::string, char> v; // v can hold integer, string, or character

还可以定义一个类表示下标的类型:

// type for arbitrary number of indices:
template<std::size_t...>
struct Indices {
};

template<typename T, std::size_t... Idx>
void printByIdx(T t, Indices<Idx...>)
{
  print(std::get<Idx>(t)...);
}

std::array<std::string, 5> arr = {"Hello", "my", "new", "!", "World"};
printByIdx(arr, Indices<0, 4, 3>());

这是元编程的第一步,这将在后续章节继续讨论。

可变参数推导指南

甚至,推导指南也可以是可变参数。例如:

namespace std {
  template<typename T, typename... U> array(T, U...)
    -> array<enable_if_t<(is_same_v<T, U> && ...), T>,
             (1 + sizeof...(U))>;
}

std::array a{42,45,77}; // 推导为 std::array<int, 3>

推导指南这里使用了折叠表达式,相当于

is_same_v<T, U1> && is_same_v<T, U2> && is_same_v<T, U3> ...

如果表达式不为 true,整个推导将会失败。

可变参数基类和 using

最后看一个例子:

#include <string>
#include <unordered_set>
class Customer
{
  private:
    std::string name;
  public:
    Customer(std::string const& n) : name(n) { }
    std::string getName() const { return name; }
};

struct CustomerEq {
  bool operator() (Customer const& c1, Customer const& c2) const {
    return c1.getName() == c2.getName();
  }
};

struct CustomerHash {
  std::size_t operator() (Customer const& c) const {
    return std::hash<std::string>()(c.getName());
  }
};

// define class that combines operator() for variadic base classes:
template<typename... Bases>
struct Overloader : Bases...
{
  using Bases::operator()...; // OK since C++17
};

int main()
{
  // combine hasher and equality for customers in one type:
  using CustomerOP = Overloader<CustomerHash,CustomerEq>;
  std::unordered_set<Customer,CustomerHash,CustomerEq> coll1;
  std::unordered_set<Customer,CustomerOP,CustomerOP> coll2;
  ...
}

这里 ,首先定义了一个类 Customer 和 两个函数对象 CustomerEqCustomerHash。然后 定义 Overloader,继承于可变参数的基类。通过如下申明,CustomerOP 可以使能两个基类的 operator() 的实现。

using CustomerOP = Overloader<CustomerHash,CustomerEq>;

后续章节将继续讨论该技术的应用。

参考

  • http://www.tmplbook.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值