Applying Tuple To Functors and Functions: The Home Stretch (Part II)

Overview

If you’ve been following this article series, this is the article you’ve been waiting to read! It is the one where I show the final versions of apply() and apply_tuple(). These versions have the following features:

  • perfect forwarding of all tuple elements to the corresponding arguments of a function (as with all previous versions), and,
  • rvalue template argument deduction is used to reduce the previous solution’s apply_tuple() and apply_tuple_impl::apply_tuple() total of eight (8) functions to four (4),

This article will present the full solution while briefly discussing the elements that make it work.

Update (Feb. 15, 2012): After posting this article, I realized I missed a few (obvious!) simplifications to the code which make it substantially easier to read and understand. The main change was to make apply_tuple() use an easy-to-use template alias as a compile-time type function guard for std::enable_if. A non-cosmetic change was also made as well: the apply_tuple_return_type_impl() prototype was modified to use std::get to remove any possibility of any type mismatches (e.g., differences with const/volatile qualifiers) within the code. I have replaced the previous code and updated the text only in places that make specific code references that would otherwise no longer match the code.


Supporting Code

Required #include Files

The following are the required #include files:

#include <cstddef>
#include <tuple>
#include <functional>
#include <type_traits>

Computing The Indices

In order to extract the elements of a std::tuple, the tuple index is needed as a template parameter, and, to extract all elements of a tuple in a single parameter expansion, a complete set of tuple parameter indices is needed to index the provided tuple. This solution stored the indices using the following type:

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

which has no definition since there will no (run-time) instance of indices created.

Generating The Indices

Previously, the indices were generated in a slightly different way. In order to make it easier to reduce the number of apply_tuple()-related functions from eight (8) to four (4), the method of generating indices was changed. Now, to generate the indices, one uses make_seq_indices<BEGIN,END> where BEGIN is the starting index (i.e., 0) and END is one past the last index. Thus the definition of make_seq_indices and its supporting code is:

template <std::size_t Begin, std::size_t End, typename Indices>
struct make_seq_indices_impl;

template <std::size_t Begin, std::size_t End, std::size_t... Indices>
struct make_seq_indices_impl<Begin, End, indices<Indices...>>
{
  using type =
    typename
      make_seq_indices_impl<Begin+1, End,indices<Indices..., Begin>
    >::type
  ;
};

template <std::size_t End, std::size_t... Indices>
struct make_seq_indices_impl<End, End, indices<Indices...>>
{
  using type = indices<Indices...>;
};

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

Indirectly Determining the Return Type

The first argument to apply() and apply_tuple() is a function or a functor. For the code to work, one needs to be able to easily determine the return type without directly using template template parameters within apply_tuple(). I came up with a clever way to do this only using function prototypes:

template <typename F>
using return_type = typename std::result_of<F>::type;

template <typename Tuple>
constexpr std::size_t tuple_size()
{
  return std::tuple_size<typename std::decay<Tuple>::type>::value;
}

template <
  typename Op,
  typename T,
  template <std::size_t...> class I,
  std::size_t... Indices
>
constexpr auto apply_tuple_return_type_impl(
  Op&& op, T&& t, I<Indices...>
)
  -> return_type<
    Op(decltype(std::get<Indices>(std::forward<T>(t)))...)
  >;

template <typename Op, typename T>
constexpr auto apply_tuple_return_type(Op&& op, T&& t)
  -> decltype(
    apply_tuple_return_type_impl(
      op, t, make_seq_indices<0,tuple_size<T>()>{}
    )
  );

where:

  • return_type and tuple_size are convenience compile-time type functions, and,
  • apply_tuple_return_type accepts forwarded types and passes them to apply_tuple_return_type_impl for template argument deduction so that function’s/functor’s return type can be determined.

The trick here is to:

  • use function prototypes to help ensure run-time code for these functions is never generated,
  • use function prototypes to trigger template argument deduction,
  • use the auto return type to permit the use of std::result_of to find the return type of Op(OpArgs...), and,
  • the auto return type to allow decltype to be used with the function’s arguments applied to apply_tuple_return_type_impl to enable computing the return type.

A consequence of the above is that it allows any client code to determine the return type of a function/functor without directly using template template parameters. For example, later in this article apply_tuple_return_type_impl is used like this:

decltype(apply_tuple_return_type(
  std::forward<Op>(op),
  std::forward<Tuple>(t)
))

Since template template parameters are not (directly) used used to determine the types that will be passed to the function/functor, there is no need to write both lvalue and rvalue overloaded functions to handle the tuple parameter. Since the above definition also uses the same function, std::get, to determine tuple element type as apply_tuple() no type mismatches will occur as a result of using this prototype and the apply_tuple() functions.

Aside: Ensuring that template template parameters need not be directly used along with another item mentioned later was key to reducing the number of functions required from eight (8) to four (4) with apply_tuple and apply_tuple_impl::apply_tuple().

Helper Functions For Readability

Without the following definitions, the presented code is harder to read and understand:

template <typename T>
constexpr bool is_void()
{
  return std::is_void<T>::value;
}

template <typename F>
constexpr bool returns_void()
{
  return std::is_void<return_type<F>>::value;
}

template <bool Cond, typename T>
using enable_if = typename std::enable_if<Cond,T>::type;

template <typename Ret>
using if_returns_void_case = enable_if<is_void<Ret>(),Ret>;

template <typename Ret>
using if_returns_nonvoid_case = enable_if<!is_void<Ret>(),Ret>;

where:

  • is_void, returns_void, and enable_if were defined to avoid having to write typename and ::type in numerous places,
  • if_returns_nonvoid_case invokes std::enable_if producing a valid type when op does not return void, and,
  • if_returns_void_case invokes std::enable_if producing a valid type when op returns void.

These definitions will beautify the code below.


Defining apply() and apply_tuple()

Definition of apply()

Recall that the definition of apply() does not use tuples: it simply forwards all arguments after the first argument to Op. So its definition remains unchanged:

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

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

Certainly, this was the easy case! :-)

Definition of apply_tuple()

The definitions below forwards all tuple elements to a functor or a function:

// void cannot be the default value for Enable...
template <typename Indices, typename Ret, typename Enable = Ret>
struct apply_tuple_impl;

// void return case...
template <
  template <std::size_t...> class I, std::size_t... Indices,
  typename Ret
>
struct apply_tuple_impl<I<Indices...>, Ret, void>
{
  template <typename Op, typename T>
  inline static Ret apply_tuple(Op&& op, T&& t)
  {
    op(
      std::forward<
        decltype(std::get<Indices>(std::forward<T>(t)))
      >(std::get<Indices>(std::forward<T>(t)))...
    );
  }
};

// non-void return case...
template <
  template <std::size_t...> class I, std::size_t... Indices,
  typename Ret
>
struct apply_tuple_impl<
  I<Indices...>,
  Ret,
  if_returns_nonvoid_case<Ret>
>
{
  template <typename Op, typename T>
  inline static Ret apply_tuple(Op&& op, T&& t)
  {
    return op(
      std::forward<
        decltype(std::get<Indices>(std::forward<T>(t)))
      >(std::get<Indices>(std::forward<T>(t)))...
    );
  }
};

// non-void return case...
template <typename Op, typename Tuple>
inline auto apply_tuple(Op&& op, Tuple&& t) ->
  if_returns_nonvoid_case<
    decltype(
      apply_tuple_return_type(
        std::forward<Op>(op), std::forward<Tuple>(t)
      )
    )
  >
{
  return
    apply_tuple_impl<
      make_seq_indices<0,tuple_size<Tuple>()>,
      decltype(
        apply_tuple_return_type(
          std::forward<Op>(op), std::forward<Tuple>(t)
        )
      )
    >::apply_tuple(std::forward<Op>(op),std::forward<Tuple>(t))
  ;
}

// void return case...
template <typename Op, typename Tuple>
inline auto apply_tuple(Op&& op, Tuple&& t) ->
  if_returns_void_case<
    decltype(
      apply_tuple_return_type(
        std::forward<Op>(op), std::forward<Tuple>(t)
      )
    )
  >
{
  apply_tuple_impl<
    make_seq_indices<0,tuple_size<Tuple>()>,
    decltype(
      apply_tuple_return_type(
        std::forward<Op>(op), std::forward<Tuple>(t)
      )
    )
  >::apply_tuple(std::forward<Op>(op),std::forward<Tuple>(t));
}

Starting with apply_tuple(), note the following:

  1. Two versions of apply_tuple() are needed: one for operations returning void and one for those returning a type. The use of enable_if allows the compiler to select the correct function based on Op.
  2. std::decay is used to strip Tuple down to is base type so that the tuple’s size can be determined.
  3. With the size, make_seq_indices is invoked to generate the indices.
  4. decltype is used to determine the operation’s return type.

The purpose of apply_tuple() is to forward everything to apply_tuple_impl::apply_tuple(). This is needed since the indices must be a parameter pack. Unlike previous version, the indices and the return type are computed without using a template template type within apply_tuple(). With apply_tuple() setting things up, the definition of apply_tuple_impl::apply_tuple() is straight-forward. The reader will note the code is identical to previous code since it still invokes std::get<Indices>(std::forward<T>(t)). This is what has changed:

  1. The template arguments do not make use of any template template parameters. This allows rvalue template argument deduction and reference collapsing to take place. Consequently, this allows one to write one function for the two cases: the void return type and non-void return type.
  2. What allowed Item 1 to be done was realizing that one wants to use decltype within the std::forward template parameter to determine the type of each element instead of the tuple’s template argument parameter pack. While this duplicates the get code twice, it permits the parameter pack expansion to work identically without explicitly using any template template parmeters. It also avoids any possibility of incorrect types between what is passed to the function and what is provided to std::forward.

The choice of not directly using template template parameters is what enables this solution to eliminate four (4) function definitions (i.e., the lvalue overloads). As a bonus the solution’s type safety is now robust since std::forward‘s parameter type exactly matches what is provided to the function. This avoids the possibility of compiler “error novels” should somehow some of the types don’t match up.


Closing Comments

Overall, this solution is not pretty, but, it is straight-forward, maintainable (especially if some comments are added), and, best of all, its use by end-users is almost effortless. The end-user need not ever see the above as he/she would simply write code like this:

double add(int a, double b)
{
  return a+b;
}

//...
std::tuple<int,double> some_tuple;
// ...

apply(add, 2, 34.2);  // or
apply_tuple(add, some_tuple);

Such code can be easily written by newcomers and experienced C++ programmers alike!

Happy Coding! :-)

5 thoughts on “Applying Tuple To Functors and Functions: The Home Stretch (Part II)”

  1. Nice, sadly though, this only works with g++ 4.7 since 4.6 (which ships with Ubuntu, and as a rule I don’t want people to have to compile their own compiler to compile my code) doesn’t support `using` aliases yet…

    1. The template aliases were only used to promote ease-of-reading and understanding. All of the template aliases can all be removed by substituting their definitions where they are used. The four-function solution however relies on compilers’ C++11 support since decltype and move semantics are needed.

      The GCC team appears to be planning to have v4.7 out soon. Additionally, all of the major C++ compilers are actively and seriously implementing C++11 in a short period of time as well. So my guess is everyone should be able to reap (at least most of if not all of) the benefits of C++11 at various points this year depending on the compiler being used.

  2. This “apply tuple” serie was great. Didn’t know before about template aliases and also about this helper function prototype trick to determine return type. That’s really neat.

    Still, I believe that your final version of apply_tuple() can be greatly simplified. Here is a couple of suggestions :

    [1] I don’t understand the necessity to have two paths, one for function that return void and one for function that return non-void.
    The following code is legal :
    void foo(){}
    void goo()
    {
    return foo();
    }
    So you can remove completely the void path in apply_tuple and just do directly something like return op(std::get(tup)…);

    [2] About this line :
    op( std::forward< decltype(std::get(std::forward(t)))
    >(std::get(std::forward(t)))… );
    I think that you don’t need to perfectly forward again the result of std::get. You can do this :
    op( std::get(std::forward(t))… );
    Because std::get already do the right thing : there is two overload of std::get, one for lvalue, one for rvalue :
    // 20.4.2.6, element access:
    template
    typename tuple_element<I, tuple >::type& get(tuple&) noexcept;
    template
    typename tuple_element<I, tuple >::type&& get(tuple&&) noexcept;

    [3] Another point, it maybe just a style issue but I prefer to pass the indices list as a parameter to a function apply_tuple_imp instead as a template parameter to a struct apply_tuple_imp, because there is so much less syntactic overhead this way.

    So I finally came up with the following, compact implementation of apply_tuple. I believe it cover all cases. What do you think of it ?

    template
    struct tuple_indices {};

    template
    struct make_indices_imp;

    template
    struct make_indices_imp<Sp, tuple_indices, Ep>
    {
    using type = typename make_indices_imp<Sp+1, tuple_indices, Ep>::type;
    };

    template
    struct make_indices_imp<Ep, tuple_indices, Ep>
    {
    using type = tuple_indices;
    };

    template
    using make_tuple_indices = typename make_indices_imp<Sp, tuple_indices, Ep>::type;

    template
    using apply_tuple_indices = make_tuple_indices<std::tuple_size<typename std::decay::type>::value>;

    template
    auto apply_tuple_imp(Op&& op, Tuple&& t, tuple_indices)
    -> decltype(std::forward(op)(std::get(std::forward(t))…))
    {
    return std::forward(op)(std::get(std::forward(t))…);
    }

    template
    auto apply_tuple(Op&& op, Tuple&& t)
    ->decltype(apply_tuple_imp(std::forward(op), std::forward(t), apply_tuple_indices()))
    {
    return apply_tuple_imp(std::forward(op), std::forward(t), apply_tuple_indices());
    }

    double func_double(int i, double d)
    {
    return i + d;
    }

    void func_void(int i, double d)
    {
    std::cout << i + d << "\n";
    }

    void func_movable(std::string&& s)
    {
    }

    int main()
    {
    std::tuple t(4, 5.0);
    std::cout << apply_tuple(func_double, t) << "\n";
    std::cout << apply_tuple([](int i, double d){return i + d;}, t) << "\n";
    std::cout << apply_tuple(func_double, std::tuple(8, 4.0)) << "\n";
    apply_tuple(func_void, t);
    apply_tuple(func_void, std::tuple(8, 4.0));

    apply_tuple(func_movable, std::forward_as_tuple(“C++o”));
    apply_tuple(func_movable, std::tuple(“C++”));
    std::tuple tmov(“C++”);
    apply_tuple(func_movable, std::move(tmov));
    }

    1. Yes, absolutely your suggestions are all good and do simplify the code considerably! I should have noticed each of them myself, however, I was very busy when I wrote the last few posts so I am not surprised some simplifications were missed. (I only spent the time to ensure the code was correct and many of the simplications possible were done.) Certainly, I do revisit code from time-to-time to have fresh eyes to better see things in a new light (esp. with the newness of C++11 features as they get implemented in compilers) and I was taking a break from it for a few weeks. Your post has me looking at it a little sooner than I was planning!

      Concerning point [1]: For anyone surprised about this point, know that in ISO C++03 (and in C++11) it states in 6.6.3 paragraph 3 that a void function can have a return statement whose expression is void (regardless of cv-qualifiers). Thus, it is legal code. While not very useful in non-template code, when using templates with functions/functors it is wonderful.

      Concerning point [2]: When I wrote the code I felt that omitting the forward would be totally correct. I kept it in however since some “playing around” code had cv-qualifier type issues and I wanted no chance that I missed something in my blog post. I thought I would revisit it after a break to see if it could be removed. (And clearly it can be as std::get() is properly overloaded for l- and r-values.)

      Concerning point [3]: It looks good. Unfortunately, your code had its < and > removed by WordPress’ editor (sigh!) as the template code wasn’t legal HTML. With respect to the indices, generally I try avoiding unnecessary function arguments that are never used –hence my use of a class. In this instance doing such makes the code simpler by eliminating the helper class entirely at no real cost (with anything better than -O0 with any decent C++ compiler) –so it works.

      I’ve modified and tested my code concerning such and all is good! (No surprise!) I’ll post an updated article very soon with the updated code so others can see it all neat and tidy.

      Observations of the results…

      1) Nicely, all of the std::enable_if guards are no longer needed. Clearly fewer symbols is better for the compiler (e.g., compile time, space, and ability to perform optimizations).

      2) Nicely, all of the series of articles on this are conceptually the same concerning the big picture of how everything works –even though the specifics differ somewhat. Even the articles that are now obsoleted with the latest tweaks are useful in that they show how to use various template metaprogramming techniques –which perhaps this article series is not a half-bad example set to use for one learning such –even if I am a bit heavy with the code/text at times?

      3) Another thing I like is that it shows how things are developed and refined over time. IMHO one learns a lot from studying how solutions are developed and especially why some solutions are less good than other solutions. For example, this is what I’ve always liked about Herb Sutter’s “Exceptional C++” and “More Exceptional C++” books: he asks, presents, and explains an entire gamut of issues concerning important topics –not just a final “best/decent” solution.

      Closing comments…

      Now only 2 apply_tuple() functions are required, 1 apply_tuple() function, the two function prototypes (with no definitions), plus the make_seq_indices (or whatever one wants to generate the indices). The rest of the code is simply some template aliases and constexpr functions that make it easier to read the code. Awesome!!

      I’ll post this soon on my blog.

Leave a Reply

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


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>