Search code examples
c++c++17variadic-templatestemplate-meta-programmingtype-traits

Detect if function with given signature exists


I'm trying to use template metaprogramming to find a function (not class method) with a specific signature. And to achieve that, I want to use the detection idiom.

Let's say that I have functions foo() and bar():

void foo(int x);

template<typename... Ts>
void bar(Ts&& ...args);

And another function called foobar() that receives a function and a parameter pack. foobar() checks if the received function can be called with the parameters received through the parameter pack:

template<typename F, typename... Ts>
void foobar(F&& fun, Ts&& ...args) {
    if constexpr(func_with_signature_exists_v<fun(declval<Args>()...)>)
        do_something;
    }

I would expect the following results from the if statement in foobar():

if constexpr(func_with_signature_exists_v<foo(int)>) // true
if constexpr(func_with_signature_exists_v<foo(float)>) // false
if constexpr(func_with_signature_exists_v<foo(int, int)>) // false
if constexpr(func_with_signature_exists_v<bar(int)>) // true
if constexpr(func_with_signature_exists_v<bar(int, int)>) // true
if constexpr(func_with_signature_exists_v<bar(int, float, int)>) // true

I tried to follow the steps from the accepted answer from this link, but the is_detected idiom didn't complain when I tried passing a float to a function expecting an int.

Also, I'd prefer if I could pass any function to the using statement, instead of explicitly specializing it for one specific function, like here:

template<typename... Args>
using test_t = decltype(f(std::declval<Args>()...));

Is there any way to achieve what I'm looking for? Or anything similar to it?

Note that this is for a side project, so I'm trying to use C++17, not C++14 or C++11.


Solution

  • foo() and bar() give completely different problems.

    First of all: bar(), that is a template function.

    Given that you want that bar() is a template function (different if is a template operator() of a class/struct), if you want something as

    static_assert(foobar(bar, 1));
    

    the best i can imagine, is that foobar() should bee a C-style macro; and a variadic one, to make the things more complicated (but don't ask to me to develop that macro).

    This is because you can't pass a template function as function argument because a template function isn't an object but a set of objects.

    For foo(), if I understand correctly, the problem is that the detection idiom say true when an argument is convertible to the function argument

    static_assert(foobar(foo, 3.4)); // you get true but you want false
    

    and that you want to pass the function as an argument.

    This is simple to solve (but I don't think is very useful).

    If you declare (no definition needed) a couple of overloaded functions as follows

    template <typename ... Args, typename R>
    std::true_type fwse (R(*)(Args...), int);
    
    template <typename ..., typename T>
    std::false_type fwse (T, long);
    

    and a constexpr variadic template variable

    template <typename T, typename ... Args>
    constexpr auto func_with_signature_exists_v
       = decltype(fwse<Args...>(std::declval<T>(), 0))::value;
    

    you can also write foobar() as follows

    template <typename F, typename ... Ts>
    constexpr auto foobar (F, Ts const & ...)
     {
       if constexpr ( func_with_signature_exists_v<F, Ts...> )
          return std::true_type{};
       else
          return std::false_type{};
     }
    

    and you get

    static_assert( foobar(foo, 7) == true );
    static_assert( foobar(foo, 3.4) == false );
    static_assert( foobar(foo, 1, 2) == false );
    

    I don't think is very useful because this works when the types for the foo() arguments has to be plain types (no const, no references).

    If you can avoid to pass values to foobar(), and if is OK to pass the Args... types directly

    static_assert( foobar<int>(foo) == true );
    static_assert( foobar<double>(foo) == false );
    static_assert( foobar<int, int>(foo) == false );
    

    you can rewrite foobar() as follows

    template <typename ... Ts, typename F>
    constexpr auto foobar (F)
     {
       if constexpr ( func_with_signature_exists_v<F, Ts...> )
          return std::true_type{};
       else
          return std::false_type{};
     }
    

    and become more flexible (because can accept also const and/or reference types for Ts...).

    The following is a full compiling example

    #include <type_traits>
    
    void foo(int x)
     { }
    
    template <typename ... Args, typename R>
    std::true_type fwse (R(*)(Args...), int);
    
    template <typename ..., typename T>
    std::false_type fwse (T, long);
    
    template <typename T, typename ... Args>
    constexpr auto func_with_signature_exists_v
       = decltype(fwse<Args...>(std::declval<T>(), 0))::value;
    
    template <typename ... Ts, typename F>
    constexpr auto foobar (F)
     {
       if constexpr ( func_with_signature_exists_v<F, Ts...> )
          return std::true_type{};
       else
          return std::false_type{};
     }
    
    int main ()
     {
       static_assert( foobar<int>(foo) == true );
       static_assert( foobar<double>(foo) == false );
       static_assert( foobar<int, int>(foo) == false );
     }