C++C++17终极元组解包器 - std::apply
苏丙榅1. std::apply 概述
std::apply 是 C++17 引入的一个实用工具函数,其主要目的是将元组(或类元组对象)解包为函数调用的参数。这解决了在 C++14 及之前版本中,需要手动解包元组参数调用函数的繁琐问题。
函数原型与语法:
1 2 3 4 5 6 7
| #include <tuple>
namespace std { template <class F, class Tuple> constexpr decltype(auto) apply(F&& f, Tuple&& t); }
|
模板参数:
F:可调用对象类型(函数、函数指针、函数对象、lambda 等)
Tuple:类元组类型(std::tuple、std::pair、std::array 或任何支持 std::get 和 std::tuple_size 的类型)
返回值:
- 返回
f 的调用结果
- 使用
decltype(auto) 自动推导返回类型,完美转发返回值
C++23 之前只有元组版本,C++23 增加了类似 std::invoke 的按序传参版本,这里我们专注 C++17 标准。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void my_func(int a, double b) { std::cout << a << ", " << b << std::endl; }
int main() { std::tuple<int, double> t = {1, 2.5}; my_func(std::get<0>(t), std::get<1>(t)); std::apply(my_func, t); return 0; }
|
std::apply 的核心依赖于 C++14 引入的整数序列机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| template <typename F, typename Tuple, std::size_t... I> constexpr decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>) { return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...); }
template <typename F, typename Tuple> constexpr decltype(auto) apply(F&& f, Tuple&& t) { return apply_impl( std::forward<F>(f), std::forward<Tuple>(t), std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{} ); }
|
工作流程:
- 获取元组大小
N
- 生成索引序列
0, 1, ..., N-1
- 使用折叠表达式展开参数包
- 通过
std::invoke 调用目标函数
2. std::apply 应用
2.1 普通函数与 std::apply
这是最基础的用法,演示如何将元组展开传给函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <tuple> #include <functional>
void add(int a, int b) { std::cout << "a + b = " << (a + b) << std::endl; }
void print_info(int id, std::string name, double score) { std::cout << "ID: " << id << ", Name: " << name << ", Score: " << score << std::endl; }
int main() { std::tuple<int, int> t1 = {10, 20}; std::apply(add, t1);
std::tuple<int, std::string, double> t2 = {101, "Alice", 95.5}; std::apply(print_info, t2);
return 0; }
|
2.2 Lambda 表达式与 std::apply
Lambda 非常适合配合 std::apply 做一些临时的计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <tuple> #include <functional>
int main() { auto t = std::make_tuple(2, 3);
std::apply([](int x, int y) { std::cout << "x * y = " << x * y << std::endl; }, t);
int base = 100; std::apply([base](int x, int y) { std::cout << "base + x + y = " << base + x + y << std::endl; }, t);
return 0; }
|
2.3 成员函数与 std::apply
成员函数与std::apply的配合使用是最容易出错的地方。还记得 std::invoke 吗?
std::invoke(&Class::func, obj, args...)
std::apply 只接受两个参数:函数和元组。
因此,对象本身必须也打包进元组里!在 C++17 中,获取 std::tuple 对象主要有以下几种方式。
使用 std::tuple 的构造函数
C++17 之前这需要显式指定模板参数,很冗长。但在 C++17 中,配合**类模板参数推导 (CTAD)**,你可以直接写构造函数,不需要指定类型。
1 2 3
| std::tuple t2(42, 'a', 100L);
|
使用 std::make_tuple 工厂函数,这是最经典的方式,它会自动推导元素类型。
1 2
| auto t1 = std::make_tuple(10, 3.14, std::string("Hello"));
|
make_tuple 会去除传入参数的引用和 const/volatile 限定符:比如std::tuple<int&> 不能通过普通引用 int& 构造,而是会退化为值类型 int。
make_tuple 配合 std::ref:如果想把变量打包成引用,必须使用 std::ref(或 std::cref)。
当使用 std::make_tuple 打包一个 std::pair 时,它会将 pair 展开成两个元素,而不是保留为一个 pair 类型。
1 2 3 4 5 6 7
| std::pair<int, double> p(1, 2.0);
std::tuple t1(p);
auto t2 = std::make_tuple(p);
|
下面是类的成员函数与std::apply的配合使用的实例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include <iostream> #include <tuple> #include <string> #include <functional>
struct Multiplier { int factor; Multiplier(int f) : factor(f) {}
int multiply(int x) { return x * factor; }
void show(int x, int y) { std::cout << "Result: " << (x + y) * factor << std::endl; } };
int main() { Multiplier m(10);
auto args1 = std::make_tuple(&m, 5); int res = std::apply(&Multiplier::multiply, args1); std::cout << "Result: " << res << std::endl;
std::apply(&Multiplier::show, std::make_tuple(&m, 2, 3)); Multiplier m2(5); std::apply(&Multiplier::show, std::make_tuple(std::ref(m2), 10, 10));
return 0; }
|
对于成员函数,std::apply 的逻辑本质上是:invoke(f, get<0>(t), get<1>(t), get<2>(t)...)所以,tuple 的第一个元素必须是对象(或对象指针/引用),后续元素才是函数的参数。这与 std::invoke 是完全一脉相承的。
2.4 返回值处理与 constexpr
2.4.1 返回值推导
std::apply 会完美转发返回值。我们可以用 auto 接收。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <tuple> #include <functional> #include <iostream>
int sum(int a, int b) { return a + b; }
int main() { auto t = std::make_tuple(1, 2); auto result = std::apply(sum, t); static int x = 0; auto get_ref = []() -> int& { return x; }; int& ref = std::apply(get_ref, std::tuple<>{}); ref = 100; std::cout << "x = " << x << std::endl; }
|
这段代码主要展示了 std::apply 的两个特性:
它可以处理空 tuple(即调用无参函数)。
它可以完美转发返回值类型(保留引用)。
get_ref:这是一个 Lambda 表达式,它不接受任何参数,但返回全局变量 x 的引用 int&。
std::tuple<>{} :这是一个空的 tuple。因为它没有元素,std::apply 调用 get_ref 时自然也不传任何参数,相当于调用 get_ref()。
int& ref = ...:
std::apply 执行 get_ref(),返回 x 的引用。
- 关键点在于
std::apply 保留了返回引用这个性质(并没有把它变成 int 副本)。
ref 成为了 x 的引用。
ref = 100;:由于 ref 是引用,这行代码实际上修改了全局变量 x 的值。
std::apply 是通用的,无论函数是有参(搭配非空 tuple)还是无参(搭配空 tuple),也无论返回的是值还是引用,它都能正确处理。
2.4.2 constexpr 和 编译期计算
C++17 标准要求 std::apply 是 constexpr 的。这意味着你可以在编译期就用它!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <tuple>
constexpr int add(int a, int b) { return a + b; }
int main() { constexpr std::tuple<int, int> t = {3, 4}; constexpr int res = std::apply(add, t); static_assert(res == 7, "Math error"); std::cout << res << std::endl;
return 0; }
|