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.

Featuresdirect callstd::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 apply uses std::invoke, std::apply(&Widget::resize, std::tuple{&w, 10, 20}) calls w.resize(10, 20). The object (or pointer) is simply the first tuple element.
  • Return types: apply returns decltype(auto), so it preserves references. std::apply([](auto& a, auto&) -> auto& { return a; }, t) returns a reference into t, which dangles if t was a temporary.
  • const tuples: std::apply(f, std::as_const(t)) makes every element arrive as const, which documents at the call site that f cannot modify the stored values.

Tuple + apply vs a parameter pack

methodWhen to useNote
Parameter pack (Args&&... args)Arguments are passed straight through in the same callPerfect forwarding with std::forward
tuple + applyArguments must be stored, queued, or passed around as one valueTuple size and types are still fixed at compile time
make_from_tupleThe stored arguments feed a constructorUses 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:

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.