Patrick Wright
Patrick Wright

Reputation: 1663

Overload of variadic template function with another template

I am trying to figure out how to "overload" a variadic function template with a "more specialized" variadic function template. For example:

#include <iostream>

template <typename... ARGS_>
void foo(void(*fn)(ARGS_...)) {
    std::cout << "Generic function pointer foo" << std::endl;
}

struct Test {
    
};

template <typename... ARGS_>
void foo(void(*fn)(ARGS_..., Test*)) {
    std::cout << "Test function pointer foo" << std::endl;
}


void test1(int a, int b) {
    std::cout << "test1()" << std::endl;
}

void test2(int a, int b, Test* x) {
    std::cout << "test2()" << std::endl;
}

int main() {
    foo(&test1);
    foo(&test2);
    
    return 0;
}

The output of this code is:

Generic function pointer foo
Generic function pointer foo

Rather than:

Generic function pointer foo
Test function pointer foo

as I want.

Conceptually, I am trying to notate "Use template method A if you have any type(s) of arguments where the LAST on is Test* and use template method B if the last type is NOT Test*."

What is the correct method to accomplish this type of behavior?

Upvotes: 6

Views: 541

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275896

Check that the last element of ARGS... is Test*.

It won't do that for you this way. One ways is:

template<class...Ts>
struct last_type {};
template<class T1, class T2, class...Ts>
struct last_type<T1, T2, Ts...>:last_type<T2, Ts...>{};
template<class T>
struct last_type<T>{
  using type=T;
};
template<class...Ts>
using last_type_t = typename last_type<Ts...>::type;

now you just:

template <typename... ARGS_>
requires std::is_same_v<last_type_t<ARGS_...>, Test*>
void foo(void(*fn)(ARGS_...)) {
   std::cout << "Test function pointer foo" << std::endl;
}

Live example.

Without concepts, you have to replace that requires clause:

template <typename... ARGS_,
  std::enable_if_t<std::is_same_v<last_type_t<ARGS_...>, Test*>, bool> = true
>
void foo(void(*fn)(ARGS_...)) {
   std::cout << "Test function pointer foo" << std::endl;
}

which is a more obscure "cargo cult" way to basically say the same thing. (you also need to invert enable if clause in the other overload; not = false but !, and handle 0 arg case (prepend void on the type list?))

The reason why your attempt doesn't work is that C++ makes ... matching insanely greedy. It is generally not a good idea to put things you want pattern matched behind it in a context where pattern matching of parameters is done.

Upvotes: 4

dfrib
dfrib

Reputation: 73216

SFINAE'd overloads based on the last parameter pack argument

You can add mutually exclusive overloads based on whether the last type in the variadiac parameter pack is Test* or not:

#include <type_traits>

template <typename... Ts>
using last_t = typename decltype((std::type_identity<Ts>{}, ...))::type;

struct Test {};

template <
    typename... ARGS_,
    std::enable_if_t<!std::is_same_v<last_t<ARGS_...>, Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
  std::cout << "Generic function pointer foo" << std::endl;
}

template <
    typename... ARGS_,
    std::enable_if_t<std::is_same_v<last_t<ARGS_...>, Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
  std::cout << "Test function pointer foo" << std::endl;
}

// Special case for empty pack (as last_t<> is ill-formed)
void foo(void (*fn)()) { std::cout << "Zero args" << std::endl; }

making use if C++20's std::type_identity for the last_t transformation trait.

Used as:

void test1(int, int b) {}
void test2(int, int b, Test *x) {}
void test3(Test *) {}
void test4() {}

int main() {
  foo(&test1); // Generic function pointer foo
  foo(&test2); // Test function pointer foo
  foo(&test3); // Test function pointer foo
  foo(&test4); // Zero args
}

Avoiding the zero-arg special case as an overload?

The zero-arg foo overload can be avoided in favour of tweaking the last_t trait into one which also accepts an empty pack, such that a query over the empty pack is used to resolve to the generic overload. Neither its semantics nor its implementation becomes as straight-forward and elegant, however, as "the last type in an empty type list" does not make much sense, meaning the trait need to be tweaked into something different:

template <typename... Ts> struct last_or_unique_dummy_type {
  using type = typename decltype((std::type_identity<Ts>{}, ...))::type;
};

template <> class last_or_unique_dummy_type<> {
  struct dummy {};

public:
  using type = dummy;
};

template <typename... Ts>
using last_or_unique_dummy_type_t =
    typename last_or_unique_dummy_type<Ts...>::type;

template <typename... ARGS_,
          std::enable_if_t<!std::is_same_v<
              last_or_unique_dummy_type_t<ARGS_...>, Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
  std::cout << "Generic function pointer foo" << std::endl;
}

template <typename... ARGS_,
          std::enable_if_t<std::is_same_v<last_or_unique_dummy_type_t<ARGS_...>,
                                          Test *>> * = nullptr>
void foo(void (*fn)(ARGS_...)) {
  std::cout << "Test function pointer foo" << std::endl;
}

Using an additional overload for the empty pack is likely the least surprising approach.


C++20 and the identity_t trick

In case you are not yet at C++20, an identity meta-function is trivial to write yourself:

template <typename T>
struct type_identity {
  using type = T;
};

Any specialization of this class template, unless partially/explicitly specialized otherwise (which is UB for the STL type), is trivial and default-constructible. We leverage this in the definition of last_t above: default-constructing a series of trivial types in an unevaluated context, and leveraging that the last of those types embeds the input to the identity trait whose specialization is that trivial type, and whose wrapped alias declaration type is the type of the last parameter in the variadic parameter pack.

Upvotes: 8

Related Questions