可变参数模板在C++17中的增强
可变参数模板在C++17中的增强
基本内容
可变参数模板, 称作variadic, 在C++11中就引入了, 但是那个时候需要人工编写递归表达式. 在C++中对此进行了增强. 因此, 在绝大多数情况下, 不需要再通过人工编写递归表达式, 简化了开发.
比如, 下面这个求和的函数:
1 | template<typename... T> |
在C++17之前, 我们需要这么实现:
1 | template<typename... T> |
不过就我个人来看, 其实还是老的模式更容易理解一些. 新的方式下还要记住左折叠, 右折叠, 等等一堆的限制, 反倒不如老的递归模式更清晰, 写起来更可靠.
其展开后的形式是(((arg1+arg2)+arg3)+...)
.
注意的一点是, return的表达式的括号一定不能省略. 省略了编译器会报错
两种单目折叠形式: 左折叠和右折叠
左折叠, 例如 (... op args)
, 其展开形式为: ((arg1 op arg2) op arg3) op ...
右折叠, 例如 (args op ...)
, 其展开形式为: arg1 op (arg2 op ...(argN-1 op argN))
这里要注意的一点是, 第一个被计算的参数的类型, 一定是要能够支持op
操作符的. 不然就会出错. 例如, 上面的例子来说, 下面的代码, 对于上面的foldSum()
函数(我们是左折叠的实现),
std::cout << foldSum(std::string("hellow"), "world", "!") << endl;
能够编译成功, 而对于下面的代码:
std::cout << foldSum("hellow", "world", std::string("!")) << endl;
就编译失败. 道理也很简单:
对左折叠: ... + args
的模板,
第一个代码被展开为: (std::string("hellow) + "world") + "!"
,
第二个代码被展开为: ("hellow" + "world") + std::string("!")
, const char*
不支持+
, 所以编译失败.
对空参数包的处理
翻译有些令人迷惑, 需要有时间去实测一下
- 如果操作符为
&&
, 结果为true
- 如果操作符为
||
, 结果为false
- 如果操作符为
,
, 结果为void
- 所有其他情况, 都是错误
它的意思, 应该是, 比如, 对于前面的
foldSum(...)
函数, 下面的调用形式foldSum()
会编译错误的.
所以C++17中还定义了双目折叠:
双目折叠形式:
同样, 双目折叠也分成左折叠和右折叠两种:
左折叠 (value op ... op args)
, 展开为 (((value op arg1) op arg2) op arg3) op ...
右折叠 (args op ... op value)
, 站卡为 arg1 op (arg2 op ...(argN op value)))
这样, 我们可以强制要求定义函数foldSum()
必须有一个参数.
作为一个普遍的原则, 在大多数情况下, 左折叠的形式更符合我们阅读代码的习惯, 因此, 在没有忒别限制的情况下, 使用左折叠的方式具有更好的可读性.
示例: 打印被分隔符隔开的参数列表
例如, 在控制台上打印hellow 42 world
知识点: 如何在参数中间增加分隔符的打印
方法1:
1 | template <typename T> |
方法2:
将上面的辅助函数改为lambda:
1 | template <typename T1, typename... Ts> |
方法3:
方法2中的lambda缺省返回了一个临时对象, 这意味着需要创建一个不必要的临时对象. 我们可以修改其实现, 让它返回参数的引用. 我们可以使用const auto&
或decltype(auto)
:
1 | template <typename T1, typename... Ts> |
方法4:
我们也可以把lambda写到一个表达式里面:
1 | template <typename T1, typename... Ts> |
这个就是炫技了, 除了让读代码的人费劲, 没啥好处.
方法5:
下面其实是最有价值的做法. 使用一个lambda来打印空格和参数. 这种做法具有普遍的使用价值, 对于很多其他的场景也可以参照. 而前面几种方法都很特化.
1 | template <typename T1, typename... Ts> |
比如下面的代码:
1 | template<typename... Ts, ApiMethodType=ApiMethodType::putMethod> |
这段代码用于拼装Restful的api字符串, 在参数之间添加分隔符'/'.
示例: 实现回调函数
这是一个最简单的回调框架( 类似于Python中的装饰器? ). 我们可以在它的前后插入各种插装动作, 手工实现各种策略. 本质上, C++的面向策略编程, 大概率就是基于这种方式进行的.
1 | inline auto callback_template = [](auto&& func, auto&&... params) |
示例: 自动调用所有基类的成员函数
1 | template <typename... Bases> |
可以看到, 调用基类的顺序和类声明的顺序是完全一致的, 在里面, 代码(..., Bases::print());
被展开为:(A::print(), B::print()), C::print();
示例: 组合哈希计算函数
下面的例子, 同样利用折叠表达式来计算多个参数的哈希值.
这个实现能够一定程度上简化, 但是并不能自动化结构成员的哈希计算. 这个要求可能太高, 即使有了反射, 我们还需要有一种描述手段来通知编译器哪个成员不需要哈希计算. 但是这是自动化的第一步:
1 | template <typename T> |
这样测试:
1 | std::size_t a{combinedHashValue(std::string("Hellow"), std::string("world"), 2023)}; |
注意, 这里, 不支持字符串指针的方式, 必须传过去std::string
, 可能是MSVC的编译器的问题?
使用折叠表达式在二叉树中使用->*
来遍历路径:
1 | struct Node |
使用:
1 | void test_FoldTraverse() |
在type traits中使用折叠表达式
1 |
|
对于下面的代码:
1 | auto a1 = IsHomogeneous<int, std::size_t, decltype(42)>::value; |
会被展开为:
1 | std::is_same<int, std::size_t>::value && std::is_save<int, decltype(42)>::value |
同样, 函数调用
1 | isHomogeneous(43, -1, "hello"); |
会被展开为
1 | std::is_same<int, int>::value && std::is_same<int, const char*>::value |