Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overload of variadic template function with another template

Tags:

c++

templates

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?

like image 332
Patrick Wright Avatar asked Aug 16 '21 19:08

Patrick Wright


People also ask

Can a template function be overloaded?

You may overload a function template either by a non-template function or by another function template. The function call f(1, 2) could match the argument types of both the template function and the non-template function.

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. However, variadic templates help to overcome this issue.

What is a variadic template?

This means that all functions that have 1 or more arguments are matched to the variadic template and all functions that with no argument are matched to the empty function. This article is contributed by MAZHAR IMAM KHAN.

What is function template overloading?

………. …… ….. ……. where, T is template argument accepting different arguments and class is a keyword. The name of the function templates are the same but called with different arguments is known as function template overloading.

What is function overloading in C++?

A template function can be overloaded either by a non-template function or using an ordinary function template. Function Overloading: In function overloading, the function may have the same definition, but with different arguments. Below is the C++ program to illustrate function overloading:

How to know how many arguments are actually passed in variadic templates?

When using variadic templates you may end up in a situation, where you would like to know how many arguments are actually passed. Let’s say that you want to store them in a table. How big should it be? sizeof... () will tell you: template <typename... Args> void func (Args... args) { int argsTable [sizeof... (args)] = {args...};


Video Answer


2 Answers

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.

like image 200
dfrib Avatar answered Oct 21 '22 14:10

dfrib


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.

like image 31
Yakk - Adam Nevraumont Avatar answered Oct 21 '22 14:10

Yakk - Adam Nevraumont