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:
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?
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.
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.
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.
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:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With