在学习这一篇内容之前,我们需要了解模板的一个规则:我们不能声明两个同名但模板参数不同的类模板。
template <typename T>
class test;
// 异常,与前面的声明不匹配
//template <typename T1, typename T2>
//class test;
这里就会有一个问题了,如何让一个模板可以接收不同数量的参数呢?
模板函数可以通过重载实现:
template <typename T>
void print();
template <typename T1, typename T2>
void print();
模板类就没有办法实现了,如果要接收不同数量的参数,必须要声明不同名称的模板类:
template <typename T1, typename T2, typename T3>
class test;
template <typename T1, typename T2>
class test1;
template <typename T1>
class test2;
无论是上面的函数模板重载还是声明不同名称的模板类,都不太适合处理未知数量参数的情况。
1、可变参数模板
C++为我们提供了可变参数模板,顾名思义,模板的参数个数是不固定的,语法如下:
template <typename... Args>
class Test;
template <typename... Args>
void func(Args...);
语法中的typename...
表示一个类型参数包,函数签名中的Args...
表示值参数包。
接下来一起了解可变参数模板应该如何使用:
1.1、可变参模板函数
可变参模板函数通常和递归一起使用,举个例子:
template <typename T>
void print(T arg) {
std::cout << arg << std::endl;
}
template <typename T, typename... Rest>
void print(T arg, Rest... rest) {
std::cout << arg << std::endl;
print(rest...);
}
声明了两个模板函数print,它们间的关系为重载,如果我们进行以下调用:
int main() {
print('1', 2, "three", 4.0);
}
// 1
// 2
// three
// 4
第二个print模板种包含有一个递归,每次执行print都会将第一个参数打印出来。typename... Rest
是模板参数包,函数签名中的Rest... rest
表示一个值参数包,rest...
表示将参数包展开。
可能会有人不理解参数包是什么,参数包是如何展开的,下面会有例子帮助理解。
上例中有一点要注意,只有一个参数的模板声明需要放在变参模板声明的上面,否则将会出现找不到合适模板的错误。
1.2、可变参模板类
template <typename... Ts>
class Tuple;
template <typename T, typename... Ts>
class Tuple<T, Ts...> : Tuple<Ts...> {
T value;
};
可变参数模板类用的可能会比较少一些,C++中的容器元组就是用可变参模板类实现的,可以包含不同类型元素的容器,这个我们后续再学习。
2、可变参数模板的常见应用场景
- 构造函数转发:我们可以用可变参数模板来转发参数给构造函数,比较常见的就是只能指针的实现:
template <typename T, typename... Args>
unique_ptr<T> make_unique(Agrs&&... args) {
return unique_ptr<T>(new T(forward<Args>(args)...));
}
我们在这里理解模板参数包和参数包的展开,先来理解模板参数包,Args表示任意数量的模板类型参数,例如:
std::string str = "hello";
auto p = make_unique<std::string>(str);
这个例子中T代表了std::string,Args 代表了 std::string&
,
再来一个例子:
auto p = make_unique<std::string>("hello");
T代表了std::string,Args 代表了 const char (&)[6]
,
上面两个例子中Args代表了一个参数,接下来举一个Agrs代表多个参数的例子:
class MyClass {
public:
MyClass(int x, double y)
: x_(x), y_(y) {
}
private:
int x_;
double y_;
};
int main() {
auto p = std::make_unique<MyClass>(42, 3.14);
return 0;
}
Args表示int, double
。
Args是参数包,Args...
表示展开,展开后相当于:
make_unique<MyClass, int, double>(int&& arg1, double&& arg2) {
return unique_ptr<MyClass>(new MyClass(forward<int>(arg1), forward<double>(arg2)));
}
Args&&... args
可以看作是将所有的参数同时声明为相同格式。
forward<Args>(args)...
这里面有两个参数包,展开时类型参数和值参数需要一一对应。
还有一点我们需要了解,make_unique它有两个模板参数:一个是要分配的对象的类型,另一个是构造这个对象所需参数的类型,实际使用中我们只指定了第一个模板参数,其他参数都是编译器推导出来的。
为模板函数提供了足够的信息后,编译器可以推导出未明确指定的模板参数。
- 元组
- 类型特性:在编译时获取类型信息,计算可变参数模板中的类型数量,举个例子:
template <typename... Args>
struct Count;
template<>
struct Count<> {
enum {
value = 0
};
};
template <typename T, typename... Rest>
struct Count<T, Rest...> {
enum {
value = 1 + Count<Rest...>::value
};
};
int main() {
std::cout << Count<int, float, double, char>::value;
}
// 4
- 日志的格式化:类似我们在可变参模板函数一节中举的例子,使用可以变模板参数的递归。