C++模板教程:变参模板、折叠表达式与SFINAE

🕒 阅读时间:16 分钟📝 字数:2656👀 阅读量:Loading...

本文是 C++ 系列教程的第 18 篇。上一篇讲解了特化与类型萃取,本篇深入模板高级技巧:变参模板(参数包、sizeof…、递归展开)、C++17 折叠表达式、SFINAE 与 enable_if、void_t 技巧、C++20 concepts 预告。

一、变参模板

1.1 什么是变参模板

变参模板(Variadic Templates)允许模板接受任意数量的参数,C++11 引入:

// 参数包:typename... Args
template <typename... Args>
void print(Args... args);
  • Args... 是类型参数包。
  • args... 是函数参数包。
  • sizeof...(Args) 获取参数个数。

1.2 参数包展开与递归

#include <iostream>
using namespace std;
// 递归终止条件:空参数版本
void printAll() {
cout << endl;
}
// 递归展开:取出第一个参数,剩余继续递归
template <typename T, typename... Rest>
void printAll(T first, Rest... rest) {
cout << first << " ";
printAll(rest...); // 递归调用剩余参数
}
int main() {
printAll(1, 2.5, "hello", 'a'); // 1 2.5 hello a
printAll(10); // 10
return 0;
}

1.3 sizeof… 获取参数个数

#include <iostream>
using namespace std;
template <typename... Args>
void countArgs(Args... args) {
cout << "参数个数: " << sizeof...(Args) << endl;
cout << "参数个数: " << sizeof...(args) << endl; // 等价
}
int main() {
countArgs(); // 0
countArgs(1); // 1
countArgs(1, 2.5, "three"); // 3
return 0;
}

二、折叠表达式(C++17)

2.1 折叠表达式语法

折叠表达式对参数包中的全部元素应用二元运算符,大大简化变参运算:

形式 含义
(pack op ...) 右折叠:a op (b op (c op init))
(... op pack) 左折叠:((init op a) op b) op c
(pack op ... op init) 带初始值的右折叠
(init op ... op pack) 带初始值的左折叠

2.2 折叠求和

#include <iostream>
using namespace std;
// C++17 折叠:一行实现任意个数求和
template <typename... Args>
auto sum(Args... args) {
return (args + ... + 0); // 右折叠,初始值 0
}
// 左折叠
template <typename... Args>
auto sumLeft(Args... args) {
return (0 + ... + args);
}
int main() {
cout << sum(1, 2, 3, 4, 5) << end
l; // 15
cout << sum(1.5, 2.5, 3.0) << endl; // 7
cout << sum() << endl; // 0
cout << sumLeft(1, 2, 3) << endl; // 6
return 0;
}

2.3 折叠打印(对比递归)

#include <iostream>
using namespace std;
// 用逗号运算符折叠打印
template <typename... Args>
void printFold(Args... args) {
// (cout << ... << args):左折叠
// 逐个输出
((cout << args << " "), ...); // 逗号折叠
cout << endl;
}
// 更实用的版本:带分隔符
template <typename... Args>
void printWithSep(const char *sep, Args... args) {
// 第一个直接输出,其余带分隔符
((cout << args), ...); // 简化版
cout << endl;
}
int main() {
printFold(1, 2.5, "hi", 'x'); // 1 2.5 hi x
printWithSep(", ", 1, 2, 3);
return 0;
}

2.4 折叠判断(逻辑运算)

#include <iostream>
using namespace std;
// 全部满足条件(&& 折叠)
template <typename... Args>
bool allPositive(Args... args) {
return ((args > 0) && ...);
}
// 任一满足条件(|| 折叠)
template <typename... Args>
bool anyZero(Args... args) {
return ((args == 0) || ...);
}
int main() {
cout << allPositive(1, 2, 3) << endl; // 1
cout << allPositive(1, -2, 3) << endl; // 0
cout << anyZero(1, 2, 0, 4) << endl; // 1
cout << anyZero(1, 2, 3) << endl; // 0
return 0;
}

三、SFINAE 基础

3.1 什么是 SFINAE

SFINAE(Substitution Failure Is Not An Error,替换失败不是错误):模板实例化时,如果某个候选替换失败(如类型不支持某操作),编译器不会报错,而是继续尝试其他候选。

3.2 利用 SFINAE 做类型判断

#include <iostream>
#include <type_traits>
using namespace std;
// 通用版本:不支持 operator<< 的类型走这里
template <typename T>
void printValue(const T &value, ...) {
cout << "不支持输出: (未知类型)" << endl;
}
// 精确版本:支持 operator<< 的类型走这里
template <typename T, typename = decltype(cout << declval<const T &>())>
void printValue(const T &value, int) {
cout << "值: " << value << endl;
}
int main() {
printValue(42, 0); // 值: 42
printValue(3.14, 0); // 值: 3.14
printValue("hello", 0); // 值: hello
return 0;
}

3.

3 enable_if 条件启用

#include <iostream>
#include <type_traits>
using namespace std;
// 仅当 T 是整数时启用
template <typename T>
typename enable_if<is_integral<T>::value>::type
process(T value) {
cout << "整数处理: " << value << endl;
}
// 仅当 T 是浮点时启用
template <typename T>
typename enable_if<is_floating_point<T>::value>::type
process(T value) {
cout << "浮点处理: " << value << endl;
}
int main() {
process(42); // 整数处理: 42
process(3.14); // 浮点处理: 3.14
// process("hi"); // 错误!string 不满足任何版本
return 0;
}

3.4 enable_if 的两种写法

#include <iostream>
#include <type_traits>
using namespace std;
// 写法一:返回类型中启用
template <typename T>
typename enable_if<is_integral<T>::value, T>::type
square(T x) {
return x * x;
}
// 写法二:模板参数中启用(C++11 更通用)
template <typename T,
typename = typename enable_if<is_floating_point<T>::value>::type>
double squareDouble(T x) {
return x * x;
}
int main() {
cout << square(5) << endl; // 25(int 版本)
cout << squareDouble(2.5) << endl; // 6.25(double 版本)
return 0;
}

四、void_t 技巧

4.1 void_t 检测特性

#include <iostream>
#include <type_traits>
using namespace std;
// void_t:任何类型都映射为 void(C++17 标准库提供)
template <typename...>
using void_t = void;
// 检测 T 是否有成员函数 size()
template <typename T, typename = void>
struct HasSize : false_type {};
template <typename T>
struct HasSize<T, void_t<decltype(declval<T>().size())>> : true_type {};
// 检测 T 是否有成员 type
template <typename T, typename = void>
struct HasType : false_type {};
template <typename T>
struct HasType<T, void_t<typename T::type>> : true_type {};
int main() {
cout << "string 有 size(): " << HasSize<string>::value << endl; // 1
cout << "int 有 size(): " << HasSize<int>::value << endl; // 0
struct WithType { using type = int; };
cout << "WithType 有 type: " << HasType<WithType>::value << endl; // 1
cout << "int 有 type: " << HasType<int>::value << endl; // 0
return 0;
}

4.2 void_t 检测可调用性

#include <iostream>
#include <type_traits>
using namespace std;
template <typename...>
using void_t = void;
// 检测 T 是否支持 operator<<
template <typename T, typename = void>
struct IsPrintable : false_type {};
template <typename T>
struct IsPrintable<T,
void_t<decltype(cout << declval<const T &>())>> : true_type {};
struct MyStruct {}; // 不支持输出
int main() {
cout << "int 可打印: " << IsPrintable<int>::value << endl; // 1
cout << "string 可打印: " << IsPrintable<string>::value << endl; // 1
cout << "MyStruct 可打印: " << IsPrintable<MyStruct>::value << endl; // 0
return 0;
}

五、变参模板实战

5.1 类型安全的 printf

#include <iostream>
using namespace std;
// 基础版本:无参数
void myPrintf(const char *format) {
cout << format << endl;
}
// 递归展开版本
template <typename T, typename... Args>
void myPrintf(const char *format, T value, Args... args) {
while (*format) {
if (*format == '%' && *(format + 1) == 'd') {
cout << value; // 输出参数
format += 2;
myPrintf(format, args...); // 递归剩余
return;
}
cout << *format++;
}
}
int main() {
myPrintf("数字: %d", 42);
myPrintf("%d 加 %d 等于 %d", 1, 2, 3);
return 0;
}

5.2 变参构造函数(完美转发预览)

#include <iostream>
#include <vector>
using namespace std;
// 自定义容器:支持任意参数构造
template <typename T>
class MyContainer {
private:
vector<T> data;
public:
// 变参构造函数
template <typename... Args>
MyContainer(Args... args) {
data.reserve(sizeof...(Args));
(data.push_back(args), ...); // C++17 逗号折叠
}
void show() const {
for (const auto &item : data) cout << item << " ";
cout << endl;
}
size_t size() const { return data.size(); }
};
int main() {
MyContainer<int> c1(1, 2, 3, 4, 5);
cout << "大小: " << c1.size() << endl; // 5
c1.show(); // 1 2 3 4 5
MyContainer<string> c2("C++", "Python");
c2.show(); // C++ Python
return 0;
}

5.3 变参模板应用场景

  1. std::make_shared/make_unique:参数转发给构造函数。
  2. std::tuple:存储任意类型任意个数。
  3. printf 风格格式化。
  4. 事件系统:任意参数的通知。
  5. *工厂模式 *:参数转发。

六、C++20 concepts 预告

6.1 concepts 简化约束

C++20 的 concepts 让模板约束更简洁直观:

#include <iostream>
#include <concepts>
using namespace std;
// 定义概念:必须是整数类型
template <typename T>
concept Integral = is_integral_v<T>;
// 用概念约束模板参数
template <Integral T>
T add(T a, T b) {
return a + b;
}
// 简化写法
template <typename T>
requires Integral<T>
T multiply(T a, T b) {
return a * b;
}
int main() {
cout << add(3, 4) << endl; // 7
cout << multiply(5, 6) << endl; // 30
// add(3.5, 4.5); // 错误!double 不满足 Integral
return 0;
}

6.2 concepts vs enable_if

维度 enable_if concepts
可读性 冗长晦涩 直观清晰
报错信息 深奥难懂 友好明确
语法 模板技巧 标准语法
标准 C++11 C++20

七、实战:任意类型最大值

综合本篇知识,实现支持任意参数个数的 max:

#include <iostream>
#include <type_traits>
using namespace std;
// 两个参数版本(递归终止)
template <typename T>
T myMax(T value) {
return value;
}
// 变参版本:比较第一个和剩余的最大值
template <typename T, typename... Args>
auto myMax(T first, Args... rest) {
auto restMax = myMax(rest...); // 递归求剩余最大值
return first > restMax ? first : restMax;
}
// 折叠表达式版本(C++17 更简洁)
template <typename... Args>
auto foldMax(Args... args) {
return (args > ...); // 不适用
}
// 正确折叠版
template <typename First, typename... Rest>
auto foldMax2(First first, Rest... rest) {
auto result = first;
((result = (rest > result ? rest : result)), ...);
return result;
}
int main() {
cout << myMax(3, 7, 2) << endl; // 7
cout << myMax(1.5, 3.2, 2.8, 4.1) << endl; // 4.1
cout << foldMax2(10, 20, 5, 15) << endl; // 20
cout << foldMax2(3.5, 2.5) << endl; // 3.5
return 0;
}

总结

本篇讲解了变参模板(参数包、sizeof…、递归展开)、C++17 折叠表达式(求和、打印、逻辑判断)、SFINAE 与 enable_if(条件启用)、void_t 技巧(特性检测)、C++20 concepts 预告,并用任意类型最大值串联实战。重点掌握:参数包递归展开模式、折叠表达式的四种形式、enable_if 的两种写法、void_t 检测原理。

下一篇将讲解类型推导与完美转发(auto/decltype/右值引用/完美转发),敬请期待!

C++模板教程:变参模板、折叠表达式与SFINAE

作者:Wang Shijie

本文链接:https://wsj-blog.pages.dev/posts/c-mu-ban-jiao-cheng-bian-can-mu-ban-zhe-die-biao-da-shi-yu/

本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

Creative Commons

留言评论