变参模板
00:00
可变参数模板与折叠表达式
概述
可变参数模板(Variadic Templates)是 C++11 引入的核心特性,允许模板接受任意数量、任意类型的参数。这一特性是现代 C++ 元编程的基石,使得类型安全的泛型容器、完美转发的工厂函数和编译期类型遍历成为可能。C++17 引入的折叠表达式进一步简化了参数包的操作,消除了递归模板实例化的需要。
可变参数模板的典型应用包括:std::make_unique、std::tuple、std::variant 和 std::visit 等标准库组件。
基础概念
参数包
- 模板参数包:
typename... Args,接受零个或多个类型参数 - 函数参数包:
Args... args,接受零个或多个函数参数 - 参数包展开:
args...,将参数包展开为逗号分隔的参数列表
sizeof… 运算符
template<typename... Args>
void printCount() {
std::cout << "参数数量: " << sizeof...(Args) << std::endl;
std::cout << "参数数量: " << sizeof...(Args) << std::endl;
}
printCount<int, double, std::string>(); // 参数数量: 3
printCount<>(); // 参数数量: 0
快速上手
基本的变参模板函数
#include <iostream>
// 使用折叠表达式打印所有参数
template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...); // C++17 折叠表达式
std::cout << std::endl;
}
// 使用
print(1, "hello", 3.14, "world"); // 输出: 1 hello 3.14 world
print("仅一个参数"); // 输出: 仅一个参数
print(); // 输出: (空行)
折叠表达式
C++17 引入的折叠表达式是操作参数包的简洁语法:
// 四种折叠形式
// 1. 一元右折叠: (pack op ...)
// 展开为: e1 op (e2 op (... op en))
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 右折叠求和
}
// sum(1, 2, 3) = 1 + (2 + 3) = 6
// 2. 一元左折叠: (... op pack)
// 展开为: ((e1 op e2) op e3) op ...
template<typename... Args>
auto sumLeft(Args... args) {
return (... + args); // 左折叠求和
}
// 3. 二元右折叠: (pack op ... op init)
template<typename... Args>
auto sumWithInit(Args... args) {
return (args + ... + 0); // 带初始值的右折叠
}
// 4. 二元左折叠: (init op ... op pack)
template<typename... Args>
auto allTrue(Args... args) {
return (true && ... && args); // 带初始值的左折叠,空包返回 true
}
详细用法
递归式参数包处理(C++11/14 风格)
#include <iostream>
// 递归终止条件
void print() {}
// 递归展开参数包
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print(rest...); // 递归调用,参数包减少一个
}
// 使用
print(1, 2.5, "hello"); // 输出: 1, 2.5, hello
完美转发与变参模板
#include <memory>
#include <utility>
// 完美转发参数包到构造函数
template<typename T, typename... Args>
std::unique_ptr<T> makeUnique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// 使用
auto ptr = makeUnique<std::string>(5, 'a'); // 构造 "aaaaa"
auto ptr2 = makeUnique<std::string>("hello"); // 构造 "hello"
变参模板与 tuple
#include <tuple>
#include <iostream>
// 遍历 tuple 的每个元素
template<typename Tuple, typename Func, size_t... Is>
void forEachImpl(Tuple& t, Func&& func, std::index_sequence<Is...>) {
(func(std::get<Is>(t)), ...); // 折叠表达式遍历
}
template<typename... Args, typename Func>
void forEach(std::tuple<Args...>& t, Func&& func) {
forEachImpl(t, std::forward<Func>(func),
std::index_sequence_for<Args...>{});
}
// 使用
auto data = std::make_tuple(42, 3.14, "hello");
forEach(data, [](const auto& item) {
std::cout << item << " ";
});
// 输出: 42 3.14 hello
变参模板类
// 类型列表
template<typename... Types>
struct TypeList {};
// 获取类型数量
template<typename List>
struct Size;
template<typename... Types>
struct Size<TypeList<Types...>> {
static constexpr size_t value = sizeof...(Types);
};
// 在头部添加类型
template<typename T, typename List>
struct PushFront;
template<typename T, typename... Types>
struct PushFront<T, TypeList<Types...>> {
using type = TypeList<T, Types...>;
};
// 使用
using MyList = TypeList<int, double, std::string>;
static_assert(Size<MyList>::value == 3);
using Extended = PushFront<char, MyList>::type;
static_assert(Size<Extended>::value == 4);
常见场景
类型安全的 printf
#include <iostream>
#include <string>
// 类型安全的格式化输出
template<typename T>
void formatPrint(std::ostream& os, const std::string& fmt, const T& value) {
auto pos = fmt.find("{}");
if (pos != std::string::npos) {
os << fmt.substr(0, pos) << value << fmt.substr(pos + 2);
}
}
template<typename T, typename... Args>
void formatPrint(std::ostream& os, const std::string& fmt,
const T& first, const Args&... rest) {
auto pos = fmt.find("{}");
if (pos != std::string::npos) {
os << fmt.substr(0, pos) << first;
formatPrint(os, fmt.substr(pos + 2), rest...);
}
}
// 使用
formatPrint(std::cout, "姓名: {}, 年龄: {}, 分数: {}\n",
"张三", 25, 95.5);
// 输出: 姓名: 张三, 年龄: 25, 分数: 95.5
多类型事件系统
#include <functional>
#include <unordered_map>
#include <any>
// 支持多种事件类型的事件总线
class EventBus {
std::unordered_map<size_t, std::any> handlers_;
public:
template<typename Event, typename Handler>
void subscribe(Handler&& handler) {
handlers_[typeid(Event).hash_code()] =
std::function<void(const Event&)>(std::forward<Handler>(handler));
}
template<typename Event>
void publish(const Event& event) {
auto it = handlers_.find(typeid(Event).hash_code());
if (it != handlers_.end()) {
auto handler = std::any_cast<std::function<void(const Event&)>>(&it->second);
if (handler) (*handler)(event);
}
}
};
注意事项
- 参数包必须在展开时使用,不能直接对参数包进行算术运算
sizeof...(Args)是编译期常量,可用于模板条件和 static_assert- 折叠表达式中的空参数包行为:
&&返回 true,||返回 false,+需要+ 0初始值,*需要* 1初始值 - 递归式参数包处理会增加编译时间和编译产物大小,优先使用折叠表达式
- 参数包展开的位置有限制:只能在函数参数列表、模板参数列表、初始化列表、基类列表等处展开
进阶用法
编译期类型遍历
#include <variant>
// 编译期遍历 variant 的所有可能类型
template<typename Variant, typename Func, size_t... Is>
auto visitAllImpl(Func&& func, std::index_sequence<Is...>) {
return std::array{func(std::variant_alternative_t<Is, Variant>{})...};
}
template<typename Variant, typename Func>
auto visitAll(Func&& func) {
return visitAllImpl<Variant>(
std::forward<Func>(func),
std::make_index_sequence<std::variant_size_v<Variant>>{}
);
}
// 使用
using MyVariant = std::variant<int, double, std::string>;
auto results = visitAll<MyVariant>([](auto val) {
return typeid(val).name();
});
C++20 Lambda 捕获包展开
// C++20: 在 Lambda 中展开参数包
template<typename... Args>
void forEachArg(Args... args) {
// 使用初始化捕获展开参数包
[... captures = std::move(args)]() {
((std::cout << captures << "\n"), ...);
}();
}