How to check if template argument is a callable with a given signature
Asked Answered
M

7

29

Basically, what I want to achieve is compile-time verification (with possibly nice error message) that registered callable (either a function, a lambda, a struct with call operator) has correct signature. Example (contents of the static_assert are to be filled):

struct A {
  using Signature = void(int, double);

  template <typename Callable>
  void Register(Callable &&callable) {
    static_assert(/* ... */);
    callback = callable;
  }

  std::function<Signature> callback;
};
Mcquiston answered 7/12, 2017 at 15:36 Comment(2)
Do you want an identical signature, or a compatible signature? For example, if your signature is bool(), would you accept type int(*)(), i.e. function pointer to function taking no arguments and returning integer? The integer will implicitly convert bool so it is compatible, but it is not identical.Charolettecharon
That's a good question - I would prefer to have as strong typing as possible, so for my use-case it would be better to reject int(*)() in this case.Mcquiston
C
13

Most of the answers are focused on basically answering the question: can you call the given function object with values of these types. This is not the same as matching the signature, as it allows many implicit conversions that you say you don't want. In order to get a more strict match we have to do a bunch of TMP. First, this answer: Call function with part of variadic arguments shows how to get the exact types of the arguments and return type of a callable. Code reproduced here:

template <typename T>
struct function_traits : public function_traits<decltype(&T::operator())>
{};

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
{
    using result_type = ReturnType;
    using arg_tuple = std::tuple<Args...>;
    static constexpr auto arity = sizeof...(Args);
};

template <typename R, typename ... Args>
struct function_traits<R(&)(Args...)>
{
    using result_type = R;
    using arg_tuple = std::tuple<Args...>;
    static constexpr auto arity = sizeof...(Args);
};

Having done that, you can now put a series of static asserts in your code:

struct A {
  using Signature = void(int, double);

  template <typename Callable>
  void Register(Callable &&callable) {
    using ft = function_traits<Callable>;
    static_assert(std::is_same<int,
        std::decay_t<std::tuple_element_t<0, typename ft::arg_tuple>>>::value, "");
    static_assert(std::is_same<double,
        std::decay_t<std::tuple_element_t<1, typename ft::arg_tuple>>>::value, "");
    static_assert(std::is_same<void,
        std::decay_t<typename ft::result_type>>::value, "");

    callback = callable;
  }

  std::function<Signature> callback;
};

Since you are passing by value this is basically all you need. If you are passing by reference, I would add an additional static assert where you use one of the other answers; probably songyuanyao's answer. This would take care of cases where for example the base type was the same, but the const qualification went in the wrong direction.

You could of course make this all generic over the type Signature, instead of doing what I do (simply repeating the types in the static assert). This would be nicer but it would have added even more complex TMP to an already non-trivial answer; if you feel like you will use this with many different Signatures or that it is changing often it is probably worth adding that code as well.

Here's a live example: http://coliru.stacked-crooked.com/a/cee084dce9e8dc09. In particular, my example:

void foo(int, double) {}
void foo2(double, double) {}

int main()
{
    A a;
    // compiles
    a.Register([] (int, double) {});
    // doesn't
    //a.Register([] (int, double) { return true; });
    // works
    a.Register(foo);
    // doesn't
    //a.Register(foo2);
}
Charolettecharon answered 7/12, 2017 at 16:53 Comment(0)
L
18

C++20 with concepts

Since few years already, we have concepts that make such checks easy. This case is straight usage of std::invocable.


#include <concepts>

struct B {
using Signature = void(int, double);

template <typename Callable>
void Register(Callable &&callable) 
    requires std::invocable<Callable, int, double>
{
    callback = callable;
}

std::function<Signature> callback;
};

C++17

In C++17 there is trait std::is_invocable<Callable, Args...>, which does exactly what you ask for. Its advantage over std::is_convertible<std::function<Signature>,...> is that you don't have to specify return type.

It might sound like overkill but I had encountered problem that had to use it. Exactly my wrapper function deduced its return type from passed Callable, but I've passed templated lambda like this one [](auto& x){return 2*x;}, so return type of it was deduced in subcall. I couldn't convert it into std::function and I ended up using local implementation of is_invocable for C++14.

Backport for C++14

I cannot find the to credit the original author. Anyway, the code:

template <class F, class... Args>
struct is_invocable
{
    template <class U>
    static auto test(U* p) -> decltype((*p)(std::declval<Args>()...), void(), std::true_type());
    template <class U>
    static auto test(...) -> decltype(std::false_type());

    static constexpr bool value = decltype(test<F>(0))::value;
};

and for your example:

struct A {
using Signature = void(int, double);

template <typename Callable>
void Register(Callable &&callable) {
    static_assert(is_invocable<Callable,int,double>::value, "not foo(int,double)");
    callback = callable;
}

std::function<Signature> callback;
};
Lobe answered 7/12, 2017 at 16:13 Comment(0)
J
17

You can use std::is_convertible (since C++11), e.g.

static_assert(std::is_convertible_v<Callable&&, std::function<Signature>>, "Wrong Signature!");

or

static_assert(std::is_convertible_v<decltype(callable), decltype(callback)>, "Wrong Signature!");

LIVE

Jorum answered 7/12, 2017 at 15:43 Comment(2)
This is a good technique, but not clear if it's exactly what the OP wants, std::function I think is quite a bit more permissive, only requires a "compatible" signature and not an identical signature.Charolettecharon
I really like this for the simplicity, however I'm going to choose Nir Friedman's answer due to stricter arguments checking.Mcquiston
C
13

Most of the answers are focused on basically answering the question: can you call the given function object with values of these types. This is not the same as matching the signature, as it allows many implicit conversions that you say you don't want. In order to get a more strict match we have to do a bunch of TMP. First, this answer: Call function with part of variadic arguments shows how to get the exact types of the arguments and return type of a callable. Code reproduced here:

template <typename T>
struct function_traits : public function_traits<decltype(&T::operator())>
{};

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
{
    using result_type = ReturnType;
    using arg_tuple = std::tuple<Args...>;
    static constexpr auto arity = sizeof...(Args);
};

template <typename R, typename ... Args>
struct function_traits<R(&)(Args...)>
{
    using result_type = R;
    using arg_tuple = std::tuple<Args...>;
    static constexpr auto arity = sizeof...(Args);
};

Having done that, you can now put a series of static asserts in your code:

struct A {
  using Signature = void(int, double);

  template <typename Callable>
  void Register(Callable &&callable) {
    using ft = function_traits<Callable>;
    static_assert(std::is_same<int,
        std::decay_t<std::tuple_element_t<0, typename ft::arg_tuple>>>::value, "");
    static_assert(std::is_same<double,
        std::decay_t<std::tuple_element_t<1, typename ft::arg_tuple>>>::value, "");
    static_assert(std::is_same<void,
        std::decay_t<typename ft::result_type>>::value, "");

    callback = callable;
  }

  std::function<Signature> callback;
};

Since you are passing by value this is basically all you need. If you are passing by reference, I would add an additional static assert where you use one of the other answers; probably songyuanyao's answer. This would take care of cases where for example the base type was the same, but the const qualification went in the wrong direction.

You could of course make this all generic over the type Signature, instead of doing what I do (simply repeating the types in the static assert). This would be nicer but it would have added even more complex TMP to an already non-trivial answer; if you feel like you will use this with many different Signatures or that it is changing often it is probably worth adding that code as well.

Here's a live example: http://coliru.stacked-crooked.com/a/cee084dce9e8dc09. In particular, my example:

void foo(int, double) {}
void foo2(double, double) {}

int main()
{
    A a;
    // compiles
    a.Register([] (int, double) {});
    // doesn't
    //a.Register([] (int, double) { return true; });
    // works
    a.Register(foo);
    // doesn't
    //a.Register(foo2);
}
Charolettecharon answered 7/12, 2017 at 16:53 Comment(0)
F
3

This is somehow another version of @R2RT 's answer when you can use C++17. We can use the trait is_invocable_r to do the job:

struct Registry {
  std::function<void(int, double)> callback;

  template <typename Callable, 
      std::enable_if_t<
          std::is_invocable_r_v<void, Callable, int, double>>* = nullptr>
  void Register(Callable callable) {
    callback = callable;
  }
};

int main() {
  Registry r;
  r.Register([](int a, double b) { std::cout << a + b << std::endl; });
  r.callback(35, 3.5);
}

which prints out 38.5

The good part of std::is_invocable_r is that it allows you to control return type along with argument types while std::is_invocable is only on callable argument types.

Fundamental answered 12/3, 2021 at 18:58 Comment(0)
L
2

If you accept to transform A in a variadic template class, you can use decltype(), to activare Register only if callable is compatible, as follows

template <typename R, typename ... Args>
struct A
 {
   using Signature = R(Args...);

   template <typename Callable>
   auto Register (Callable && callable)
      -> decltype( callable(std::declval<Args>()...), void() )
    { callback = callable; }

   std::function<Signature> callback;
 };

This way, if you prefer, calling Register() with a incompatible function, you can obtain a soft error and activate another Register() function

void Register (...)
 { /* do something else */ };
Laws answered 7/12, 2017 at 15:46 Comment(0)
U
1

You can use the detection idiom, which is a form of sfinae. I believe this works in c++11.

template <typename...>
using void_t = void;

template <typename Callable, typename enable=void>
struct callable_the_way_i_want : std::false_type {};

template <typename Callable>
struct callable_the_way_i_want <Callable, void_t <decltype (std::declval <Callable>()(int {},double {}))>> : std::true_type {};

Then you can write a static assert in your code like so:

static_assert (is_callable_the_way_i_want <Callable>::value, "Not callable with required signature!");

The advantage of this over the answers I see above is:

  • It works for any callable, not just a lambda
  • theres no runtime overhead or std::function business. std::function may cause a dynamic allocation, for instance, that would be otherwise unnecessary.
  • you can actually write a static_assert against the test and put a nice human-readable error message there

Tartan Llama wrote a great blogpost about this technique, and several alternatives, check it out! https://blog.tartanllama.xyz/detection-idiom/

If you need to do this alot then you may want to look at the callable_traits library.

Ursel answered 7/12, 2017 at 16:3 Comment(1)
This is not working when Callable being detected actually requires two int args, since double can be converted to int implicitly, compiler will falsely accept the callable, although it's not of exact signature void(*func)(int, double)Bolin
B
0

Based on @Nir Friedman's answer.

template<typename T>
struct function_traits: public function_traits<decltype(&T::operator())> {};

template<typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const> {
    using signature = ::std::tuple<ReturnType, ClassType, Args...>;
};

template<typename ReturnType, typename ... Args>
struct function_traits<ReturnType(&)(Args...)> {
    using signature = ::std::tuple<ReturnType, void, Args...>;
};

template<typename T> using func_sig = typename function_traits<T>::signature;

template<typename Callable1, typename Callable2>
struct has_same_signature {
    static constexpr bool value = ::std::is_same<func_sig<Callable1>, func_sig<Callable2>>::value;
};

Then you can do some strict check like

int f(int) {
}

int g(double) {
}

template<typename T>
void some_func(T&& a) {
    static_assert(has_same_signature<T, int(&)(int)>::value)
}

int main() {
    some_func(f); // success
    some_func(g); // fail
}
Blowhole answered 9/9, 2022 at 11:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.