C++ tuple apply | "Application of tuples" guide

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:

Featuresdirect callstd::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 or optional, std::invoke comes to mind first, and if the argument set is a tuple, it is resolved with std::apply. The first argument of apply is a callable object, so the lambda, function object, and binding results are entered as is.
  • Return type: Return type is inferred using decltype(auto) or std::invoke_result_t, and is verified at compile time in the template API whether “if a tuple is inserted, it matches the function signature”.
  • const tuple: If you pass a read-only tuple like std::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

methodWhen to useMemo
Parameter Pack (...)Passing template variable arguments directlyPerfect forward idiom with std::forward
tuple + applyWhen a bundle is determined at runtime, or save/move as a single valueThe number of arguments must be fixed to compile
make_from_tupleWhen matching the contents of the tuple as is to the constructorThe 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 tuple and passing it to the verification function bool validate(T...) with apply.
  • 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 with apply internally to create a bind call).
  • Test Fixture: Representing input cases with std::tuple and calling function under test with apply simplifies 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:

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.


  • 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 |