{{announcement.body}}
{{announcement.title}}

Variadic Template C++: Implementing Unsophisticated Tuple

DZone 's Guide to

Variadic Template C++: Implementing Unsophisticated Tuple

So, in this article, I will explain the variadic template in C++ with the help of an unsophisticated tuple implementation.

· Web Dev Zone ·
Free Resource

From C++11, std::tuple is an incredible expansion to Modern C++ that offers a fixed-size col­lec­tion of het­ero­ge­neous values. Un­for­tu­nately, tu­ples can be somewhat dubious to manage in a conventional fash­ion. But, subsequently released C++ stan­dard in­tro­duced a few fea­tures and helpers that greatly re­duce the nec­es­sary boil­er­plate. 

So, in this article, I will explain the variadic template in C++ with the help of unsophisticated tuple implementation. I'll also walk you through a tricky part of tuple i.e. loop through tuple element. Because I have covered the variadic template in my prior article i.e. C++ Template: A Quick UpToDate Look, my focus here would be a blend of variadic template and tuple implementation with more up to date C++ gauges.

 Motivation

It is often useful to define class/struct/union/function that accepts a variable number and type of arguments.

If you have already used C, you'll know that printf function can accept any number of arguments. Such functions are entirely implemented through macros or ellipses operator. And because of that it has several disadvantages, like type-safety, cannot accept references as arguments, etc.

Variadic Class Template: Implementing Tuple Class

So, let's build our own ADT in the same way as std::tuple with the help of variadic template.

The variadic template in C++ usually starts with the general (empty) definition that also serves as the base-case for template recursion termination in the later specialization:

C++
 




xxxxxxxxxx
1


 
1
template <typename... T>
2
struct Tuple { };



This already allows us to define an empty structure i.e. Tuple<> object;, albeit that isn't very useful yet. Next comes the recursive case specialization:

C++
 




xxxxxxxxxx
1
16


 
1
template<
2
            typename T, 
3
            typename... Rest    // Template parameter pack
4
        >
5
struct Tuple<T, Rest...> {      // Class parameter pack
6
    T first;
7
    Tuple<Rest...> rest;        // Parameter pack expansion
8
 
           
9
    Tuple(const T& f, const Rest& ... r)
10
        : first(f)
11
        , rest(r...) {
12
    }
13
};
14
 
           
15
Tuple<bool>                 t1(false);           // Case 1
16
Tuple<int, char, string>    t2(1, 'a', "ABC");   // Case 2



How Does Variadic Class Template Works?

To understand variadic class template, consider the two use case above i.e. Tuple<int, char, string> t2(1, 'a', "ABC");

  •  The declaration first matches against the specialization, yielding a structure with int first; and Tuple<char, string> rest; data members.
  •  The rest definition again matches with specialization, yielding a structure with char first; and Tuple<string> rest; data members.
  •  The rest definition again matches this specialization, creating its own string first; and Tuple<> rest; members.
  •  Finally, this last rest matches against the base-case definition, producing an empty structure.

You can visualize this as follows:

C++
 




xxxxxxxxxx
1


 
1
Tuple<int, char, string>
2
-> int first
3
-> Tuple<char, string> rest
4
    -> char first
5
    -> Tuple<string> rest
6
        -> string first
7
        -> Tuple<> rest
8
            -> (empty)



Variadic Function Template: Implementing get<>() Function for Tuple Class

So far, we have designed a data structure with a variable number and type of data members. But still, it isn't useful, as there is no mechanism to retrieve data from our Tuple class. So let's design one:

C++
 




xxxxxxxxxx
1


 
1
template<
2
            size_t idx, 
3
            template <typename...> class Tuple, 
4
            typename... Args
5
        >
6
auto get(Tuple<Args...> &t) {
7
    return GetHelper<idx, Tuple<Args...>>::get(t);
8
}



As you can see, this get function is templatized on the idx. So, usage can be like get<1>(t), similar to std::tuple. Though, the actual work is done by a static function in a helper class i.e. GetHelper.

Note also the use of a C++14-style auto return type that makes our lives significantly simpler, as otherwise, we would need quite a complicated expression for the return type.

So, on to the helper class. This time, we will need an empty forward declaration and two specializations. First the empty declaration:

C++
 




xxxxxxxxxx
1


 
1
template<
2
            size_t idx, 
3
            typename T
4
        >
5
struct GetHelper;



Now, the base-case (when idx==0). In this specialization, we just return the first member:

C++
 




xxxxxxxxxx
1


 
1
template<
2
            typename T, 
3
            typename... Rest
4
        >
5
struct GetHelper<0, Tuple<T, Rest...>> {
6
    static T get(Tuple<T, Rest...> &data) {
7
        return data.first;
8
    }
9
};



In the recursive case, we decrement idx and invoke the GetHelper for the rest member:

C++
 




xxxxxxxxxx
1
10


 
1
template<
2
            size_t idx, 
3
            typename T, 
4
            typename... Rest
5
        >
6
struct GetHelper<idx, Tuple<T, Rest...>> {
7
    static auto get(Tuple<T, Rest...> &data) {
8
        return GetHelper<idx - 1, Tuple<Rest...>>::get(data.rest);
9
    }
10
};



To work through an example, suppose we have Tuple data and we need get<1>(data). This invokes GetHelper<1, Tuple<T, Rest...>>>::get(data) (the 2nd specialization).

This in turn invokes GetHelper<0, Tuple<T, Rest...>>>::get(data.rest). Finally, it returns (by the 1st specialization as now idx is 0) data.rest.first.

So that's it! Here is the whole functioning code, with some example use in the main function:

C++
 




xxxxxxxxxx
1
68


 
1
// Forward Declaration & Base Case -----------------------------------------
2
template<
3
            size_t idx,
4
            typename T
5
        >
6
struct GetHelper { };
7
 
           
8
template <typename... T>
9
struct Tuple { };
10
// -------------------------------------------------------------------------
11
 
           
12
// GetHelper ---------------------------------------------------------------
13
template<
14
            typename T,
15
            typename... Rest
16
        >
17
struct GetHelper<0, Tuple<T, Rest...>> { // Specialization for index 0
18
    static T get(Tuple<T, Rest...> &data) {
19
        return data.first;
20
    }
21
};
22
 
           
23
template<
24
            size_t idx,
25
            typename T,
26
            typename... Rest
27
        >
28
struct GetHelper<idx, Tuple<T, Rest...>> { // GetHelper Implementation
29
    static auto get(Tuple<T, Rest...> &data) {
30
        return GetHelper<idx - 1, Tuple<Rest...>>::get(data.rest);
31
    }
32
};
33
// -------------------------------------------------------------------------
34
 
           
35
// Tuple Implementation ----------------------------------------------------
36
template<
37
            typename T,
38
            typename... Rest
39
        >
40
struct Tuple<T, Rest...> {
41
    T                   first;
42
    Tuple<Rest...>      rest;
43
 
           
44
    Tuple(const T &f, const Rest &... r)
45
        : first(f)
46
        , rest(r...) {
47
    }
48
};
49
// -------------------------------------------------------------------------
50
 
           
51
 
           
52
// get Implementation ------------------------------------------------------
53
template<
54
            size_t idx, 
55
            template <typename...> class Tuple, 
56
            typename... Args
57
        >
58
auto get(Tuple<Args...> &t) {
59
    return GetHelper<idx, Tuple<Args...>>::get(t);
60
}
61
// -------------------------------------------------------------------------
62
 
           
63
 
           
64
int main() {
65
    Tuple<int, char, string> t(500, 'a', "ABC");
66
    cout << get<1>(t) << endl;
67
    return 0;
68
}



Variadic Template vs Fold Expression

There is two way to process C++ parameter pack i.e.
  1.  Recursion
  2.  Fold Expression(From C++17)
At whatever point conceivable, we should process a parameter pack with fold expression instead of using recursion. Because it has some benefits as:
  • Less code to write
  • Faster code (without optimizations), as you just have a single expression instead of multiple function calls
  • Faster to compile, as you deal with fewer template instantiation

Processing a Parameter Pack With Recursion

As we have seen earlier, the variadic template starts with empty definition i.e. base case for recursion.

C++
 




xxxxxxxxxx
1


 
1
void  print()  {}



Then the recursive case specialization:

C++


This is now sufficient for us to use the print function with variable number and type of arguments. For example:

C++
 




xxxxxxxxxx
1


 
1
print(500, 'a', "ABC");



Processing a Parameter Pack With Fold Expression

C++
 




xxxxxxxxxx
1


 
1
template<   
2
            typename First, 
3
            typename... Rest                    // Template parameter pack
4
        >     
5
void print(First first, Rest... rest) {         // Function parameter pack
6
    cout << first << endl;
7
    print(rest...);                             // Parameter pack expansion
8
} 



See, no cryptic boilerplate required. Isn't this solution looks neater?

There are total 3 types of folding: Unary fold, Binary fold & Fold over a comma. Here we have done left folding over a comma. You can read more about Fold Expression here.

Loop-Through/Iterate Over Tuple Elements in C++

If I give you a task to print the elements of tuple, the first thing that comes to your mind is:

C++
 




xxxxxxxxxx
1


 
1
template <typename... Args>
2
void print(const std::tuple<Args...> &t) {
3
    for (const auto &elem : t) // Error: no begin/end iterator
4
        cout << elem << endl;
5
}



But, this just can't work. std::tuple doesn't have begin & end iterator. OK! So, now you might try raw loop right?

C++
 




xxxxxxxxxx
1


 
1
template <typename... Args>
2
void print(const std::tuple<Args...>&   t) {
3
    for (int i = 0; i < sizeof...(Args); ++i)
4
        cout << std::get<i>(t) << endl;    // Error :( , `i` needs to be compile time constant
5
}



No! you can't. I know that std::get<> works with a number as non-type template argument.

But, that number has to be compile-time constant to make this working. So there are many solutions & we will go through quite enough ones.

C++11: Loop Through Tuple Elements

C++
 




xxxxxxxxxx
1
23


 
1
// Template recursion
2
template <size_t i, typename... Args>
3
struct printer  {
4
    static void print(const tuple<Args...> &t) {
5
        cout << get<i>(t) << endl;
6
        printer<i + 1, Args...>::print(t);
7
    }
8
};
9
 
           
10
// Terminating template specialisation
11
template <typename... Args>
12
struct printer<sizeof...(Args), Args...> {
13
    static void print(const tuple<Args...> &) {}
14
};
15
 
           
16
template <typename... Args>
17
void print(const tuple<Args...> &t) {
18
    printer<0, Args...>::print(t);
19
}
20
 
           
21
tuple<int, char, string> t(1, 'A', "ABC");
22
print(t);
23
// Note: might not work in GCC, I've used clang



This isn't that complicated as it looks, believe me. If you know recursion & template specialisation, it won't take you more than 30 seconds to figure out what's going on here.

For our example tuple<int, char, string> t(1, 'A', "ABC");, printer::print() calls template recursion i,e, template<size_t i, typename... Args> struct printer{}; each time with incremented non-type template parameter i. And when i == sizeof...(Args), our recusion stops by calling template specialization i.e. template<typename... Args> struct printer<sizeof...(Args), Args...> { };.

 C++17: Loop Through Tuple Elements

With C++ 17, it's slightly better because we have Fold Expressions. So, we don't need recursion any more.

C++
 




xxxxxxxxxx
1


 
1
template <typename... Args>
2
void print(const std::tuple<Args...> &t) {
3
    std::apply([](const auto &... args) {
4
        ((cout << args << endl), ...);
5
    }, t);
6
}



std::apply designed as tuple helper that accepts functor or lambda expression. Though you can do better if wants to dispatch to different implementation according to type, you might use overloaded class as:

C++
 




xxxxxxxxxx
1
17


 
1
template <class... Ts>
2
struct overloaded : Ts... {
3
    using Ts::operator()...;
4
};
5
 
           
6
// Deduction guide, google `CTAD for aggregates` for more info
7
template <class... Ts>
8
overloaded(Ts...) -> overloaded<Ts...>;   // not needed from C++20
9
 
           
10
auto f = overloaded {
11
    [](const int &a)        { cout << "From int: " << a << endl; },
12
    [](const char &b)       { cout << "From char: " << b << endl; },
13
    [](const string &c)     { cout << "From string: " << c << endl; },
14
};
15
 
           
16
tuple<int, char, string>    t(1, 'A', "ABC");
17
std::apply([&](const auto &... e) { (f(e), ...); }, t);



C++23: Loop Through Tuple Elements

C++
 




xxxxxxxxxx
1


 
1
template <typename... Args>
2
void print(const std::tuple<Args...> &t) {
3
    for... (const auto &elem : t)
4
        cout << elem << endl;
5
}



So, from C++23 we might have expansion statement i.e. for...(). That looks like a loop, though it isn't. It just stencil out each call with scope as:

C++
 




xxxxxxxxxx
1
15


 
1
template <typename... Args>
2
void print(const tuple<Args...> &t) {
3
    {
4
        const auto &elem = get<0>(t);
5
        cout << elem << endl;
6
    }
7
    {
8
        const auto &elem = get<1>(t);
9
        cout << elem << endl;
10
    }
11
    {
12
        const auto &elem = get<2>(t);
13
        cout << elem << endl;
14
    }
15
}



And it is obvious that there is no break and continue, as it isn't loop.

It basically works for every standard container which can access by std::get<>(). For example, a plain array, std::tuple, std::pair, std::array, unexpanded argument packs, constexpr ranges, etc.

Closing Words

There are still many things missing in our tuple class like copy constructor, move constructors, some operators and helper classes(like std::tuple_size). But I hope now you get the idea of how it can be implemented using the variadic template. By the way, implementing those missing things will be a good start for learning variadic template on your own.

Have Any Suggestions, Query or Wants to Say Hi? Take the Pressure Off, You Are Just a Click Away.

Topics:
c++, cpp, cpp17, data structures, tuple, tutorial, variadic templates

Published at DZone with permission of Vishal Chovatiya . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}