std::apply and make_from_tuple in C++17: Calling Functions with Tuple Arguments
Key takeaways
std::apply calls a function with the elements of a tuple as its arguments, and std::make_from_tuple does the same for a constructor. The post shows how they work (index_sequence plus std::invoke), where they are useful (deferred calls, argument queues, test tables), and the errors people hit: overloaded functions, dangling references from forward_as_tuple, and argument-count mismatches.
What is apply?
std::apply (C++17, <tuple>) calls a function with the elements of a tuple as its arguments: std::apply(f, std::tuple{1, 2, 3}) is f(1, 2, 3). It is the bridge between “a group of values stored as one object” and “a function that takes those values as separate parameters”.
#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)
The problem it solves is that a tuple’s elements can only be reached with a compile-time index, std::get<0>(t), so writing the call by hand requires knowing the tuple’s length at the call site. That is easy for three fixed arguments and impossible inside a generic template that stores “whatever arguments the user passed”:
// ❌ Index-based: works only when you know the length when writing the code
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: works for any tuple length
int result = std::apply(add, args);
Typical uses are deferred calls (store the arguments now, call later), thread and task queues, test tables where each row is a tuple of inputs, and generic wrappers that need to forward a stored argument list.
How apply works
The standard library implementation is short enough to read. It generates the indices 0, 1, ..., N-1 as a template parameter pack with std::make_index_sequence, then expands std::get<I>(tuple)... into the argument list:
// 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>>>{}
);
}
Everything happens at compile time: the tuple’s size is part of its type, so the expansion produces exactly the direct call func(std::get<0>(t), std::get<1>(t), std::get<2>(t)), which the optimizer inlines. The real implementation differs in two ways that matter. It calls through std::invoke, so member function pointers work, and it returns decltype(auto), so a function returning a reference still returns a reference through apply; the simplified auto above would return a copy.
Because it uses std::tuple_size and std::get, apply also works with std::pair and std::array, not just std::tuple.
| Features | direct call | std::apply |
|---|---|---|
| save arguments for later | ❌ Not possible | ✅ Store the tuple |
| deferred call | ❌ Not possible | ✅ Call when needed |
| arbitrary argument count in generic code | ❌ Difficult | ✅ Easy |
| Performance | ✅ Direct | ✅ Inlined to the same call |
// 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);
Unpacking a tuple into a call
#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")
}
Note the type of the third element: std::make_tuple(42, 3.14, "hello") produces std::tuple<int, double, const char*>, not a tuple holding a std::string. The call still works because const char* converts to const std::string& at the call, but it means a temporary string is built every time, and code that inspects the tuple’s types sees a pointer. Write std::string{"hello"} or use "hello"s when the element should really be a string.
Lambdas, constructors, deferred calls, and variadic logging
Applying a tuple to a 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
}
Constructing an object from a tuple
#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"});
// Unpack the tuple into make_unique's arguments
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;
}
The lambda takes std::string z by value, so apply copies the string out of args, which stays intact. To move it instead, pass std::move(args): apply forwards the tuple’s value category to std::get, so each element arrives as an rvalue. After that, args holds a moved-from string, the same trade-off as moving any other object.
A wrapper that stores a call for later
#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
}
This is the core of what std::thread, std::async and task queues do internally: capture a callable and its arguments now, invoke later. Two design decisions are hidden here. The class stores copies (std::tuple<Args...> with the deduced, decayed types), which is the safe default, because references captured for a later call are the classic source of dangling bugs. And execute() passes args as an lvalue, so it can be called repeatedly; a one-shot task would std::apply(std::move(func), std::move(args)) to move expensive arguments into the call.
Printing a parameter pack through a tuple
#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
}
Here the tuple is not really needed: logArgs already has the pack, and ((std::cout << args << " "), ...) would print it directly. The tuple-plus-generic-lambda form becomes useful when the pack has to be stored first (a log record queued for a background thread) or when the values arrive as a tuple from somewhere else, such as a function returning several values.
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;
}
std::make_from_tuple<T>(t) is T(std::get<I>(t)...), a direct-initialization, so it can call explicit constructors. It uses parentheses, not braces, which matters for aggregates: before C++20, a struct with no constructor cannot be created this way at all, because T(a, b) does not perform aggregate initialization; C++20 allows parenthesized aggregate initialization and the call then compiles. It also cannot create objects in place for you; for emplace-style construction into a container, apply a lambda that calls emplace_back instead.
References, arity mismatches, overload sets, and copies
Tuples that copy when you expected references
int x = 42;
auto args = std::make_tuple(x); // copy
// ✅ Store a reference instead
auto args2 = std::forward_as_tuple(x); // std::tuple<int&>
std::apply([](int& val) {
val = 100;
}, args2);
std::cout << x << std::endl; // 100
std::make_tuple always decays and copies (unless you wrap an argument in std::ref), while std::forward_as_tuple and std::tie store references. The danger is in the other direction: std::forward_as_tuple(compute()) stores an rvalue reference to a temporary that is destroyed at the end of the statement, so keeping that tuple in a variable and applying it later reads a dead object. The code compiles, usually “works” in debug builds, and fails unpredictably with optimization. I treat forward_as_tuple as something that only appears inside a single function call expression, never as the initializer of a named variable.
The wrong number of elements
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);
The error for a mismatch does not say “wrong number of elements”. It is reported deep inside the library, as something like no matching function for call to '__invoke(void (*&)(int, int), int&, int&, int&)' from libstdc++, followed by notes about std::invoke candidates. The argument list in that message (int&, int&, int&) is what the tuple expanded into; compare it with the function’s parameters to find the mismatch.
Passing an overloaded function
void show(int);
void show(double);
auto t = std::make_tuple(42);
// std::apply(show, t); // error: couldn't deduce template parameter
std::apply([](auto&&... a) { show(std::forward<decltype(a)>(a)...); }, t); // OK
std::apply takes the callable as a template parameter, and an overload set has no single type to deduce, so the call fails before overload resolution can look at the tuple. The same applies to function templates (std::apply(std::max<int>, t) fails too, because standard library functions may have extra overloads). Wrapping the call in a generic lambda moves overload resolution to where the arguments are known.
What apply actually costs
// apply itself is inlined away
// But building the tuple can copy
auto args = std::make_tuple(1, 2, 3); // copy
std::apply(func, args);
// ✅ Direct calls (if available)
func(1, 2, 3);
std::apply adds no runtime cost of its own; the cost is in creating the tuple. For integers that is nothing, but a tuple of strings or vectors built only to be unpacked on the next line copies them for no reason. Use apply when the arguments are already in a tuple or need to be stored; when you have the values at hand, call the function directly.
Async calls, memoization, and batch processing
Running a stored call on another thread
#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
The tuple is taken by value and moved into the lambda’s capture, so the worker thread owns its own copy of the arguments. That is deliberate: if the tuple held references to the caller’s locals, the caller could return before the thread reads them.
A cache keyed by argument tuples
When a set of arguments is used as a key, std::tuple + std::apply lets the same code call the function and index the cache.
#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; });
With a fixed two-argument signature, apply is not buying much here; the pattern pays off when the class is generalized to template<typename R, typename... Args> with a std::map<std::tuple<Args...>, R> cache, since std::tuple already provides the operator< that std::map needs.
Batch processing and table-driven tests
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
The same shape works well for data-driven tests: a std::vector<std::tuple<Input1, Input2, Expected>> of cases and a loop that applies the function under test to the input part of each row.
Member functions, return types, and other details
- Member functions: because
applyusesstd::invoke,std::apply(&Widget::resize, std::tuple{&w, 10, 20})callsw.resize(10, 20). The object (or pointer) is simply the first tuple element. - Return types:
applyreturnsdecltype(auto), so it preserves references.std::apply([](auto& a, auto&) -> auto& { return a; }, t)returns a reference intot, which dangles iftwas a temporary. consttuples:std::apply(f, std::as_const(t))makes every element arrive asconst, which documents at the call site thatfcannot modify the stored values.
Tuple + apply vs a parameter pack
| method | When to use | Note |
|---|---|---|
Parameter pack (Args&&... args) | Arguments are passed straight through in the same call | Perfect forwarding with std::forward |
tuple + apply | Arguments must be stored, queued, or passed around as one value | Tuple size and types are still fixed at compile time |
| make_from_tuple | The stored arguments feed a constructor | Uses T(args...), so explicit constructors work |
If you already have (args...) as a variadic template parameter, there is no need to build a tuple; tuple + apply shines when the call has to happen somewhere else or later: deferred execution, queuing, serialization. A tuple does not make the argument list dynamic, though. Its length and element types are part of its type, so “an argument list decided at runtime” (for example, a scripting binding) still needs something like a std::vector<std::any> and per-signature dispatch code.
Connection with metaprogramming
Implementations of std::apply are built on std::index_sequence and std::get<I>. For a tuple whose length is known at compile time, the expansion is a plain call with no 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))...);
}
The same index_sequence trick is how you write your own tuple algorithms, such as calling a function on every element or transforming a tuple into another tuple. C++20’s std::bind_front covers the related case of fixing the first few arguments of a function, and a lambda with an init-capture is often clearer than either.
FAQ
Q1: What is a tuple?
A: A fixed-size group of values of possibly different types, added in C++11.
std::tuple<int, double, std::string> t{42, 3.14, "hello"};
auto [x, y, z] = t; // C++17 structured binding
Q2: How do I unpack a tuple?
A:
- structured binding (C++17):
auto [x, y, z] = tuple;to get named variables - tie:
std::tie(x, y, z) = tuple;to assign into existing variables - apply:
std::apply(func, tuple);to pass the elements to a function
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);
Q3: How do I store references?
A: Use std::tie for lvalues you own, or std::forward_as_tuple for pass-through, and make sure the referenced objects outlive the tuple.
int x = 42;
// make_tuple: copy
auto t1 = std::make_tuple(x);
// forward_as_tuple: reference
auto t2 = std::forward_as_tuple(x);
std::apply([](int& val) {
val = 100;
}, t2);
std::cout << x << '\n'; // 100
Q4: How do you handle empty tuples?
A: std::apply(f, std::tuple<>{}) calls f() with no arguments, which is what makes generic code work for zero-argument callables.
void func() {
std::cout << "No arguments\n";
}
std::tuple<> empty;
std::apply(func, empty); // func()
Q5: Where can I read more?
A:
- “C++17 The Complete Guide” by Nicolai Josuttis
- cppreference.com - std::apply
- cppreference.com - std::make_from_tuple
std::apply turns a stored tuple into a function call, std::make_from_tuple turns it into a constructor call, and both cost nothing beyond building the tuple.
Related Articles
- std::tuple, tie and structured bindings
- Variadic templates
- Perfect forwarding
- C++ Structured Bindings
- C++ Function Pointer
- C++ CTAD