当前位置: 首页 > article >正文

[C++11]可变参数模板和参数包展开

可变参数模板

文章目录

    • 可变参数模板
      • 可变参数模板的概念
      • 可变参数模板的定义方式
    • 模板参数包的展开
      • 递归展开参数包
        • sizeof...计算参数包大小
      • 逗号表达式展开参数包
      • enable_if方式展开
      • 折叠表达式展开
    • 总结

可变参数模板的概念

可变参数模板(Variadic templates)C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。

  • C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
  • 在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。

可变参数模板的定义方式

template <class... T>
void func(T... args)
{
    //...
}

上面的我们把带…的模板参数称为模板参数包(template parameter pack)

上面这个函数模板的参数 args 前面有省略号,我们称之为模板参数包(template parameter pack)的可变模版参数,它里面包含了0到N个模版参数,而我们是无法直接获取 args 中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数。

模板参数包的展开

递归展开参数包

递归展开的方式如下:

  • 函数模板除去参数包可变模板参数外至少要有一个模板参数,用于从参数包中拿出一个参数
  • 剩余参数包递归调用
  • 每次剥离一个参数,直至参数包空
template <class T, class... Args>
void foo(T first, Args... args)
{
    cout << first << " ";
    foo(args...);
}
//int main()
//foo("good", 2, "hello", 4, 110);

我们写出如上一个带可变模板参数的函数foo,我们尝试在main函数中调用发现无法调用

有递归自然有出口,当函数包空时,仍会递归一个空包作为参数,而我们没有空参数函数,所以我们还要再增加一个空参数的函数来进行特化

如下:

void foo()
{
    cout << endl;
}

template <class T, class... Args>
void foo(T first, Args... args)
{
    cout << first << " ";
    foo(args...);
}

int main()
{
    foo(1, 2, 3, 4);
    foo("good", 2, "hello", 4, 110);
    return 0;
}
//输出
//1 2 3 4 
//good 2 hello 4 110

当然我们可以规定递归出口为其他数量的参数,如:

void foo(int a)
{
    cout << endl;
}

template <class T, class... Args>
void foo(T first, Args... args)
{
    cout << first << " ";
    foo(args...);
}

int main()
{
    foo(1, 2, 3, 4);
    foo("good", 2, "hello", 4, 110);
    return 0;
}
//输出
//1 2 3 
//good 2 hello 4

当然,当我们定义递归出口为一个参数的函数时,我们调用foo必须传入不少于一个参数

sizeof…计算参数包大小

我们其实是可以计算参数包的大小的,如sizeof…(args)

void foo()
{
    cout << endl;
}
template <class T, class... Args>
void foo(T first, Args... args)
{
    cout << first << " " << sizeof...(args) << endl;
    foo(args...);
}

int main()
{
    foo(1, 2, 3, 4);
    return 0;
}
//输出
//1 3
//2 2
//3 1
//4 0

那么我们是否可以通过对参数包大小的判断来结束函数递归,从而省去无参数或者少参数函数作为递归出口呢?

template <class T, class... Args>
void foo(T first, Args... args)
{
    cout << first << " " << sizeof...(args) << endl;
    if (!sizeof...(args))
        return;
    foo(args...);
}

我们发现直接报错了,也就是说这种方式不可行

  • 我们在学习函数模板时知道,函数模板并不能直接调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
  • 而这个推演过程是在编译时进行的,我们函数体中递归调用函数,也就是说推演会不断继续下去,仍会进行参数包为空的函数推演,此时就会报错了,因为我们的函数至少要有一个参数,而又没有重载的空参数函数了
  • 这里的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。

逗号表达式展开参数包

逗号表达式展开包其实是利用了C++11新特性,列表初始化。

我们列表初始化的原理就是先用列表构建initializer_list,再用initializer_list去构建我们的容器

如果我们把参数包放入初始化列表中会怎样呢?

template <class... Args>
void foo(Args... args)
{
    initializer_list<int> a{args...};
    for (auto x : a)
        cout << x << " ";
}
//输出
//1 2 3 4 

我们发现参数包放入初始化列表中,由于初始化列表从左往右执行,参数包中的参数会被逐个取出,此时由于没有递归展开,所以我们不需要再额外定义空参数的重载函数,传入参数也没有数目限制。

利用初始化列表和逗号表达式结合,我们可以如下展开参数包:

template <class... Args>
void foo(Args... args)
{
    (void)initializer_list<int>{(cout << args << " ", 0)...};
}

int main()
{
    foo(1, 2, 3, 4, "GenshinImpact", 3.14);
    return 0;
}
//输出
//1 2 3 4 GenshinImpact 3.14

我们发现很顺利的输出了,甚至不受类型限制

其实剖析一下发现逗号表达式是一种很犯规的写法,我们逗号表达式的返回值是最右边的表达式,也就是0,所以最终用来初始化列表的元素是0,但是由于列表初始化要从左向右执行,所以我们的参数包会被展开,假如参数包有N个参数,我们展开N次,但是此次返回值都是0,所以得到了N个0的列表,而参数包内的内容都被输出了。

enable_if方式展开

enable_if是C++11新引入的一个结构体,定义如下:

  // Primary template.
  /// Define a member typedef @c type only if a boolean constant is true.
  template<bool, typename _Tp = void>
    struct enable_if
    { };

  // Partial specialization for true.
  template<typename _Tp>
    struct enable_if<true, _Tp>
    { typedef _Tp type; };

我们可以看出下面是上面的一个偏特化。当我们传入第一个参数为true时会用第二个模板来实例化,将_Tp typedef为type,而第一个模板什么也没做。

故而enable_if常用于需要根据不同的类型的条件实例化不同模板的情形。也就是说,在不同条件下选用不同类型,其广泛的应用在 C++ 的模板元编程(meta programming)之中,利用的就是SFINAE原则,英文全称为Substitution failure is not an error,意思就是匹配失败不是错误,假如有一个特化会导致编译时错误,只要还有别的选择,那么就无视这个特化错误而去选择另外的实现。

因而我们可以借此来解决我们递归展开函数包递归出口函数和参数限制的问题。

具体流程就是:

  • 利用参数包构建tuple(元组)
  • 以下标访问元组元素,同时利用下标是否等于元组元素个数作为条件重载两个函数
  • 当下标小于value,那么对对应下标元素操作
  • 当下标等于value,则进入对应函数体

代码如下:

template <size_t k = 0, class tup>
typename enable_if<k == std::tuple_size<tup>::value>::type _foo(const tup &t)
{
    cout << endl;
}
template <size_t k = 0, class tup>
    typename enable_if < k<std::tuple_size<tup>::value>::type _foo(const tup &t)
{
    cout << get<k>(t) << " ";
    _foo<k + 1, tup>(t);
}

template <class... Args>
void foo(Args... args)
{
    _foo<0>(make_tuple(args...));
}

int main()
{
    foo(2023, "GenshinImpact", "hello", 2024);
    return 0;
}

优雅,实在是太优雅了。

折叠表达式展开

前面几种都是C++11的内容,而我们的折叠表达式(Fold Expressions)则是我们C++17的新语法特性,使用折叠表达式可以简化对C++11中引入的参数包的处理,可以在某些情况下避免使用递归,更加方便的展开参数。

如下示例:

template <class... Args>
void foo(Args... args)
{
    (cout << ... << args) << endl;
}

int main()
{
    foo(2023, "GenShinImpact", "hello");
    return 0;
}

简洁了不少,但是如何格式化呢?需要增加格式化辅助函数。

template <class T>
string format(const T &t)
{
    stringstream ss;
    ss << " " << t << " ";
    return ss.str();
}

template <class... Args>
void foo(Args... args)
{
    (cout << ... << format(args)) << endl;
}

int main()
{
    foo(2023, "GenShinImpact", "hello");
    return 0;
}
//输出
// 2023  GenShinImpact  hello 

也可以直接利用逗号表达式进行简化

template <class... Args>
void foo(Args... args)
{
    (cout << ... << (cout << args, " ")) << endl;
}

int main()
{
    foo(2023, "GenShinImpact", "hello");
    return 0;
}
//输出
//2023 GenShinImpact hello 

括号里逗号表达式的返回值是" “,当输出完从参数包里拆出的args,返回” "给左边的输出流输出

总结

可变参数模板参数高度泛化,提高了编程的泛用性。

而为了实现可变参数模板我们引入了参数包,于是需要对参数包进行展开,我们可以:

  1. 通过递归每次拆出一个参数,展开参数包
  2. 利用初始化列表展开参数包
  3. 通过enable_if和元组结合展开参数包
  4. C++17直接引入折叠表达式,便于展开参数包

http://www.kler.cn/news/137110.html

相关文章:

  • 【Flutter】基础入门:开发环境搭建
  • 2024软考网络工程师笔记 - 第10章.组网技术
  • js---三元表达式详解
  • Track 01:Intro
  • Excel重新踩坑3:条件格式;基本公式运算符;公式中的单元格引用方式;公式菜单栏其他有用的功能说明;
  • Linux系统——ssh远程连接
  • 打破传统束缚,释放服务潜能:本地生活服务商聚合系统引领行业新风向!
  • 2013年12月13日 Go生态洞察:Go在App Engine上的工具、测试和并发
  • SpringBoot 集成Sa-Token 一个轻量级Java权限认证框架,让鉴权变得简单、优雅!
  • C++11的unique_ptr独占的智能指针
  • 同为科技(TOWE)工业连接器:保障高效、可靠、安全的电气连接
  • 【报错记录】解决使用Kotlin写的SpringBoot项目使用Aspect切面无法生效的问题
  • axios的封装之axios是基于什么封装的?
  • Web 自动化神器 TestCafe(二)—元素定位篇
  • 七大查找算法
  • 【图数据库实战】gremlin语法
  • c# IEnumerable--扩展方法
  • SD-WAN技术:重新定义网络连接方式
  • less相关
  • 基于STC12C5A60S2系列1T 8051单片机的模数芯片ADC0832实现模数转换应用
  • 【开发流程】持续集成、持续交付、持续部署
  • Android 13.0 Launcher3仿ios长按app图标实现抖动动画开始拖拽停止动画
  • Hibernate查询的方法
  • 维基百科文章爬虫和聚类【二】:KMeans
  • py Selenium来启动多个浏览器窗口或标签页,并操作它们
  • 回顾以前的java