学习内容
本节学习可变参数模板各版本异同,后续请关注 学习C++11/14/17/20/23关键词版本更替 ,将持续更新~~
前情提要
-
Pack(包),C++定义的一类实体,代表一组可扩展的元素
-
模板参数包:在 C++11 之前,函数模板或类模板的参数数量是固定的。例如,template<typename T, typename U> 只能接受两个类型参数。而有了模板参数包,可以写成 template<typename... Args>,其中的 Args 就是一个模板参数包,它可以代表 0 个、1 个或多个类型参数
cpptemplate <typename... Ts> struct MyTuple{}; MyTuple<> t1; MyTuple<int> t2; MyTuple<int ,float ,char> t3; MyTuple<0> t4; // error, 只允许带入类型,不会隐式推导 template<typename U , typename... Ts> //遵守模板参数包在模板参数列表最后一个参数 struct Valid; template<typename... Ts, typename U , typename = void> void Valid(U u ,Ts... ts); //ok , 因为可扩展包在参数列表最后一个参数 Valid( 1.f , 1 , 2 ,3 ); // U类型为double , Ts类型为 { int ,int ,int } // 错误示例 template<typename... Ts, typename U> void add(Ts... arg , U u ) // 参数包必须写在函数模板的最后一个参数位置 ,否则在调用时编译器无法确定有效参数个数 { (std::cout << ... << arg) << std::endl; } // 错误示例 template<typename... Ts,typename U> // 当扩展包出现在类模板中时,必须作为最后一个模板参数出现 struct Exam {}; -
函数参数包,函数中可以接受零个或多个函数实参的参数
cpptemplate< typename... Args> void print(Args... args) { (std::cout << ... << args ) << std::endl; } print(); print(1); print(2.f,100); -
lambda初始化捕获包(C++20),通过包展开来捕获多个初始化器
在C++20之前 ,参数包(如 Args... args)并不是一个集合,lambda 的捕获列表只能捕获单个变量。直接写 [args...] 或 [&args...] 语法可以通过,但每个元素都是独立捕获的,无法对整个包进行复制、移动或类型转换。 C++20时进行改进,允许将包打包成一个对象 ,[capture = expression],把参数包展开并存入 std::tuple、std::array 或自定义结构中
cpptemplate<typename ... Args> void foo(Args... args) { auto lamdba = [args...]{ };// 使用args... 的副本 auto lamdbaRef = [&args...]{ };// 引用args... } template<typename ... Args> void foos(Args... args) { // 将扩展包作为一个独立的整体进行move,并构造一个tuple auto lambda = [tup = std::make_tuple(std::move(args)...)] { std::apply([](auto&... elements) { (std::cout << ... << elements) << std::endl; },tup); }; } -
结构化绑定包(C++26)
结构化绑定声明中,用于引入零个或多个绑定标识符 ,通过使用...rest的语法,将其中一部分元素打包成一个新的参数包
cppint arr[4] = { 1 ,2, 3, 4 }; auto [...arrs] = arr; //结构化绑定 std::tuple<int, double, std::string> data{42, 3.14, "hello"}; // 捕获所有元素为一个包 auto [...all] = data; // all 是一个参数包,包含元素 {42, 3.14, "hello"} // 捕获第一个元素,其他元素组成包 auto [first, ...rest] = data; // first 是 int(42) // rest 是一个参数包,包含元素 {3.14, "hello"} // 捕获最后一个元素,前面元素组成包 auto [...init, last] = data; // last 是 string("hello") // init 是一个参数包,包含元素 {42, 3.14} // 混合使用:捕获头部和尾部元素,中间打包 auto [fst, ...mid, lst] = data;
-
-
模板参数包
-
常量模板参数包
cpptemplate <int ... Values> struct A { static constexpr int value = (0 + ... + Values); }; -
类型模板参数包
cpptemplate<typename... Types> struct B{}; -
受约束的类型模板参数包
cpptemplate <std::integral... Ts> void f(Ts... args){} -
模板模板参数包
cpptemplate <template<typename>class ... Containers> struct ContainerList{};
-
Pack展开,一个模式后面跟着省略号...,并且该模式至少出现一个包的名字
- 单包展开
cpp
template<class... Us>
void f(Us... us) {}
template<class... Ts>
void g(Ts... ts) { f(&args...); } // 其中,&args... 是一个包展开,&args称为其模式
g(1 , 0.2 , "a"); // Ts...展开为(int ,double ,const char*)
args...展开为三个实参 ( p1,p2,p3 ), &args...展开为 (&p1 , &p2 , &p3) ,最终调用为 f(int* p1, double* p2 , const char** p3)
- 多包展开
cpp
template <typename...>
struct Tuple{};
template <typename T1 , typename T2>
struct Pair{};
template< class... Args1>
struct zip
{
template<class... Args2>
struct with
{
typedef Tuple<Pair<Args1,Args2>...> type;
//Pair<Args1,Args2>... 是包展开 ,Pair<Args1,Args2>是模式
};
};
typedef zip<short,int>::with<unsigned short,unsigned>::type T1;
/*
Pair<Args1,Args2>... 展开如下:
Pair<short,unsigned short> , Pair<int , unsigned int>
T1实际为Tuple<Pair<short,unsigned short> , Pair<int,unsigned int >>
*/
对于一个模式中出现两个包(Args1 Args2),会同步展开,两个包长度必须相同。
注:单个模板参数列表中,参数包只能有一个,且必须是最后一个目标参数。如果处理两组关联的异构类型/值,需通过嵌套模板或std::tuple包装实现
- 嵌套包展开
cpp
template<class... Args>
void g(Args... args)
{
f( const_cast<const Args*>(&args)...);
// const_cast<const Args*>(&args)为模式,同时展开了Args(类型包)与args(值包) ,包长度必须相同且一一对应
}
template<class... Args>
void g(Args... args)
{
f(h(args...) + args...);
//内层展开 args... 展开为h(p1,p2,p3), 外层展开为 (h(p1,p2,p3) + args...)
// (h(p1,p2,p3) + args...) 再展开为 h(p1+p2+p3) + p1 , h(p1+p2+p3) + p2 , h(p1+p2+p3) + p3
}
- 空包展开
当一个扩展包的元素个数为0时,称为空包。空包展开实例化不会改变其外围结构的语法解释
cpp
template<class... Bases>
struct X : Bases... {};
template<class... Args>
void f(Args.. args) { X<Args...> x(args...); }
template void f<>(); //ok
包索引,也称为参数包下标,解决了无法直接、简洁地访问参数包中特定位置的元素
- 在 C++20 及之前,访问包中某个元素通过 1、 递归展开 2、std::tuple + std::get
cpp
// 递归 ,可读性差编译慢
template<size_t I, typename Head, typename... Tail>
decltype(auto) get_by_index(const Head& head, const Tail&... tail) {
if constexpr (I == 0) return head;
else return get_by_index<I-1>(tail...);
}
// 转成 tuple,存在拷贝或引用丢失的风险
template<typename... Args>
void foo(Args&&... args) {
auto t = std::forward_as_tuple(std::forward<Args>(args)...);
auto& second = std::get<1>(t); // 访问第二个元素
}
- C++26 包索引语法:args...[N]
从 C++26 开始,可以直接对参数包使用下标运算符(...[]),其中索引必须是编译期常量表达式
cpp
template<typename... Args>
void process(Args&&... args) {
// 访问第一个元素(索引 0)
auto&& first = args...[0];
// 访问第三个元素
auto&& third = args...[2];
// 可以用 constexpr 变量或整形常量
constexpr size_t idx = 2;
auto&& elem = args...[idx];
// 在编译期 if 中判断类型
if constexpr (std::is_same_v<decltype(args...[0]), int>) {
// ...
}
}
注: C++26同时支持typename...Ts的包索引,如Ts...[0] 可以取出第一个参数列表的类型
使用场景
- 打印任意数量参数
cpp
template<typename T , typename... Args>
std::unique_ptr<T> createObject(Args&&... args)
{
return std::make_unique<T>(std::forward<Args>(args)...);
}
class MyClass {
public:
MyClass(int a, std::string b)
{
std::cout << "a: << " << a << " ,b: << " << b << std::endl;
}
MyClass(int a, std::string b ,double c)
{
std::cout << "a: << " << a << " ,b: << " << b << " ,c: << " << c << std::endl;
}
};
int main()
{
auto obj1 = createObject<MyClass>(10, "test");
auto obj2 = createObject<MyClass>(100, "test1",20);
auto obj3 = createObject<MyClass>(170, "test2", 20.f);
auto obj4 = createObject<std::string>();
}