apply_tuple(op, t) (C++11 Final Version)

Overview

It has been a long while since my last post! All of my previous posts were written at a time when GCC and Clang had various issues with C++11 and did not have various portions of C++11 implemented. Very nicely, GCC and CLang now fully implement C++11! Despite the hiatus on this blog, I have been very busy writing C++ code including a lot of C++ template metaprogramming code. Included within such has been updates to apply_tuple() function. This post describes the (final) updates for apply_tuple() freely using appropriate C++11 features as needed.


Deleting Old Stuff

When I started messing around with coding apply_tuple() in 2011 (see the Articles By Topic link):

  • the C++11 standard had only been recently approved,
  • the C++ compilers were beginning to implement C++11,
  • C++11 features could not necessarily be used as they triggered compiler bugs or they were not yet implemented, and
  • everyone that wrote code in C++98/C++03 were all starting to explore how to do things in new and better ways in C++11.

As a result, the older apply_tuple() was done using older C++98 techniques –the last vestiges of such appeared in the Part III of my last apply_tuple() blog post which I am happy to delete from the code. Specifically, the following are completely deleted from the previous blog post:

  • struct make_seq_indices_impl
  • return_type type alias
  • apply_tuple_return_type_impl()
  • apply_tuple_return_type()
  • tuple_size() since it is not needed for one line of code

Since C++11 supports the decltype keyword to determine the type of an expression. This allows one to delete return_type, apply_tuple_return_type_impl(), and apply_tuple_return_type(). Additionally, SFINAE can be done with template parameters and this allows the deletion of struct make_seq_indices_impl. Finally, since tuple_size() is only one line of code, it was removed and replaced with a template parameter with a default value that computed the size of a tuple.

Rather than explain everything relative to the last version which forces one to read all of the previous blog posts, I will explain the solution from start to finish here. This is the final version of apply_tuple(). If you are interested in the earlier versions then look at my older posts –although after reading this, you might wonder along with me if I should just delete the older posts!

If you still need to write C++98 code, then the older posts are useful. Except for variadic template parameters, everything can be easily done using C++98 features. Variadic parameters can be handled in C++98 by hard coding definitions having 1, 2, 3, etc. parameters as needed. The use of constexpr (e.g., tuple_size) can be defined as C++ template metaprogramming (TMP) structs instead. The definitions of std::decay, etc. can be used via Boost or TR1 library code.

The discussion of the final apply_tuple() solution will start at the top of the source code file and work its way to the end. Simply concatenate all of the code for the complete working program.


The Required #include Files

This solution requires the use of std::tuple-related items and std::size_t, therefore these #include files are needed:

#include <cstddef>
#include <type_traits>


Generating the Tuple Indices

C++'s std::tuple requires using get<index>(tuple_var)</index> to access the tuple's elements. Unfortunately for newcomers to C++, INDEX must be a compile-time constant std::size_t that is permitted to be used as a template parameter. This means one cannot use a traditional for-loop to iterate through all elements to process them. While one could write a recursive function template to do this, that is too much work. It is better to create a list of std::size_t indices inside of a template parameter list that can be unrolled with a variadic template parameter list of tuples. In essence, if the tuple has 5 elements, one wants to obtain these indices:

indices<0,1,2,3,4>

These indices can be generated in a straight-forward manner. First one needs to tell the C++ compiler that indices is an empty struct that has zero or more std::size_t valued template parameters:

template <std::size_t...>
struct indices { };

The purpose of indices is only to hold the index values needed to be used with:

get<INDEX>(tuple_var)

The actual generating of these values is done in a C++ TMP metafunction called make_seq_indices.

If you are familiar with what was discussed in previous posts concerning make_seq_indices, know that the solution below was rewritten to be simpler and safer (since it checks that Begin <= End).

The definition of make_seq_indices is in several parts. It is defined using a struct that is partially specialized over its template arguments. The partial specializations cover these conditions:

  • the general case, i.e., when Begin > End, if invoked generates a compile-time error message via static_assert,
  • when Begin < End, and
  • when Begin == End which ends the make_seq_indices metafunction.

The definition of the general case requires Begin and End to be set, it sets Indices to be an empty list, and the final parameter is needed to use std::enable_if to define an appropriate partial specialization based on the values of Begin and End. The definition of the general case of make_seq_indices is:

template <
  std::size_t Begin,
  std::size_t End,
  typename Indices = indices<>,
  typename Enable = void
>
struct make_seq_indices
{
  static_assert(Begin <= End, "Begin must be <= End");
};

Due to the use of std::enable_if below, the general case will only be used when Begin <= End.

In the case when Begin < End one needs to add the current value of Begin to the end of the list of indices, I<Indices...> and recursively invoke make_seq_indices with the value of Begin plus one:

template <
  std::size_t Begin,
  std::size_t End,
  template <std::size_t...> class I,
  std::size_t... Indices
>
struct make_seq_indices<
  Begin, End,
  I<Indices...>,
  typename std::enable_if<Begin < End, void>::type
>
{
  using type =
    typename make_seq_indices<
      Begin+1, End,
      I<Indices..., Begin>
    >::type
  ;
};

Finally, when Begin == End the recursive calls must stop and the resulting Indices type is returned:

template <
  std::size_t Begin,
  std::size_t End,
  typename Indices
>
struct make_seq_indices<
  Begin, End,
  Indices,
  typename std::enable_if<Begin == End, void>::type
>
{
  using type = Indices;
};

Since it is very annoying to write expressions such as:

typename make_seq_indices<0, 10>::type

to obtain a list of indices, a C++11 type alias will be used instead like this:

make_seq_indices_T<0, 10>

to obtain a list of indices. The type alias is defined as:

template <std::size_t Begin, std::size_t End>
using make_seq_indices_T =
  typename make_seq_indices<Begin, End>::type
;


Applying an Operation on a List of Function Arguments

It is useful to consider how to pass a set of function arguments to a function or functor. The code to do this is:

template <typename Op, typename... Args>
inline constexpr auto apply(Op&& op, Args&&... args) ->
  decltype(std::forward<Op>(op)(std::forward<Args>(args)...))
{
  return std::forward<Op>(op)(std::forward<Args>(args)...);
}

The use of std::forward in conjunction with Op&& and Args&&... allows the following:

  • the compiler will perfectly forward op and all arguments, args... to the function/functor being called, and,
  • the std::forward used with op permits op to be treated as an rvalue if it is an rvalue.

The second bullet is important since C++11 supports "Move Semantics For *this" member functions and using std::forward on op will ensure op is an rvalue if it was originally an rvalue.

For more information on "Move Semantics For *this" see N2439.

Unlike the tuple solution below, nothing special needs to be done to pass the arguments in this version to op since the Args parameter pack is simply expanded inside the call to op:

std::forward<Args>(args)...


Applying an Operation to Each and Every std::tuple Element

Unlike the apply() function above, to apply a tuple the apply_tuple() function will receive a single std::tuple argument –but it will need to retrieve each tuple element for that single argument. Since the single std::tuple function argument is not and cannot be a template parameter pack, one needs a way of obtaining a template parameter pack containing the indices required to invoke the std::get() function on each tuple argument. Doing so enables one to expand the template parameter pack over the indices –which will give us the desired result:

// This function overload applies op to all tuple indices...
template <
  typename Op,
  typename Tuple,
  template <std::size_t...> class I,
  std::size_t... Indices
>
inline constexpr auto apply_tuple(
  Op&& op,
  Tuple&& t,
  I<Indices...>&&
) ->
  decltype(
    std::forward<Op>(op)(
      std::get<Indices>(std::forward<Tuple>(t))...
    )
  )
{
  return
    std::forward<Op>(op)(
      std::get<Indices>(std::forward<Tuple>(t))...
    )
  ;
}

But this is not the function the client user will invoke since one wants the indices to be generated automatically. Generating indices was the purpose of make_seq_indices and make_seq_indices_T metafunction! Thus, the definition of apply_tuple() client will directly invoke is:

// This function overload forwards op and t along with the
// indices of the tuple generated by make_seq_indices...
template <
  typename Op,
  typename Tuple,
  typename Indices =
    make_seq_indices_T<
      0,
      std::tuple_size<typename std::decay<Tuple>::type>::value
    >
>
inline constexpr auto apply_tuple(Op&& op, Tuple&& t) ->
  decltype(
    apply_tuple(
      std::forward<Op>(op),
      std::forward<Tuple>(t),
      Indices{}
    )
  )
{
  return
    apply_tuple(
      std::forward<Op>(op),
      std::forward<Tuple>(t),
      Indices{}
    )
  ;
}

Notice that there is an extra template parameter, Indices, that has a default value set. The caller of this function will never set its value. This is a handy way of creating a template parameter "variable" to hold a compile-time computed value in a function template's parameter list. In this particular instance, the Indices is used twice and by storing Indices in a template parameter, one does not have to copy-and-paste the code for it twice.

Earlier code went through some extraordinary tricks to obtain the return type of the call, op(args...). With C++11's decltype, the return value of such can be obtained by writing, decltype(op(args...)) provided the new function declarator syntax is used where the return type appears after the function arguments. This code is easier to read and understand. Unfortunately, one does have to duplicate the code in with the function's return type and with the return statement in the function.


A Sample Use

The following code is an example of using apply() and apply_tuple():

#include <iostream>

double add(int a, double b) { return a+b; }
std::tuple<int,double> some_tuple{2, 34.2};

int main()
{
  using namespace std;
  cout
    << apply(add, 2, 34.2) << endl
    << apply_tuple(add, some_tuple) << endl
  ;
}


Closing Comments

This solution is shorter and much easier to maintain than the previous solutions in my earlier blog posts. This is understandable given the amount of time that has elapsed since I first started and the evolution of C++ compilers from C++98 support to fully supporting C++11. I have been doing a lot of C++11 coding (normal and C++ TMP) within the last year and have been able to explore all of the new C++11 language features and having been learning new and better ways to write code that is more easily maintained and more efficient (than C++98, e.g., fully exploiting move semantics). One of the tasks that I have been doing now that GCC and CLang fully support C++11, is revisiting and updating old code to better use C++11's features. This post clearly shows doing such can be well-worth the effort!

I will again resume blogging concerning C++11 topics and other cool things in the fall. Between now and then, I will be posting some simpler coding-related posts as fun things to try out! 🙂

5 Replies to “apply_tuple(op, t) (C++11 Final Version)”

  1. Very interesting. I think in the interest of preservation of history, the old blog posts should be left up. Perhaps just add notes to them to indicate that they should not be followed.

    1. Good idea to add notes stating there is a new, final, and better version! 🙂 I will do this in the near future. That said, for anyone forced to stay with C++98 or C++03 the older posts are useful (even though they are not 100% C++98/03 they can be made to work with such).

  2. Why a "final version" ?
    C++ is always evolving ! And C++11 is so passé already :p

    The following C++14 code, compiles and run with clang trunk and libc++ trunk (with std=c++1y )

    #include <cstddef>
    #include <type_traits>
    #include <tuple>
    #include <utility> //  for std::integer_sequence

    template <typename Op, typename... Args>
    inline constexpr auto apply(Op&& op, Args&&... args)
    {
      return std::forward<Op>(op)(std::forward<Args>(args)...);
    }

    // This function overload applies op to all tuple indices...
    template <
      typename Op,
      typename Tuple,
      std::size_t... Indices
    >
    inline constexpr auto apply_tuple(
      Op&& op,
      Tuple&& t,
      std::integer_sequence<size_t, Indices...>&&
    )
    {
      return
        std::forward<Op>(op)(
          std::get<Indices>(std::forward<Tuple>(t))...
        )
      ;
    }

    // This function overload forwards op and t along with the
    // indices of the tuple generated by make_seq_indices...
    template <
      typename Op,
      typename Tuple,
      typename Indices =
        std::make_index_sequence<
          std::tuple_size<typename std::decay<Tuple>::type>::value
        >
    >
    inline constexpr auto apply_tuple(Op&& op, Tuple&& t)
    {
      return
        apply_tuple(
          std::forward<Op>(op),
          std::forward<Tuple>(t),
          Indices{}
        )
      ;
    }

    #include <iostream>

    double add(int a, double b) { return a+b; }
    std::tuple<int,double> some_tuple{2, 34.2};

    int main()
    {
      using namespace std;
      cout
        << apply(add, 2, 34.2) << endl
        << apply_tuple(add, some_tuple) << endl
      ;
    }

    Much nicer, right ? 🙂

    1. It is not a final version for all time! It is the essentially final version for my article series on it as compilers have evolved to support more and more of C++11. 🙂

      The version you've given is equivalent to my version as integer_sequence is in essence the same as make_seq_indices. That said, it is wonderful that one won't have to provide a make_seq_indices in C++14 –but unfortunately until compilers and their users "catch up" to C++11 (let alone C++14) such things must be provided.

Leave a Reply

Your email address will not be published. Required fields are marked *