回到顶部 暗色模式

C++ Templates(2):非类模版参数

1. 非类模版参数

1.1 非类模版参数类

        你可以实现一个固定大小的 $Stack$ ,这样就可以避开内存管理的开销。为了让 $Stack$ 更加灵活,你可以让用户决定最大大小。

#include <array>
#include <cassert>

template<typename T, std::size_t Maxsize>
class Stack {
private:
  std::array<T, Maxsize> elems;
  std::size_t numElems;

public:
  Stack();
  void push(T const& elem);
  void pop();
  T const& top() const;
  bool empty() const { return numElems == 0; }
  std::size_T size() const { return numElems; }
};

template<typename T, std::size_t Maxsize>
Stack<T, Maxsize>::Stack() : numElems(0) {}

template<typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::push(T const& elem) {
  assert(numElems < Maxsize);
  elems[numElems] = elem;
  ++numElems;
}

template<typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::pop() {
  assert(!elems.empty());
  --numElems;
}

template<typename T, std::size_t Maxsize>
T const& Stack<T, Maxsize>::top() const {
  assert(!elems.empty());
  return elems[numElems - 1];
}

        $Maxsize$ 的类型是 $int$ ,它指定了 $Stack$ 内部 $array$ 成员的大小。此外,它也用于 $push$ 函数中检查栈是否已满。
        你也可以指定一个默认类型和大小:

template<typename T = int, std::size_t Maxsize = 100>
class Stack;

        然而,这不是一个好设计。默认值应该是一个符合直觉且正确的值,但是不管是 $int$ 还是 $100$ 都不够通用。所以,最好还是把这些交给用户决定。

1.2 非类模版参数函数

        你也可以为函数定义非类模版参数。

template<int Val, typename T>
T addValue(T, x) { return x + Val; }

        这种类型的函数在将函数或者操作作为模版参数时是否有用。例如,C++标准库中你就可以传入该函数的一个实例来实现为每个元素加上一个值:

std::tranform(source.begin(), source.end(),
              dest.begin(), addValue<5, int>);

        注意你必须同时指定 $Val$ 和类型 $int$ 。类型推导只会在立即调用时进行,并且 $std::transform$ 也需要一个完全类型来确定它第四个参数的模版类型。
        同样的,你也可以指定一个默认值:

template<auto Val, typename T = decltype(Val)>
T foo();

template<typename T, T Val = T{}>
T bar();

1.3 非类模版参数的限制

        非类模版参数有一些限制。一般,你只能使用整型常量、对象/函数/成员指针、对象/函数的左值引用或者 $std::nullptr_-t$ 。浮点数和类对象是不能使用的。
        当传递给指针或引用模版类型时,对象不能是字符串常量、临时变量、数据成员或者其他子对象。在C++17之前,这些限制随着C++版本被渐渐放宽了,之前的版本还存在一些额外限制:

        因此这样是不行的:

template<char const* name>
class MyClass;

MyClass<"hello"> x;  // 错误

        但是,这样是可以的 ( 取决于C++版本 ):

extern char const s03[] = "hi";  // 外部链接
char const s11[] = "hi";  // 内部链接

int main() {
  Message<s03> m03;  // 所有版本
  Message<s11> m11;  // C++11
  static char const s17[] = "hi";  // 无链接
  Message<s17> m17;  // C++17
}

1.4 auto

        从C++17开始,你可以通过 $auto$ 类型参数来接收任意一个允许的非类模版参数。

#include <array>
#include <cassert>

template<typename T, auto Maxsize>
class Stack {
public:
  using size_type = decltype(Maxsize);

private:
  std::array<T, Maxsize> elems;
  size_type numElems;

public:
  Stack();
  void push(T const& elem);
  void pop();
  T const& top() const;
  bool empty() const { return numElems -- 0; }
  size_type size() const { return numElems; }
};

template<typename T, auto Maxsize>
Stack<T, Maxsize>::Stack() : numElems(0) {}

template<typename T, auto Maxsize>
void Stack<T, Maxsize>::push(T const& elem) {
  assert(numElems < Maxsize);
  elems[numElems] = elem;
  ++numElems;
}

template<typename T, auto Maxsize>
void Stack<T, Maxsize>::pop() { 
  assert(!elems.empty());
  --numElems;
}

template<typename T, auto Maxsize>
T const& Stack<T, Maxsize>::top() const {
  assert(!elems.empty());
  return elems[numElems - 1];
}

        你也可以结合 $decltype$ 使用:

template<decltype(auto) N>
class C;

int i;
C<(i)> x;

        这样 $N$ 就可以成为一个引用类型。

2. 变长模版

2.1 变长模版

        C++11开始,你可使用变长参数模版。

#include <iostream>

void print() {}

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

        你也可以这样实现:

#include <iostream>

template<typename T>
void print(T arg) {
  std::cout << arg << '\n';
}

template<typename T, typename... Types>
void print(T firstArg, Types... args) {
  print(firstArg);
  print(args...);
}

        这种方式实现的依据是,如果两个函数模版不同之处只有模版尾部是否具有变长参数,那么没有变长参数的函数模版调用优先级更高。
        C++11也引入了新的运算符 $sizeof\dots$ :

template<typename T, typename... Types>
void print(T firstArg, Types... args) {
  std::cout << sizeof...(Types) << '\n';
  std::cout << sizeof...(args) << '\n';
}

        上面两次调用会打印剩余的参数数量,结果都相同。
        你可能认为我们可以像这样使用该运算符:

template<typename T, typename... Types>
void print(T firstArg, Types... args) {
  std::cout << firstArg << '\n';
  if (sizeof...(args) > 0) {
    print(args...);  // 错误
  }
}

        然而,这种方式是错误的。一段代码是否会被使用是运行时决定的,而不是编译时决定的,这意味着编译器必须实例化所有代码,其中包括了调用无参数版本的 $print$ ,因为并没有这样的 $print$ 函数重载,所以产生了编译错误。

2.2 折叠表达式

        C++17开始我们可以使用二元运算符来计算参数集里所有参数的运算结果。

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

        如果参数集为空,表达式的格式就不正确,这时 && 运算符返回 $true$ ,|| 运算符返回 $false$ ,逗号运算符返回 $void$ 。

折叠表达式 效果
$(\dots\ op\ pack)$ $(((pack_1\ op\ pack_2)\ op\ pack_3)\ \dots\ op\ pack_N)$
$(pack\ op\ \dots)$ $(pack_1\ op\ (\dots\ (pack_{N-1}\ op\ pack_N)))$
$(init\ op\ \dots\ op\ pack)$ $(((init\ op\ pack_1)\ op\ pack2)\ \dots\ op\ pack_N)$
$(pack\ op\ \dots\ op\ init)$ $(pack_1\ op\ (\dots\ (pack_N\ op\ init)))$
struct Node {
  int value;
  Node* left;
  Node* right;
  Node(int i = 0)
    : value(i), left(nullptr), right(nullptr) {}
};

auto left = &Node::left;
auto right = &Node::right;

template<typename T, typename... TP>
Node* traverse(T np, TP... paths) {
  return (np->*...->*paths);
}

int main() {
  Node* root = new Node{0};
  root->left = new Node{1};
  root->left->right = new Node{2};
  // ...
  Node* node = traverse(root, left, right);
}

        通过折叠表达式,我们可以简化之前的打印函数:

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

        你会注意到这次元素之间没有空格隔开。为了解决这个问题,我们需要重载 $operator$<< 函数:

template<typename T>
class AddSpace {
private:
  T const& ref;

public:
  AddSpace(T const& r) : ref(r) {}
  friend std::ostream& operator<<(std::ostream& os, AddSpace<T> s) {
    return os << s.ref << ' ';
  }
};

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

2.3 变长模版应用

        变长模版在泛型库中有很多应用。一个典型的例子是转发一系列的参数,例如:

auto sp = std::make_shared<std::complex<float>>(4.2, 7.7);
std::thread t(foo, 42, "hello");
std::vector<Customer> v;
v.emplace_back("Tim", "Jovi", 1962);

        通常,参数会使用完美转发。相应的,声明会变成:

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

2.3 变长模版和变长表达式

        除了上面的例子,参数集还可以在其他地方出现,例如表达式、类模版、$using$ 语句甚至推导指引。
        除了直接转发,你还可以对参数集进行计算:

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

        这个函数会把每个值打印两遍。
        你也可以让每个数加 $1$ ,注意 $1$ 和 $\dots$ 之间要隔开:

template<typename... T>
void addOne(T const&... args) {
  print(args + 1...);  // 错误
  print(args + 1 ...);
  print((args + 1)...);
}

        同理,你也可以在编译期对变长模版进行计算。

template<typename T1, typename... TN>
constexpr bool isHomogeneous(T1, TN...) {
  return (std::is_same_v<T1, TN> && ...);
}

        作为另一个例子,变长模版也可以用于列表索引:

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

        你也可以结合非类模版参数使用:

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

        变长模版也可以作为类模版,$std::tuple$ 和 $std::variant$ 就是很好的例子。

template<typename... Elements>
class Tuple;

template<typename... Types>
class Variant;

        你也可以定义一个表示一个列表的类型:

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(t, Indices<0, 1, 2>());

auto t = std::make_tuple(12, "monkeys", 2.0);
printByIdx(t, Indices<0, 1, 2>());

        变长模版也可以应用于推导指引中。C++标准库中就有一个例子:

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

        最后,考虑下面这个例子:

#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());
  }
};

template<typename... Bases>
struct Overloader : Bases... {
  using Bases::operator()...;  // C++17
};

int main() {
  using CustomerOP = Overloader<CustomerHash, CustomerEq>;
  std::unordered_set<Customer, CustomerHash, CustomerEq> coll1;
  std::unordered_set<Customer, CustomerOP, CustomerOP> coll2;
}

        通过继承变长基类,我们让 $CustomerOP$ 拥有了两个 $operator(\ )$ 函数,其中一个用于比较,另一个用于求哈希值。

C++ Templates(2):非类模版参数