Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to overload variadic templates when they're not the last argument

Basically the problem can be summed up with this example:

template <typename ...Us>
void foo(Us...) { std::cout << "A\n"; }

template <typename ...Us>
void foo(Us..., int) { std::cout << "B\n"; }

int main(){
  foo(1,2,3);
}

This calls the first foo (prints A). How can I get it to call the second foo?

If this used a non-variadic template, or if the "int" was the first argument, then the overload rules would call the right function. That is, a specific type (int) is a better match than a template so it would call the second foo. But apparently that's not the case with variadic templates? Is there any way to overload a variadic template when it's not the last argument?

like image 686
JoshG79 Avatar asked Nov 07 '16 22:11

JoshG79


3 Answers

When a parameter pack doesn't appear last in the parameter declaration, it is a non-deduced context. A non-deduced context means that the template arguments have to be given explicitly. This is why foo #1 is a better overload. You can force the second overload call by providing explicit arguments (foo<int,int>(1,2,3)) or as you said, move the int to the front.

To make things clear, you can overload a function with variadic templates, but when they do not appear as the last argument, they cannot be deduced, which automatically disqualifies them as candidates when explicit arguments are not provided. When they are provided, the template parameters are replaced with their provided types and the resulting non-template function is a candidate in overload resolution.

To answer your question, you can put all arguments into a tuple and pick out the last and test that one. Then pass on an overload based on a simple is_same check:

template<class...Us>
void foo_impl(true_type,Us...); // last argument is int
template<class...Us>
void foo_impl(false_type,Us...); // last argument non-int

template<class...Us>
void foo( Us&&...us ) {
  using tuple=tuple<Us...>;
  using last=decltype(get<sizeof...(Us)-1>(declval<tuple>()));
  foo_impl(is_same<decay_t<last>,int>{}, forward<Us>(us)...);
}
like image 165
David G Avatar answered Nov 04 '22 10:11

David G


You could use SFINAE with some std::tuple functionality help (c++11 capable code):

#include <type_traits>
#include <tuple>
#include <iostream>

template <typename ...Us>
typename std::enable_if<!std::is_same<typename std::tuple_element<sizeof...(Us) - 1, std::tuple<Us...>>::type, int>::value>::type foo(Us...) { 
   std::cout << "A\n"; 
}

template <typename ...Us>
typename std::enable_if<std::is_same<typename std::tuple_element<sizeof...(Us) - 1, std::tuple<Us...>>::type, int>::value>::type foo(Us...) { 
    std::cout << "B\n"; 
}

int main(){
  foo(1,2,3);
}

Output:

B

If you want it to test if some other parameter from pack is of a given type just change std::tuple_element first parameter to desired index value.

[live demo]


If you wish to also utilize other parameters form parameter pack e.g. by recursive call, then your in bigger trouble... c++11 does not come with the functionality of creating index pack. You either need to implement that functionality yourself

#include <tuple>
#include <utility>
#include <iostream>
#include <initializer_list>

template <class T, T... Vs>
struct integer_sequence { };

template <class T, class, class, class = integer_sequence<T>, class = integer_sequence<T, 0>, class = void>
struct make_integer_sequence_impl;

template <class T, T ICV1, T... Res, T... Pow>
struct make_integer_sequence_impl<T, std::integral_constant<T, ICV1>, std::integral_constant<T, 0>, integer_sequence<T, Res...>, integer_sequence<T, Pow...>, typename std::enable_if<(ICV1 > 0)>::type>: make_integer_sequence_impl<T, std::integral_constant<T, ICV1/2>, std::integral_constant<T, ICV1%2>, integer_sequence<T, Res...>, integer_sequence<T, Pow..., (Pow + sizeof...(Pow))...>> { };

template <class T, T ICV1, T... Res, T... Pow>
struct make_integer_sequence_impl<T, std::integral_constant<T, ICV1>, std::integral_constant<T, 1>, integer_sequence<T, Res...>, integer_sequence<T, Pow...>, void>: make_integer_sequence_impl<T, std::integral_constant<T, ICV1/2>, std::integral_constant<T, ICV1%2>, integer_sequence<T, Pow..., (Res + sizeof...(Pow))...>, integer_sequence<T, Pow..., (Pow + sizeof...(Pow))...>> { };

template <class T, class Res, class Pow>
struct make_integer_sequence_impl<T, std::integral_constant<T, 0>, std::integral_constant<T, 0>, Res, Pow, void> {
   using type = Res;
};

template <class T, T V>
using make_integer_sequence = typename make_integer_sequence_impl<T, std::integral_constant<T, V/2>, std::integral_constant<T, V%2>>::type;

template <size_t V>
using make_index_sequence = make_integer_sequence<size_t, V>;

template <size_t... V>
using index_sequence = integer_sequence<size_t, V...>;

void foo() { }

template <typename ...Us>
void foo(Us... us);

template <typename ...Us, std::size_t... Is>
typename std::enable_if<!std::is_same<typename std::tuple_element<sizeof...(Us) - 1, std::tuple<Us...>>::type, int>::value>::type foo_impl(index_sequence<Is...>, Us... us) { 
    std::cout << std::get<sizeof...(Us) - 1>(std::forward_as_tuple(us...)) << "A\n";  
   foo(std::get<Is>(std::forward_as_tuple(us...))...);
}

template <typename ...Us, std::size_t... Is>
typename std::enable_if<std::is_same<typename std::tuple_element<sizeof...(Us) - 1, std::tuple<Us...>>::type, int>::value>::type foo_impl(index_sequence<Is...>, Us... us) { 
    std::cout << std::get<sizeof...(Us) - 1>(std::forward_as_tuple(us...)) << "B\n"; 
   foo(std::get<Is>(std::forward_as_tuple(us...))...);
}

template <typename ...Us>
void foo(Us... us) {
    foo_impl(make_index_sequence<sizeof...(Us) - 1>{}, std::forward<Us>(us)...);
}

int main(){
  foo(1,2,3);
}

[live demo]

or reconsider parameters access pattern:

#include <iostream>

void foo() { }

template <typename Other, typename ...Us>
void foo(Other first, Us... rest) { 
    foo(rest...);
    std::cout << first << "A\n"; 
}

template <typename ...Us>
void foo(int first, Us... rest) { 
    foo(rest...);
    std::cout << first << "B\n"; 
}

int main(){
  foo(1,2,3);
}

[live demo]

like image 28
W.F. Avatar answered Nov 04 '22 10:11

W.F.


The other answers are very heavy in terms of compiling baggage (std::tuple is a very complex template), so, let me show you an straightforward way.

First, I have solved this very problem, I needed to preserve the order of the arguments because they were generated somewhere else, and in general changing the order of arguments is busy work.

Overloading is very powerful, but it makes automatically deducting types of arguments harder; that is one reason why there are no partial specializations of template functions. For the purposes of your user interface, you have to make it so that you have a wrapper that decides whether the types end in int or not. Hence you need a type trait for that:

template<typename T, typename... Ts>
struct LastIsInt{
    constexpr static bool value = LastIsInt<Ts...>::value;
};

template<typename T>
struct LastIsInt<T> {
    constexpr static bool value = false;
};

template<>
struct LastIsInt<int> {
    constexpr static bool value = true;
};

Your choice with respect to an empty type parameter pack.

Then, you can:

template<typename... Args> void foo_of_int(Args &&...);
template<typename... Args> void foo_of_non_int(Args &&...);

template<typename... Args>
void foo(Args &&...args) {
    if(LastIsInt<Args...>::value) {
        foo_of_int(std::forward<Args>(args)...);
    } else {
        foo_of_non_int(std::forward<Args>(args)...);
    }
}
like image 2
TheCppZoo Avatar answered Nov 04 '22 08:11

TheCppZoo