Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make generic computations over heterogeneous argument packs of a variadic template function?

PREMISE:

After playing around with variadic templates a little bit, I realized that achieving anything which goes slightly beyond the trivial meta-programming tasks soon becomes pretty cumbersome. In particular, I found myself wishing for a way to perform generic operations over an argument pack such as iterate, split, loop in a std::for_each-like fashion, and so on.

After watching this lecture by Andrei Alexandrescu from C++ and Beyond 2012 on the desirability of static if into C++ (a construct borrowed from the D Programming Language) I had the feeling that some sort of static for would come handy as well - and I feel more of these static constructs could bring benefit.

So I started wondering if there is a way to achieve something like this for argument packs of a variadic template function (pseudo-code):

template<typename... Ts> void my_function(Ts&&... args) {     static for (int i = 0; i < sizeof...(args); i++) // PSEUDO-CODE!     {         foo(nth_value_of<i>(args));     } } 

Which would get translated at compile-time into something like this:

template<typename... Ts> void my_function(Ts&&... args) {     foo(nth_value_of<0>(args));     foo(nth_value_of<1>(args));     // ...     foo(nth_value_of<sizeof...(args) - 1>(args)); } 

In principle, static_for would allow for even more elaborate processing:

template<typename... Ts> void foo(Ts&&... args) {     constexpr s = sizeof...(args);      static for (int i = 0; i < s / 2; i++)     {         // Do something         foo(nth_value_of<i>(args));     }      static for (int i = s / 2; i < s; i++)     {         // Do something different         bar(nth_value_of<i>(args));     } } 

Or for a more expressive idiom like this one:

template<typename... Ts> void foo(Ts&&... args) {     static for_each (auto&& x : args)     {         foo(x);     } } 

RELATED WORK:

I did some search on the Web and found out that something does indeed exist:

  • This link describes how to convert a parameter pack into a Boost.MPL vector, but that only goes half the way (if not less) towards the goal;
  • this question on SO seems to call for a similar and slightly related meta-programming feature (splitting an argument pack into two halves) - actually, there are several questions on SO which seem to be related to this issue, but none of the answer I have read solves it satisfactorily IMHO;
  • Boost.Fusion defines algorithms for converting an argument pack into a tuple, but I would prefer:
    1. not to create unnecessary temporaries to hold arguments that can (and should be) perfectly forwarded to some generic algorithms;
    2. have a small, self-contained library to do that, while Boost.Fusion is likely to include way more stuff than is needed to address this issue.

QUESTION:

Is there a relatively simple way, possibly through some template meta-programming, to achieve what I am looking for without incurring in the limitations of the existing approaches?

like image 424
Andy Prowl Avatar asked Jan 10 '13 15:01

Andy Prowl


People also ask

What is Variadic template in C++?

Variadic templates are class or function templates, that can take any variable(zero or more) number of arguments. In C++, templates can have a fixed number of parameters only that have to be specified at the time of declaration.

What is the use of Variadic templates?

With the variadic templates feature, you can define class or function templates that have any number (including zero) of parameters. To achieve this goal, this feature introduces a kind of parameter called parameter pack to represent a list of zero or more parameters for templates.

What is parameter pack in c++?

Parameter packs (C++11) A parameter pack can be a type of parameter for templates. Unlike previous parameters, which can only bind to a single argument, a parameter pack can pack multiple parameters into a single parameter by placing an ellipsis to the left of the parameter name.


2 Answers

Since I was not happy with what I found, I tried to work out a solution myself and ended up writing a small library which allows formulating generic operations on argument packs. My solution has the following features:

  • Allows iterating over all or some elements of an argument pack, possibly specified by computing their indices on the pack;
  • Allows forwarding computed portions of an argument pack to variadic functors;
  • Only requires including one relatively short header file;
  • Makes extensive use of perfect forwarding to allow for heavy inlining and avoids unnecessary copies/moves to allow for minimum performance loss;
  • The internal implementation of the iterating algorithms relies on Empty Base Class Optimization for minimizing memory consumption;
  • It is easy (relatively, considering it's template meta-programming) to extend and adapt.

I will first show what can be done with the library, then post its implementation.

USE CASES

Here is an example of how the for_each_in_arg_pack() function can be used to iterate through all the arguments of a pack and pass each argument in input to some client-supplied functor (of course, the functor must have a generic call operator if the argument pack contains values of heterogenous types):

// Simple functor with a generic call operator that prints its input. This is used by the // following functors and by some demonstrative test cases in the main() routine. struct print {     template<typename T>     void operator () (T&& t)     {         cout << t << endl;     } };  // This shows how a for_each_*** helper can be used inside a variadic template function template<typename... Ts> void print_all(Ts&&... args) {     for_each_in_arg_pack(print(), forward<Ts>(args)...); } 

The print functor above can also be used in more complex computations. In particular, here is how one would iterate on a subset (in this case, a sub-range) of the arguments in a pack:

// Shows how to select portions of an argument pack and  // invoke a functor for each of the selected elements template<typename... Ts> void split_and_print(Ts&&... args) {     constexpr size_t packSize = sizeof...(args);     constexpr size_t halfSize = packSize / 2;      cout << "Printing first half:" << endl;     for_each_in_arg_pack_subset(         print(), // The functor to invoke for each element         index_range<0, halfSize>(), // The indices to select         forward<Ts>(args)... // The argument pack         );      cout << "Printing second half:" << endl;     for_each_in_arg_pack_subset(         print(), // The functor to invoke for each element         index_range<halfSize, packSize>(), // The indices to select         forward<Ts>(args)... // The argument pack         ); } 

Sometimes, one may just want to forward a portion of an argument pack to some other variadic functor instead of iterating through its elements and pass each of them individually to a non-variadic functor. This is what the forward_subpack() algorithm allows doing:

// Functor with variadic call operator that shows the usage of for_each_***  // to print all the arguments of a heterogeneous pack struct my_func {     template<typename... Ts>     void operator ()(Ts&&... args)     {         print_all(forward<Ts>(args)...);     } };  // Shows how to forward only a portion of an argument pack  // to another variadic functor template<typename... Ts> void split_and_print(Ts&&... args) {     constexpr size_t packSize = sizeof...(args);     constexpr size_t halfSize = packSize / 2;      cout << "Printing first half:" << endl;     forward_subpack(my_func(), index_range<0, halfSize>(), forward<Ts>(args)...);      cout << "Printing second half:" << endl;     forward_subpack(my_func(), index_range<halfSize, packSize>(), forward<Ts>(args)...); } 

For more specific tasks, it is of course possible to retrieve specific arguments in a pack by indexing them. This is what the nth_value_of() function allows doing, together with its helpers first_value_of() and last_value_of():

// Shows that arguments in a pack can be indexed template<unsigned I, typename... Ts> void print_first_last_and_indexed(Ts&&... args) {     cout << "First argument: " << first_value_of(forward<Ts>(args)...) << endl;     cout << "Last argument: " << last_value_of(forward<Ts>(args)...) << endl;     cout << "Argument #" << I << ": " << nth_value_of<I>(forward<Ts>(args)...) << endl; } 

If the argument pack is homogeneous on the other hand (i.e. all arguments have the same type), a formulation such as the one below might be preferable. The is_homogeneous_pack<> meta-function allows determining whether all the types in a parameter pack are homogeneous, and is mainly meant to be used in static_assert() statements:

// Shows the use of range-based for loops to iterate over a // homogeneous argument pack template<typename... Ts> void print_all(Ts&&... args) {     static_assert(         is_homogeneous_pack<Ts...>::value,          "Template parameter pack not homogeneous!"         );      for (auto&& x : { args... })     {         // Do something with x...     }      cout << endl; } 

Finally, since lambdas are just syntactic sugar for functors, they can be used as well in combination with the algorithms above; however, until generic lambdas will be supported by C++, this is only possible for homogeneous argument packs. The following example also shows the usage of the homogeneous-type<> meta-function, which returns the type of all arguments in a homogeneous pack:

 // ...  static_assert(      is_homogeneous_pack<Ts...>::value,       "Template parameter pack not homogeneous!"      );  using type = homogeneous_type<Ts...>::type;  for_each_in_arg_pack([] (type const& x) { cout << x << endl; }, forward<Ts>(args)...); 

This is basically what the library allows doing, but I believe it could even be extended to carry out more complex tasks.

IMPLEMENTATION

Now comes the implementation, which is a bit tricky in itself so I will rely on comments to explain the code and avoid making this post too long (perhaps it already is):

#include <type_traits> #include <utility>  //=============================================================================== // META-FUNCTIONS FOR EXTRACTING THE n-th TYPE OF A PARAMETER PACK  // Declare primary template template<int I, typename... Ts> struct nth_type_of { };  // Base step template<typename T, typename... Ts> struct nth_type_of<0, T, Ts...> {     using type = T; };  // Induction step template<int I, typename T, typename... Ts> struct nth_type_of<I, T, Ts...> {     using type = typename nth_type_of<I - 1, Ts...>::type; };  // Helper meta-function for retrieving the first type in a parameter pack template<typename... Ts> struct first_type_of {     using type = typename nth_type_of<0, Ts...>::type; };  // Helper meta-function for retrieving the last type in a parameter pack template<typename... Ts> struct last_type_of {     using type = typename nth_type_of<sizeof...(Ts) - 1, Ts...>::type; };  //=============================================================================== // FUNCTIONS FOR EXTRACTING THE n-th VALUE OF AN ARGUMENT PACK  // Base step template<int I, typename T, typename... Ts> auto nth_value_of(T&& t, Ts&&... args) ->     typename std::enable_if<(I == 0), decltype(std::forward<T>(t))>::type {     return std::forward<T>(t); }  // Induction step template<int I, typename T, typename... Ts> auto nth_value_of(T&& t, Ts&&... args) ->     typename std::enable_if<(I > 0), decltype(         std::forward<typename nth_type_of<I, T, Ts...>::type>(             std::declval<typename nth_type_of<I, T, Ts...>::type>()             )         )>::type {     using return_type = typename nth_type_of<I, T, Ts...>::type;     return std::forward<return_type>(nth_value_of<I - 1>((std::forward<Ts>(args))...)); }  // Helper function for retrieving the first value of an argument pack template<typename... Ts> auto first_value_of(Ts&&... args) ->     decltype(         std::forward<typename first_type_of<Ts...>::type>(             std::declval<typename first_type_of<Ts...>::type>()             )         ) {     using return_type = typename first_type_of<Ts...>::type;     return std::forward<return_type>(nth_value_of<0>((std::forward<Ts>(args))...)); }  // Helper function for retrieving the last value of an argument pack template<typename... Ts> auto last_value_of(Ts&&... args) ->     decltype(         std::forward<typename last_type_of<Ts...>::type>(             std::declval<typename last_type_of<Ts...>::type>()             )         ) {     using return_type = typename last_type_of<Ts...>::type;     return std::forward<return_type>(nth_value_of<sizeof...(Ts) - 1>((std::forward<Ts>(args))...)); }  //=============================================================================== // METAFUNCTION FOR COMPUTING THE UNDERLYING TYPE OF HOMOGENEOUS PARAMETER PACKS  // Used as the underlying type of non-homogeneous parameter packs struct null_type { };  // Declare primary template template<typename... Ts> struct homogeneous_type;  // Base step template<typename T> struct homogeneous_type<T> {     using type = T;     static const bool isHomogeneous = true; };  // Induction step template<typename T, typename... Ts> struct homogeneous_type<T, Ts...> {     // The underlying type of the tail of the parameter pack     using type_of_remaining_parameters = typename homogeneous_type<Ts...>::type;      // True if each parameter in the pack has the same type     static const bool isHomogeneous = std::is_same<T, type_of_remaining_parameters>::value;      // If isHomogeneous is "false", the underlying type is the fictitious null_type     using type = typename std::conditional<isHomogeneous, T, null_type>::type; };  // Meta-function to determine if a parameter pack is homogeneous template<typename... Ts> struct is_homogeneous_pack {     static const bool value = homogeneous_type<Ts...>::isHomogeneous; };  //=============================================================================== // META-FUNCTIONS FOR CREATING INDEX LISTS  // The structure that encapsulates index lists template <unsigned... Is> struct index_list { };  // Collects internal details for generating index ranges [MIN, MAX) namespace detail {     // Declare primary template for index range builder     template <unsigned MIN, unsigned N, unsigned... Is>     struct range_builder;      // Base step     template <unsigned MIN, unsigned... Is>     struct range_builder<MIN, MIN, Is...>     {         typedef index_list<Is...> type;     };      // Induction step     template <unsigned MIN, unsigned N, unsigned... Is>     struct range_builder : public range_builder<MIN, N - 1, N - 1, Is...>     {     }; }  // Meta-function that returns a [MIN, MAX) index range template<unsigned MIN, unsigned MAX> using index_range = typename detail::range_builder<MIN, MAX>::type;  //=============================================================================== // CLASSES AND FUNCTIONS FOR REALIZING LOOPS ON ARGUMENT PACKS  // Implementation inspired by @jogojapan's answer to this question: // http://stackoverflow.com/questions/14089637/return-several-arguments-for-another-function-by-a-single-function  // Collects internal details for implementing functor invocation namespace detail {     // Functor invocation is realized through variadic inheritance.     // The constructor of each base class invokes an input functor.     // An functor invoker for an argument pack has one base class     // for each argument in the pack      // Realizes the invocation of the functor for one parameter     template<unsigned I, typename T>     struct invoker_base     {         template<typename F, typename U>         invoker_base(F&& f, U&& u) { f(u); }     };      // Necessary because a class cannot inherit the same class twice     template<unsigned I, typename T>     struct indexed_type     {         static const unsigned int index = I;         using type = T;     };      // The functor invoker: inherits from a list of base classes.     // The constructor of each of these classes invokes the input     // functor with one of the arguments in the pack.     template<typename... Ts>     struct invoker : public invoker_base<Ts::index, typename Ts::type>...     {         template<typename F, typename... Us>         invoker(F&& f, Us&&... args)             :             invoker_base<Ts::index, typename Ts::type>(std::forward<F>(f), std::forward<Us>(args))...         {         }     }; }  // The functor provided in the first argument is invoked for each // argument in the pack whose index is contained in the index list // specified in the second argument template<typename F, unsigned... Is, typename... Ts> void for_each_in_arg_pack_subset(F&& f, index_list<Is...> const& i, Ts&&... args) {     // Constructors of invoker's sub-objects will invoke the functor.     // Note that argument types must be paired with numbers because the     // implementation is based on inheritance, and one class cannot     // inherit the same base class twice.     detail::invoker<detail::indexed_type<Is, typename nth_type_of<Is, Ts...>::type>...> invoker(         f,         (nth_value_of<Is>(std::forward<Ts>(args)...))...         ); }  // The functor provided in the first argument is invoked for each // argument in the pack template<typename F, typename... Ts> void for_each_in_arg_pack(F&& f, Ts&&... args) {     for_each_in_arg_pack_subset(f, index_range<0, sizeof...(Ts)>(), std::forward<Ts>(args)...); }  // The functor provided in the first argument is given in input the // arguments in whose index is contained in the index list specified // as the second argument. template<typename F, unsigned... Is, typename... Ts> void forward_subpack(F&& f, index_list<Is...> const& i, Ts&&... args) {     f((nth_value_of<Is>(std::forward<Ts>(args)...))...); }  // The functor provided in the first argument is given in input all the // arguments in the pack. template<typename F, typename... Ts> void forward_pack(F&& f, Ts&&... args) {     f(std::forward<Ts>(args)...); } 

CONCLUSION

Of course, even though I provided my own answer to this question (and actually because of this fact), I am curious to hear if alternative or better solutions exist which I have missed - apart from the ones mentioned in the "Related Works" section of the question.

like image 152
Andy Prowl Avatar answered Sep 23 '22 07:09

Andy Prowl


Let me post this code, based on the discussion:

#include <initializer_list> #define EXPAND(EXPR) std::initializer_list<int>{((EXPR),0)...}  // Example of use: #include <iostream> #include <utility>  void print(int i){std::cout << "int: " << i << '\n';} int print(double d){std::cout << "double: " << d << '\n';return 2;}  template<class...T> void f(T&&...args){   EXPAND(print(std::forward<T>(args))); }  int main(){   f();   f(1,2.,3); } 

I checked the generated code with g++ -std=c++11 -O1 and main only contains 3 calls to print, there is no trace of the expansion helpers.

like image 32
Marc Glisse Avatar answered Sep 22 '22 07:09

Marc Glisse