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

Overview

This article updates my previous article on writing an efficient implementation of applying a tuple to a function using perfect forwarding.

This coding endeavour and article series started last year when fellow student, Bryan St. Amour, wrote an initial version and we've been hacking and discussing this (and other metaprogramming topics) off-and-on every since with our own separate code solutions:

  • his implementation unrolls the tuple to be applied using function calls, and,
  • my version relies on expanding the arguments pack within a single function call.

Bryan's solution syntactically resembles a functional programming style, e.g., his latest version uses template aliases as type functions and constexpr functions to perform the "magic" in a terse way. My approach has been to generate the function call without "recursively" unrolling the tuple elements using a function, i.e., to unroll all tuple elements in a single parameter pack expansion. Both of our solutions perform exactly the same operation from the end-user's perspective and are efficient with various pros and cons to each version –which will be a future article.

Aside: Bryan has an excellent update to his code and tells me he will be blogging about it soon. When he does I will delete this comment and link directly to it in the above paragraph.

Bryan and I agree we have worked out the details of what is necessary to extract a std::tuple's elements and efficiently pass them to a function call: we have been (and are) polishing our code and better exploiting C++11. Hence, we are in "The Home Stretch".

To avoid having far too much in one article, I will split up The Home Stretch article over a series of blog posts. The solutions presented so far:

  • do not yet handle applying a tuple to function that return void,
  • do not yet employ rvalue template argument deduction for apply_tuple() and apply_tuple_impl::apply_tuple() functions, and,
  • do not yet make use of template aliases and/or constexpr functions in C++11.

Clearly a final solution should have such with the same efficiency of the previously discussed versions. Know that such is possible and will be presented.

Article Source Code: cpp_code-apply_tuple-v6.zip


Handling void Functions/Functors

A small oversight in the previously presented code was that it could not handle functions and functors that return void. Fortunately this is easy to fix by using std::enable_if found in <type_traits>.

How std::enable_if Works

Details concerning std::enable_if can be found in the Boost Enable If library which explains one can use std::enable_if in practice with class and function templates.

NOTE: This article will only explore one aspect of its use as it pertains to the presented code.

A C++11 definition of std::enable_if could be:

template <bool Cond, class T>
struct enable_if { typedef T type; };

template <class T>
struct enable_if<false, T> { };

which makes it a compile-time C++ template metaprogramming function where:

  • if Cond is true, then std::enable_if<true,T>::type is defined and is a valid type, otherwise,
  • if Cond is false, then std::enable_if<false,T>::type is not defined and is, therefore, an invalid type.

It is important to recognize that templates are patterns and when template parameters are substituted in the template, the result may or may not contain valid code and/or type constructs. Equally important is recognizing, in the case of function templates, that the C++ compiler will look for the best match of a given function when it is overloaded. Both of these together will permit std::enable_if to be used without triggering a compile-time error and to properly select code for a function that returns void versus one that does not. For example, consider:

template <class T>
auto example(T)
  ->
    typename std::enable_if<
      std::is_same<T,void*>::value,
      void
    >::type
{
  std::cout << "void return!" << std::endl;
}

template <class T>
auto example(T t)
  ->
    typename std::enable_if<
      !std::is_same<T,void*>::value,
      T
    >::type
{
  std::cout << "This one returns t!" << std::endl;
  return t;
}

for which one should note:

  • if the type of the argument is void*, then std::enable_if<Cond,T>::type in the first example function will be void since std::is_same<T,void*>::value is true but it is not defined in the second example function; or,
  • if the type of the argument is not void*, then std::enable_if<Cond,T>::type in the first example function but it is not defined in the second.

Since one of the definitions always results in an invalid definition, the compiler discards the function template with the invalid return type (which would be a compile-time error if there wasn't another function that could be selected) in favour of the one with a valid return type. Thus, given any type T, there exists an unambiguous definition of example for the compiler to use. 🙂

Aside: C and C++ compilers only select the function to call based on its name and the number and types of its arguments. Never is the return type of the function used to select which function to call. This has not changed here: std::enable_if simply permits one to select a function template based on one or more of its template parameters.

Using std::enable_if With apply_tuple

It is now straight-forward to use std::enable_if with apply_tuple to handle void functions as follows:

  • given a void function, apply_tuple cannot be defined to return the result of calling apply_tuple_impl::apply_tuple, and,
  • given a non-void function, apply_tuple must return what apply_tuple_impl::apply_tuple returns.

Except for the addition of std::enable_if, the previous code's definitions of apply_tuple and apply_tuple_impl::apply_tuple represent the latter. Thus, like example(T) above, one only needs to write versions of apply_tuple and apply_tuple_impl::apply_tuple when a void function is passed as an argument. Before presenting the solution, let's define two C++11 template aliases to determine the return type using std::enable_if so the code is more readable and maintainable:

template <typename Op, typename... Args>
using enable_if_op_does_not_return_void =
  typename std::enable_if<
    !std::is_void<
      typename std::result_of<Op(Args...)>::type
    >::value,
    typename std::result_of<Op(Args...)>::type
  >::type
;

template <typename Op, typename... Args>
using enable_if_op_returns_void =
  typename std::enable_if<
    std::is_void<
      typename std::result_of<Op(Args...)>::type
    >::value,
    typename std::result_of<Op(Args...)>::type
  >::type
;

which permits apply() to be defined as:

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

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

and apply_tuple() to be defined as:

template <
  typename Indices
>
struct apply_tuple_impl;

template <
  template <std::size_t...> class I,
  std::size_t... Indices
>
struct apply_tuple_impl<I<Indices...>>
{
  template <
    typename Op,
    typename... OpArgs,
    template <typename...> class T
  >
  static auto apply_tuple(Op&& op, T<OpArgs...>&& t)
    -> enable_if_op_does_not_return_void<Op,OpArgs...>
  {
    return op( std::forward<OpArgs>(std::get<Indices>(t))... );
  }

  template <
    typename Op,
    typename... OpArgs,
    template <typename...> class T
  >
  static auto apply_tuple(Op&& op, T<OpArgs...>&& t)
    -> enable_if_op_returns_void<Op,OpArgs...>
  {
    op( std::forward<OpArgs>(std::get<Indices>(t))... );
  }

  template <
    typename Op,
    typename... OpArgs,
    template <typename...> class T
  >
  static auto apply_tuple(Op&& op, T<OpArgs...> const& t)
    -> enable_if_op_does_not_return_void<Op,OpArgs...>
  {
    return op( std::forward<OpArgs const>(std::get<Indices>(t))... );
  }

  template <
    typename Op,
    typename... OpArgs,
    template <typename...> class T
  >
  static auto apply_tuple(Op&& op, T<OpArgs...> const& t)
    -> enable_if_op_returns_void<Op,OpArgs...>
  {
    op( std::forward<OpArgs const>(std::get<Indices>(t))... );
  }
};

template <
  typename Op,
  typename... OpArgs,
  typename Indices = typename make_indices<0, OpArgs...>::type,
  template <typename...> class T
>
inline auto apply_tuple(Op&& op, T<OpArgs...>&& t)
  -> enable_if_op_does_not_return_void<Op,OpArgs...>
{
  return apply_tuple_impl<Indices>::apply_tuple(
    std::forward<Op>(op),
    std::forward<T<OpArgs...>>(t)
  );
}

template <
  typename Op,
  typename... OpArgs,
  typename Indices = typename make_indices<0, OpArgs...>::type,
  template <typename...> class T
>
inline auto apply_tuple(Op&& op, T<OpArgs...> const& t)
  -> enable_if_op_does_not_return_void<Op,OpArgs...>
{
  return apply_tuple_impl<Indices>::apply_tuple(
    std::forward<Op>(op),
    std::forward<T<OpArgs...> const>(t)
  );
}

template <
  typename Op,
  typename... OpArgs,
  typename Indices = typename make_indices<0, OpArgs...>::type,
  template <typename...> class T
>
inline auto apply_tuple(Op&& op, T<OpArgs...>&& t)
  -> enable_if_op_returns_void<Op,OpArgs...>
{
  apply_tuple_impl<Indices>::apply_tuple(
    std::forward<Op>(op),
    std::forward<T<OpArgs...>>(t)
  );
}

template <
  typename Op,
  typename... OpArgs,
  typename Indices = typename make_indices<0, OpArgs...>::type,
  template <typename...> class T
>
inline auto apply_tuple(Op&& op, T<OpArgs...> const& t)
  -> enable_if_op_returns_void<Op,OpArgs...>
{
  apply_tuple_impl<Indices>::apply_tuple(
    std::forward<Op>(op),
    std::forward<T<OpArgs...> const>(t)
  );
}

Success! 🙂


Closing Comments

Although the above code now supports void functions, the number of apply-related functions has doubled even though the code within each function is essentially the same. Fortunately, it is possible to exploit C++11's ability to use rvalue template argument deduction to accept lvalues or rvalues (by collapsing references). This feature only works if the function argument is a simple template type (which it is not here). Fortunately, with some cleverness, this is possible and will be the focus of the next article. The resulting solution will eliminate one half of the functions above (i.e., those taking constant references will be eliminated). 🙂

Leave a Reply

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