C++ tuple apply | "Application of tuples" guide
이 글의 핵심
A practical guide to C++ tuple apply.
What is apply?
std::apply is a function introduced in C++17 that **unpacks the elements of a tuple into function arguments. This is useful when passing values stored in a tuple to a function.
#include <tuple>
int add(int a, int b, int c) {
return a + b + c;
}
std::tuple<int, int, int> args{1, 2, 3};
// apply: unpack tuple
int result = std::apply(add, args); // add(1, 2, 3)
Why do you need it?:
- Tuple Unpack: Convert tuples to function arguments
- Lazy call: Save arguments in advance and call later
- Metaprogramming: Variable argument handling
- Simplicity: No need for index-based access
// ❌ Index-based: hassle
std::tuple<int, int, int> args{1, 2, 3};
int result = add(std::get<0>(args), std::get<1>(args), std::get<2>(args));
// ✅ apply: concise
int result = std::apply(add, args);
How apply works:
// Conceptual Implementation
template<typename Func, typename Tuple, size_t... Indices>
auto apply_impl(Func&& func, Tuple&& tuple, std::index_sequence<Indices...>) {
return func(std::get<Indices>(std::forward<Tuple>(tuple))...);
}
template<typename Func, typename Tuple>
auto apply(Func&& func, Tuple&& tuple) {
return apply_impl(
std::forward<Func>(func),
std::forward<Tuple>(tuple),
std::make_index_sequence<std::tuple_size_v<std::decay_t<Tuple>>>{}
);
}
apply vs direct call:
| Features | direct call | std::apply |
|---|---|---|
| save argument | ❌ Not possible | ✅ Available |
| delayed call | ❌ Not possible | ✅ Available |
| variable arguments | ❌ Difficulty | ✅ Easy |
| Performance | ✅ Fast | ✅ Inline Capable |
// direct call
int result1 = add(1, 2, 3);
// apply: Called after storing arguments
auto args = std::make_tuple(1, 2, 3);
int result2 = std::apply(add, args);
Default use
#include <tuple>
void print(int x, double y, const std::string& z) {
std::cout << x << ", " << y << ", " << z << std::endl;
}
int main() {
auto args = std::make_tuple(42, 3.14, "hello");
std::apply(print, args); // print(42, 3.14, "hello")
}
Practical example
Example 1: Lambda
#include <tuple>
int main() {
auto args = std::make_tuple(10, 20, 30);
auto result = std::apply([](int a, int b, int c) {
return a + b + c;
}, args);
std::cout << "sum: " << result << std::endl; // 60
}
Example 2: Constructor
#include <tuple>
#include <memory>
struct Widget {
int x;
double y;
std::string z;
Widget(int x, double y, std::string z)
: x(x), y(y), z(std::move(z)) {}
};
int main() {
auto args = std::make_tuple(42, 3.14, std::string{"hello"});
// After passing constructor arguments to apply, make_unique
auto widget = std::apply([](int x, double y, std::string z) {
return std::make_unique<Widget>(x, y, std::move(z));
}, args);
std::cout << widget->x << ", " << widget->y << ", " << widget->z << std::endl;
}
Example 3: Function wrapper
#include <tuple>
#include <functional>
template<typename Func, typename... Args>
class DelayedCall {
Func func;
std::tuple<Args...> args;
public:
DelayedCall(Func f, Args... a)
: func(f), args(std::forward<Args>(a)...) {}
auto execute() {
return std::apply(func, args);
}
};
int main() {
auto delayed = DelayedCall{
[](int a, int b) { return a + b; },
10, 20
};
std::cout << "Result: " << delayed.execute() << std::endl; // 30
}
Example 4: Variable arguments
#include <tuple>
template<typename... Args>
void logArgs(Args&&... args) {
auto tuple = std::make_tuple(std::forward<Args>(args)...);
std::apply([](const auto&... values) {
((std::cout << values << " "), ...);
std::cout << std::endl;
}, tuple);
}
int main() {
logArgs(1, 2.5, "hello", true);
// 1 2.5 hello 1
}
make_from_tuple
#include <tuple>
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
int main() {
auto args = std::make_tuple(10, 20);
// make_from_tuple: Constructor call
auto point = std::make_from_tuple<Point>(args);
std::cout << point.x << ", " << point.y << std::endl;
}
Frequently occurring problems
Issue 1: References
int x = 42;
auto args = std::make_tuple(x); // copy
// ✅ See also
auto args2 = std::forward_as_tuple(x); // reference
std::apply([](int& val) {
val = 100;
}, args2);
std::cout << x << std::endl; // 100
Problem 2: Number of arguments
void func(int a, int b) {
std::cout << a + b << std::endl;
}
// ❌ Number of arguments mismatch
// auto args = std::make_tuple(1, 2, 3);
// std::apply(func, args); // error
// ✅ Accurate count
auto args = std::make_tuple(1, 2);
std::apply(func, args);
Problem 3: Type inference
// auto: complex type
auto t = std::make_tuple(42, 3.14);
// explicit type
std::tuple<int, double> t2{42, 3.14};
Issue 4: Performance
// apply can be inline
// Minimize overhead
// But the cost of creating a tuple is
auto args = std::make_tuple(1, 2, 3); // copy
std::apply(func, args);
// ✅ Direct calls (if available)
func(1, 2, 3);
Utilization pattern
// 1. Return multiple values
std::tuple<int, std::string> parse();
// 2. Save function arguments
auto args = std::make_tuple(1, 2, 3);
std::apply(func, args);
// 3. Constructor call
auto obj = std::make_from_tuple<T>(args);
// 4. Variable argument handling
template<typename... Args>
void process(Args&&... args);
Practice pattern
Pattern 1: Asynchronous operations
#include <tuple>
#include <future>
template<typename Func, typename... Args>
auto asyncApply(Func&& func, std::tuple<Args...> args) {
return std::async(std::launch::async, [func = std::forward<Func>(func), args = std::move(args)]() {
return std::apply(func, args);
});
}
// use
int compute(int a, int b, int c) {
return a * b + c;
}
auto args = std::make_tuple(10, 20, 5);
auto future = asyncApply(compute, args);
std::cout << "Result: " << future.get() << '\n'; // 205
Pattern 2: Function Cache
When using a set of arguments as keys, you can uniformly call a variable argument function with std::tuple + std::apply.
#include <map>
#include <tuple>
#include <functional>
template<typename Func>
class MemoizedBinary {
Func func_;
std::map<std::pair<int, int>, int> cache_;
public:
explicit MemoizedBinary(Func f) : func_(std::move(f)) {}
int operator()(int a, int b) {
auto key = std::make_pair(a, b);
if (auto it = cache_.find(key); it != cache_.end()) {
return it->second;
}
auto tup = std::make_tuple(a, b);
int result = std::apply(func_, tup);
cache_.emplace(key, result);
return result;
}
};
// use
auto add_cached = MemoizedBinary([](int a, int b) { return a + b; });
Pattern 3: Batch processing
template<typename Func>
class BatchProcessor {
Func func_;
std::vector<std::tuple<int, int>> batch_;
public:
BatchProcessor(Func func) : func_(func) {}
void add(int a, int b) {
batch_.emplace_back(a, b);
}
void process() {
for (auto& args : batch_) {
auto result = std::apply(func_, args);
std::cout << "Result: " << result << '\n';
}
batch_.clear();
}
};
// use
BatchProcessor processor([](int a, int b) {
return a + b;
});
processor.add(1, 2);
processor.add(3, 4);
processor.process();
// Result: 3
// Result: 7
Advanced use of std::apply
- Combination with
std::invoke: When calling a function contained in a member pointer oroptional,std::invokecomes to mind first, and if the argument set is a tuple, it is resolved withstd::apply. The first argument ofapplyis a callable object, so the lambda, function object, and binding results are entered as is. - Return type: Return type is inferred using
decltype(auto)orstd::invoke_result_t, and is verified at compile time in the template API whether “if a tuple is inserted, it matches the function signature”. consttuple: If you pass a read-only tuple likestd::apply(f, std::as_const(t)), it becomes clear whether the element is modifiable when it is a reference.
Unpack function arguments: tuple vs parameter pack
| method | When to use | Memo |
|---|---|---|
Parameter Pack (...) | Passing template variable arguments directly | Perfect forward idiom with std::forward |
tuple + apply | When a bundle is determined at runtime, or save/move as a single value | The number of arguments must be fixed to compile |
make_from_tuple | When matching the contents of the tuple as is to the constructor | The explicit constructor can also be called |
If you already have (args...) as a variable argument template, there is no need to make it a tuple, and tuple + apply shines when you need to “call it all at once” like lazy execution/queuing/serialization.
Reinforcement of actual patterns
- Settings/CLI parsing: It is easy to manage the argument order in one place by tying the key-value into a
tupleand passing it to the verification functionbool validate(T...)withapply. - SQL Binding·RPC Stub: If the column/field type is fixed to a tuple, you can wrap the procedure call with
apply(in reality, the DB API does not support tuples, so unpack it withapplyinternally to create abindcall). - Test Fixture: Representing input cases with
std::tupleand calling function under test withapplysimplifies data-driven testing.
Connection with metaprogramming
Implementations of std::apply typically use the access of std::index_sequence** and std::get<I>. That is, for a tuple whose length is determined at compile time, an expansion call is made without runtime overhead.
// Concept: std::get<I>(t)... for 0..N-1 as index_sequence.
template<class F, class 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))...);
}
When used with C++23 std::bind_front / std::invoke_r, etc., it is easy to organize the pattern of passing only the remaining arguments to a partially applied function as a tuple. From a metaprogramming perspective, it is useful to remember apply as “the glue that converts tuple types into function signatures”.
FAQ
Q1: What is a tuple?
A: A container that groups multiple values** in C++11. You can save different types.
std::tuple<int, double, std::string> t{42, 3.14, "hello"};
auto [x, y, z] = t; // C++17 structured binding
Q2: What is apply?
A: A function that unpacks tuples into function arguments in C++17.
int add(int a, int b) { return a + b; }
auto args = std::make_tuple(2, 3);
int result = std::apply(add, args); // add(2, 3)
Q3: How to unpack tuples?
A:
- structured binding (C++17):
auto [x, y, z] = tuple; - tie:
std::tie(x, y, z) = tuple; - apply:
std::apply(func, tuple);
std::tuple<int, double, std::string> t{42, 3.14, "hello"};
// structured binding
auto [x, y, z] = t;
// tie
int a;
double b;
std::string c;
std::tie(a, b, c) = t;
// apply
std::apply([](int x, double y, const std::string& z) {
std::cout << x << ", " << y << ", " << z << '\n';
}, t);
Q4: How do I store references?
A: Use std::forward_as_tuple.
int x = 42;
// make_tuple: copy
auto t1 = std::make_tuple(x);
// forward_as_tuple: see
auto t2 = std::forward_as_tuple(x);
std::apply([](int& val) {
val = 100;
}, t2);
std::cout << x << '\n'; // 100
Q5: What is the performance of apply?
A: Inlineable, so overhead is minimal.
// Compiler optimizes inline
std::apply(add, std::make_tuple(1, 2, 3));
// → add(1, 2, 3) (same as direct call)
Q6: What is make_from_tuple?
A: Creates an object using **tuples as constructor arguments.
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
auto args = std::make_tuple(10, 20);
auto point = std::make_from_tuple<Point>(args); // Point(10, 20)
Q7: How do you handle empty tuples?
A: Empty tuples are also possible. Used for functions without arguments.
void func() {
std::cout << "No arguments\n";
}
std::tuple<> empty;
std::apply(func, empty); // func()
Q8: What are tuple apply learning resources?
A:
- “Effective Modern C++” by Scott Meyers
- “C++17 The Complete Guide” by Nicolai Josuttis
- cppreference.com - std::apply
Related posts: tuple, structured-binding, variadic-templates.
One-line summary: std::apply is a C++17 function that unpacks the elements of a tuple into function arguments.
Good article to read together (internal link)
Here’s another article related to this topic.
- C++ invoke and apply | “Function Invoke” utility guide
- C++ Structured Binding | “Structured Binding” C++17 Guide
- C++ Copy Elision | “Copy Elision” Guide
Practical tips
These are tips that can be applied right away in practice.
Debugging tips
- If you run into a problem, check the compiler warnings first.
- Reproduce the problem with a simple test case
Performance Tips
- Don’t optimize without profiling
- Set measurable indicators first
Code review tips
- Check in advance for areas that are frequently pointed out in code reviews.
- Follow your team’s coding conventions
Practical checklist
This is what you need to check when applying this concept in practice.
Before writing code
- Is this technique the best way to solve the current problem?
- Can team members understand and maintain this code?
- Does it meet the performance requirements?
Writing code
- Have you resolved all compiler warnings?
- Have you considered edge cases?
- Is error handling appropriate?
When reviewing code
- Is the intent of the code clear?
- Are there enough test cases?
- Is it documented?
Use this checklist to reduce mistakes and improve code quality.
Keywords covered in this article (related search terms)
This article will be helpful if you search for C++, tuple, apply, unpack, C++17, etc.
Related articles
- C++ invoke and apply |
- C++ any |
- Modern C++ (C++11~C++20) Core Grammar Cheat Sheet | A glance at frequently used items in the workplace
- C++ CTAD |
- C++ string vs string_view |