C++模板(第二版)笔记之第四章:变参模板

一、变参模板

1.变参模板eg

可以将模板参数定义成能够接受任意多个模板参数的情况。 这一类模板被称为变参模板(variadic template)。

  • eg:可以通过调用下面代码中的 print()函数来打印一组数量和类型都不确定的参数:
#include <iostream>
//为了结束递归, 重载了不接受参数的非模板函数 print(), 它会在参数包为空的时候被调用。
void print ()
{
    
}

//这些被称为 args的剩余参数, 是一个函数参数包(function parameter pack) :
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
}


int main()
{
    std::string s("world");
    print (7.5, "hello", s);
}
  • 测试:
7.5
hello
World

解释:

  • 因为这个调用首先会被扩展成:

print<double, char const*, std::string> (7.5, “hello”, s);
其中:
firstArg 的值是 7.5, 其类型 T 是 double。
args 是一个可变模板参数, 它包含类型是 char const*的“hello” 和类型是 std::string 的“world”

  • 在打印了 firstArg 对应的 7.5 之后, 继续调用 print()打印剩余的参数, 这时 print()被扩展为:

print<char const*, std::string> (“hello”, s);
其中:
firstArg 的值是“hello” , 其类型 T 是 char const *。
args 是一个可变模板参数, 它包含的参数类型是 std::string。

  • 在打印了 firstArg 对应的“hello” 之后, 继续调用 print()打印剩余的参数, 这时 print()被扩展为:
    print<std::string> (s);

其中:
firstArg 的值是“world” , 其类型 T 是 std::string。
args 是一个空的可变模板参数, 它没有任何值

  • 这样在打印了 firstArg 对应的“ world” 之后, 就会调用被重载的不接受参数的非模板函数print(), 从而结束了递归。

2.变参和非变参模板的重载

  • eg:当两个函数模板的区别只在于尾部的参数包的时候, 会优先选择没有尾部参数包的那一个函数模板。
#include <iostream>
//为了结束递归, 重载了不接受参数的非模板函数 print(), 它会在参数包为空的时候被调用。
template<typename T>
void print (T arg)
{
	std::cout << arg << '\n' ; //print passed argument
}

//这些被称为 args的剩余参数, 是一个函数参数包(function parameter pack) :
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
}
  • 另外:
#include <iostream>
 template<typename T>
void print(T arg)
{
  (std::cout << arg) << '\n';
}
int main()
{
    std::cout << "Hello World";
    print();
    return 0;
}
  • 测试:
/home/insights/insights.cpp:10:5: error: no matching function for call to 'print'
    print();
    ^~~~~
/home/insights/insights.cpp:3:6: note: candidate function template not viable: requires single argument 'arg', but no arguments were provided
void print(T arg)
     ^
1 error generated.
Error while processing /home/insights/insights.cpp.

3.sizeof… 运算符

C++11 为变参模板引入了一种新的 sizeof 运算符: sizeof…。 它会被扩展成参数包中所包含的参数数目。

  • 运算符 sizeof…既可以用于模板参数包, 也可以用于函数参数包。
template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
	std::cout << firstArg << ’ \n’ ; //print first argument
	std::cout << sizeof(Types) << ’ \n’ ; //print number of remaining
	types
	std::cout << sizeof(args) << ’ \n’ ; //print number of remainingargs}
  • 错误做法:这样可能会让你觉得, 可以不使用为了结束递归而重载的不接受参数的非模板函数 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()函数模板, 虽然 args…为空, if 语句中的 print(args…)也依然会被实例化, 但此时没有定义不接受参数的 print()函数,因此会报错

  • 其他方法:如果使用 constexp if, 就可以在函数内部决定是否要继续递归下去, 而不用再单独定义一个函数来终结递归:

template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
	std::cout << firstArg << ’ \n’ ;
	if constexpr(sizeof(args) > 0) { 
		print(args…); //code only available if sizeof…(args)>0 (sinceC++17)
	}
}

解释:

  • 这里如果只给 print()传递一个参数, 那么 args…就是一个空的参数包, 此时 sizeof…(args)等于
    0。
  • 这样 if 语句里面的语句就会被丢弃掉, 也就是说这部分代码不会被实例化。 因此也就不
    再需要一个单独的函数来终结递归

二、折叠表达式

从 C++17 开始, 提供了一种可以用来计算参数包(可以有初始值) 中所有参数运算结果的二元运算符。

  • 几乎所有的二元运算符都可以用于折叠表达式
  • eg:下面的函数会返回 s 中所有参数的和。如果参数包是空的, 这个表达式将是不合规范的。
template<typename… T>
auto foldSum (T… s) {
	return (+ s); // ((s1 + s2) + s3) …
}

更好的方法

template<class ...T>
auto sum(T ...t)
{
    return (0+...+t);
}

  • 可能的折叠表达式:
    在这里插入图片描述

  • eg:使用折叠表达式和运算符->*遍历一条二叉树的路径:

// define binary tree structure and traverse helpers:
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;
// traverse tree, using fold expression:
template<typename T, typename… TP>
Node* traverse (T np, TP… paths) 
{
//折叠表达式从 np 开始遍历了 paths 中所有可变成员。
	return (np ->*->* paths); // np ->* paths1 ->* paths2 …
}

int main()
{
	// init binary tree structure:
	Node* root = new Node{0};
	root->left = new Node{1};
	root->left->right = new Node{2}; 
	//traverse binary tree:
	Node* node = traverse(root, left, right);
}
  • eg:在参数包各元素之间并不会打印空格。它可以在所有要打印的参数后面追加一个空格:
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... Args>
void print (Args... args) {
( 
	std::cout <<... << AddSpace<Args>(args) ) << '\n' ;
}

//更好的方法
template <class T0, class ...T>
void print(T0 const &t0, T const &...t) {
    std::cout << t0;
    ((std::cout << ' ' << t), ...);//C++ 逗号运算符
    std::cout << std::endl;
}

三、变参模板的使用

一个重要的作用是转发任意类型和数量的参数。

通常是使用移动语义对参数进行完美转发(perfectly forwarded)

注意, 之前关于常规模板参数的规则同样适用于变参模板参数。

  • 比如, 如果参数是按值传递的, 那么其参数会被拷贝, 类型也会退化(decay) 。 如果是按引用传递的, 那么参数会是实参的引用, 并且类型不会退化:
// 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);

四、变参类模板和变参表达式

参数包还可以出现在其它一些地方, 比如表达式, 类模板, using 声明, 甚至是推断指引中。

1.变参表达式

  • eg:先是将参数包中的所有的参数都翻倍, 然后将结果传给 print():
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
}
  • eg:折叠表达式的一种应用:下面这个例子可以用来判断所有参数包中参数的类型是否相同:
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 const *( 这里因为是按值传递, 所以
发生了类型退还, 否则类型将依次被推断为: char const[6], char const[1], char const[6]char const[2]) 。

2.变参下标

  • eg:通过一组变参下标来访问第一个参数中相应的元素:
// Example program
#include <iostream>
#include <string>
#include <vector>
using namespace std;

//为了结束递归, 重载了不接受参数的非模板函数 print(), 它会在参数包为空的时候被调用。
template<typename T>
void print (T arg)
{
	std::cout << arg << '\n' ; //print passed argument
}

//这些被称为 args的剩余参数, 是一个函数参数包(function parameter pack) :
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
}


template<typename C,typename... Idx>
void printElems (C const& coll, Idx... idx)
{
    if constexpr(sizeof...(idx) > 0) 
    {
        print (coll[idx]...);
    }
    else
    {
        std::cout<<"need 1 paras at least"<<std::endl;
    }
}    

int main()
{
    //相当于调用了:print (coll[2], coll[0], coll[3]);
    std::vector<std::string> coll = {"good", "times", "say", "bye"};
    printElems(coll,2,0,3);
    
}
  • eg:也可以将非类型模板参数声明成参数包。
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);

3.变参类模板

  • eg:通过任意多个模板参数指定了 class 相应数据成员的类型:
template<typename… Elements>class Tuple;
Tuple<int, std::string, char> t; // t can hold integer, string, and character
  • eg:指定对象可能包含的类型:
template<typename… Types>
class Variant;
Variant<int, std::string, char> v; // v can hold integer, string, or character
  • eg:将 class 定义成代表了一组下表的类型:
// type for arbitrary number of indices:
template<std::size_t…>
struct Indices {
};
  • eg:定义一个通过 print()打印 std::array 或者 std::tuple 中元素的函数, 具体打印哪些元素由编译阶段的 get<>从给定的下标中获取:
template<typename T, std::size_t… Idx>
void printByIdx(T t, Indices<Idx…>)
{
	print(std::get<Idx>(t));
}

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

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

4.变参推断指引

  • eg: std::array 定义了如下推断指引:
namespace std {
template<typename T, typename… U> array(T, U…)
//is_same_v<T, U1> && is_same_v<T, U2> && is_same_v<T, U3> …
//如果结果是 false(也就是说 array 中元素不是同一种类型) , 推断指引会被弃用, 总的类型推断失败。
-> array<enable_if_t<(is_same_v<T, U> &&), T>, (1 + sizeof(U))>;
}

初始化:std::array a{42,45,77};
会将指引中的 T 推断为 array( 首) 元素的类型, 而 U...会被推断为剩余元素的类型。 因此array 中元素总数目是 1 + sizeof...(U), 等效于如下声明:
std::array<int, 3> a{42,45,77};

5.变参基类及其使用

  • eg:
#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:
	//从其每个基类中引入了 operator()的声明
	//从 CustomerHash 和 CustomerEq 派生出了 CustomerOP, 而且派生类中会包含两个基类中的operator()的实现。
	using CustomerOP = Overloader<CustomerHash,CustomerEq>;
	std::unordered_set<Customer,CustomerHash,CustomerEq> coll1;
	std::unordered_set<Customer,CustomerOP,CustomerOP> coll2;}

五、总结

通过使用参数包, 模板可以有任意多个任意类型的参数。
为了处理这些参数, 需要使用递归, 而且需要一个非变参函数终结递归(如果使用编译期判断, 则不需要非变参函数来终结递归) 。
运算符 sizeof…用来计算参数包中模板参数的数目。
变参模板的一个典型应用是用来发送(forward) 任意多个任意类型的模板参数。
通过使用折叠表达式, 可以将某种运算应用于参数包中的所有参数。

  • ref:《C++Template》第二版
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜欢打篮球的普通人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值