Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A small tuple can be built by storing the first type in a parameter pack and recursively storing the remaining types. A forwarding constructor preserves whether each value passed to it is an lvalue or rvalue, while indexed get can walk the nested storage at compile time. The implementation below uses C++17 for access; the underlying variadic-template representation works in C++11.

What variadic templates contribute

A variadic template has at least one parameter pack: a template parameter that can contain zero or more arguments. A pack expansion applies a pattern to each argument in the pack. Variadic templates became part of C++ in C++11; cppreference records the feature-test macro __cpp_variadic_templates as 200704L.

A tuple is a fixed-size collection of heterogeneous values: unlike an array, its elements need not share one type. The standard std::tuple can also be empty. Its interface includes indexed access through get and type information through tuple_size and tuple_element.

Represent the tuple as recursive storage

For a teaching implementation, give the empty pack its own base case. For a nonempty pack, store the first type as head and store the rest in another tuple. Each recursive step consumes one type, so the chain ends at simple_tuple<>.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#include <cstddef>
#include <string>
#include <type_traits>
#include <utility>

template<class... Ts>
struct simple_tuple;

template<>
struct simple_tuple<> {};

template<class Head, class... Tail>
struct simple_tuple<Head, Tail...> {
    Head head;
    simple_tuple<Tail...> tail;

    template<class H, class... Us>
    explicit simple_tuple(H&& h, Us&&... us)
        : head(std::forward<H>(h)),
          tail(std::forward<Us>(us)...) {}
};

The constructor’s H&& and Us&&... are forwarding references because their types are deduced. std::forward passes each argument onward with its original value category: an lvalue stays an lvalue, while an rvalue stays an rvalue. Each recursive tuple constructor receives the remaining arguments through the expansion std::forward<Us>(us).... With no remaining types or arguments, construction reaches the empty tuple.

For example, simple_tuple<int, std::string> values(7, "templates") stores an integer followed by a string. The string can be constructed from the string literal because the constructor forwards that argument to the std::string member.

Write indexed access with compile-time recursion

For index zero, get returns the current node’s head. For a larger index, it recurses into the tail with the index reduced by one. In C++17, if constexpr discards the branch that does not apply during instantiation, and decltype(auto) preserves the reference category of the returned expression.

template<std::size_t I, class Head, class... Tail>
decltype(auto) get(simple_tuple<Head, Tail...>& t) {
    if constexpr (I == 0)
        return (t.head);
    else
        return get<I - 1>(t.tail);
}

template<std::size_t I, class Head, class... Tail>
decltype(auto) get(const simple_tuple<Head, Tail...>& t) {
    if constexpr (I == 0)
        return (t.head);
    else
        return get<I - 1>(t.tail);
}

template<std::size_t I, class Head, class... Tail>
decltype(auto) get(simple_tuple<Head, Tail...>&& t) {
    if constexpr (I == 0)
        return std::move(t.head);
    else
        return get<I - 1>(std::move(t.tail));
}

template<std::size_t I, class Head, class... Tail>
decltype(auto) get(const simple_tuple<Head, Tail...>&& t) {
    if constexpr (I == 0)
        return std::move(t.head);
    else
        return get<I - 1>(std::move(t.tail));
}

Parentheses around t.head matter: with decltype(auto), (t.head) deduces a reference, while an unparenthesized member access has different decltype rules. The overloads return a mutable lvalue reference from a mutable lvalue tuple, a const lvalue reference from a const lvalue tuple, and rvalue references from rvalue tuples. Moving a const tuple produces a const rvalue reference, which generally cannot be used to modify or move from a member.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the index form to access repeated types as well as distinct ones. An index outside the tuple’s range fails during compilation because recursion eventually reaches the empty tuple, which has no matching get overload.

int main() {
    simple_tuple<int, std::string> values(7, "templates");
    get<0>(values) = 9;

    const auto& read_only = values;
    static_assert(std::is_same<
        decltype(get<0>(read_only)), const int&>::value,
        "const lvalue access returns a const reference");

    static_assert(std::is_same<
        decltype(get<1>(std::move(values))), std::string&&>::value,
        "rvalue access returns an rvalue reference");
}

Adapt the access technique to earlier standards

C++11 and C++14

The recursive representation and forwarding constructor use C++11 variadic templates, rvalue references, and std::forward. The access listing above additionally uses if constexpr, which is a C++17 feature; decltype(auto) is available starting in C++14. In C++11, implement indexed access with helper classes specialized for index zero and the recursive case, and use trailing return types or explicit reference return types to preserve the result. In C++14, decltype(auto) is available, but the branch still needs a helper or overload specialization because if constexpr is not.

C++17

if constexpr makes the recursive get easy to read because only the selected branch is instantiated. Fold expressions also replace many recursive functions that consume every value in a pack, such as applying an operation to each element. They are useful for pack-wide operations, but they do not by themselves provide indexed storage or indexed access.

C++26

C++26 adds pack indexing, which can select a pack element directly instead of peeling a type pack recursively in suitable template code. cppreference lists __cpp_pack_indexing as 202311L. Compiler support depends on the implementation and its C++26 language-mode status; check the compiler’s documentation and feature-test macro before relying on it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

What this tuple leaves out

This example demonstrates the mechanics, not the full behavior of std::tuple. It does not implement allocator propagation, empty-base optimization, constrained constructors, exception specifications, or the complete standard tuple interface. Its recursive composition also creates a nested type structure, and indexed access recursively instantiates the path to the requested element.

  • Type-based access: this example only implements index-based get. A type-based accessor needs rules for repeated types; standard tuple type-based access requires the requested type to occur exactly once.
  • Tuple traits and structured bindings: interoperability requires appropriate tuple_size and tuple_element traits and a discoverable get overload. Merely naming a function get does not make a custom type equivalent to std::tuple.
  • Alternative storage: indexed-leaf layouts can give each element a separately indexed base and are often used to enable optimizations such as empty-base optimization. They are more involved than the recursive member layout shown here.
  • Pack-depth considerations: recursive storage and access are clear for learning, but large packs can mean deeper template instantiation. A different representation can be preferable when compile-time depth or object layout is a design concern.

When to use this design

Use recursive composition when the goal is to understand how a parameter pack can describe a heterogeneous sequence, how an empty pack provides the base case, and how forwarding preserves argument categories. For application code, prefer std::tuple unless a custom representation or template exercise gives you a concrete reason to build another one. The standard tuple already supplies the broader access and interoperability vocabulary described by cppreference.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.