Search code examples
c++templateslambdasfinae

SFINAE error in a trait to find the return type of any callable


I am playing around with SFINAE and trying, as an exercise, to get information about "function-like" objects (functions, function pointers, lambdas, and anything overloading the parenthesis operator). I've stripped my code down to the following:

#include <iostream>
#include <functional>
#include <string>
#include <type_traits>

template<typename Callable, typename = void>
struct function_type_info;

template<typename R, typename G, typename... T>
struct function_type_info<R(G::*)(T...)> {
    using return_type = R;
};

template<typename C>
struct function_type_info<C, typename std::enable_if<!std::is_member_function_pointer<C>::value, C>::type> {
    using return_type = typename function_type_info<decltype(&C::operator())>::return_type;
};

struct Callback {
    int operator()(double, std::string) {
        return 0;
    }
};

int main(int argc, char** argv) {
    auto b = [](double, std::string) -> int { return 0; };
    auto c = Callback{};

    std::cout << std::boolalpha;

    std::cout << std::is_same<function_type_info<decltype(&Callback::operator())>::return_type, int>::value << std::endl;
    std::cout << std::is_same<function_type_info<decltype(b)>::return_type, int>::value << std::endl;
    std::cout << std::is_same<function_type_info<decltype(c)>::return_type, int>::value << std::endl;

    return 0;
}

The compiler gives me the following errors:

<source>:32:64: error: incomplete type 'function_type_info<main(int, char**)::<lambda(double, std::string)> >' used in nested name specifier
   32 |     std::cout << std::is_same<function_type_info<decltype(b)>::return_type, int>::value << std::endl;
      |                                                                ^~~~~~~~~~~
<source>:32:80: error: template argument 1 is invalid
   32 |     std::cout << std::is_same<function_type_info<decltype(b)>::return_type, int>::value << std::endl;
      |                                                                                ^
<source>:33:64: error: incomplete type 'function_type_info<Callback>' used in nested name specifier
   33 |     std::cout << std::is_same<function_type_info<decltype(c)>::return_type, int>::value << std::endl;
      |                                                                ^~~~~~~~~~~
<source>:33:80: error: template argument 1 is invalid
   33 |     std::cout << std::is_same<function_type_info<decltype(c)>::return_type, int>::value << std::endl;

I cannot figure out why I'm getting this error. With lambdas and Callback objects, the second specialization of function_type_info should be accepted: std::is_member_function_pointer<C>::value is false, so !std::is_member_function_pointer<C>::value is true, so std::enable_if<!std::is_member_function_pointer<C>::value, C>::type should evaluate to C and the template deduction should succeed.

If I change std::is_member_function_pointer<C>::value, C>::type into std::is_member_function_pointer<C>::value, void>::type then is works for Callback, but not for lambdas. Again, I'm at a loss as to why this change makes it work for Callback in the first place, and why, if it works for Callbacks, it doesn't for lambdas. The error for lambdas is the following:

<source>: In instantiation of 'struct function_type_info<main(int, char**)::<lambda(double, std::string)> >':
<source>:32:62:   required from here
<source>:16:11: error: invalid use of incomplete type 'struct function_type_info<int (main(int, char**)::<lambda(double, std::string)>::*)(double, std::__cxx11::basic_string<char>) const, void>'
   16 |     using return_type = typename function_type_info<decltype(&C::operator())>::return_type;
      |           ^~~~~~~~~~~
<source>:7:8: note: declaration of 'struct function_type_info<int (main(int, char**)::<lambda(double, std::string)>::*)(double, std::__cxx11::basic_string<char>) const, void>'
    7 | struct function_type_info;
      |        ^~~~~~~~~~~~~~~~~~
<source>: In function 'int main(int, char**)':
<source>:32:80: error: template argument 1 is invalid
   32 |     std::cout << std::is_same<function_type_info<decltype(b)>::return_type, int>::value << std::endl;

It's as if I was not allowed to refer to the lambda's parenthesis operator...

Note: I know I can get return type information through other ways (std::result_of/std::invoke_result), but as I said, I'm doing this as an exercise.


Solution

  • You have two separate problems in your code. Here is a working version.

    Problem A

    The primary template uses

    template<typename Callable, typename = void>
    struct function_type_info;
    

    The specialization is defined as:

    template<typename C>
    struct function_type_info<C, typename std::enable_if<!std::is_member_function_pointer<C>::value, C>::type> {
        using return_type = typename function_type_info<decltype(&C::operator())>::return_type;
    };
    

    The mistake is providing C as the second template argument to std::enable_if. You only ever use specializations of function_type_info where the second parameter is void, not C.

    To fix this, write:

    std::enable_if<!std::is_member_function_pointer<C>::value, void>
    // or simply
    std::enable_if<!std::is_member_function_pointer<C>::value>
    

    Problem B

    The call operator of lambda expressions is const by default, so you need another specialization to cover that:

    template<typename R, typename G, typename... T>
    struct function_type_info<R(G::*)(T...) const, void> {
        using return_type = R;
    };
    

    Then, define 46 more partial specializations to cover all combinations of const, volatile, noexcept, ref-qualifiers, and variadic arguments.

    Here is a compact version:

    template<typename>struct pointer_to_member_function_traits;
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=S&;using function_type=R(A...);using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=S&;using function_type=R(A...)noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const S&;using function_type=R(A...)const;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const S&;using function_type=R(A...)const noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)volatile>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=volatile S&;using function_type=R(A...)volatile;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)volatile noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=volatile S&;using function_type=R(A...)volatile noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const volatile>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const volatile S&;using function_type=R(A...)const volatile;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const volatile noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const volatile S&;using function_type=R(A...)const volatile noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=S&;using function_type=R(A...)&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=S&;using function_type=R(A...)&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const S&;using function_type=R(A...)const&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const S&;using function_type=R(A...)const&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)volatile&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=volatile S&;using function_type=R(A...)volatile&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)volatile&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=volatile S&;using function_type=R(A...)volatile&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const volatile&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const volatile S&;using function_type=R(A...)const volatile&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const volatile&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const volatile S&;using function_type=R(A...)const volatile&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)&&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=S&&;using function_type=R(A...)&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)&&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=S&&;using function_type=R(A...)&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const&&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const S&&;using function_type=R(A...)const&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const&&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const S&&;using function_type=R(A...)const&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)volatile&&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=volatile S&&;using function_type=R(A...)volatile&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)volatile&&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=volatile S&&;using function_type=R(A...)volatile&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const volatile&&>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const volatile S&&;using function_type=R(A...)const volatile&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A...)const volatile&&noexcept>{constexpr static bool is_variadic=false;using class_type=S;using reference_type=const volatile S&&;using function_type=R(A...)const volatile&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=S&;using function_type=R(A......);using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=S&;using function_type=R(A......)noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const S&;using function_type=R(A......)const;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const S&;using function_type=R(A......)const noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)volatile>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=volatile S&;using function_type=R(A......)volatile;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)volatile noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=volatile S&;using function_type=R(A......)volatile noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const volatile>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const volatile S&;using function_type=R(A......)const volatile;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const volatile noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const volatile S&;using function_type=R(A......)const volatile noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=S&;using function_type=R(A......)&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=S&;using function_type=R(A......)&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const S&;using function_type=R(A......)const&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const S&;using function_type=R(A......)const&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)volatile&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=volatile S&;using function_type=R(A......)volatile&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)volatile&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=volatile S&;using function_type=R(A......)volatile&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const volatile&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const volatile S&;using function_type=R(A......)const volatile&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const volatile&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const volatile S&;using function_type=R(A......)const volatile&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)&&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=S&&;using function_type=R(A......)&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)&&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=S&&;using function_type=R(A......)&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const&&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const S&&;using function_type=R(A......)const&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const&&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const S&&;using function_type=R(A......)const&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)volatile&&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=volatile S&&;using function_type=R(A......)volatile&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)volatile&&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=volatile S&&;using function_type=R(A......)volatile&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const volatile&&>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const volatile S&&;using function_type=R(A......)const volatile&&;using return_type=R;using argument_types=std::tuple<A...>;};
    template<typename R,typename S,typename...A>struct pointer_to_member_function_traits<R(S::*)(A......)const volatile&&noexcept>{constexpr static bool is_variadic=true;using class_type=S;using reference_type=const volatile S&&;using function_type=R(A......)const volatile&&noexcept;using return_type=R;using argument_types=std::tuple<A...>;};
    

    Or use this code from wreien/lazy-query, which is slightly less insane because it uses macros to generate these 48 combinations.

    This is obviously half-serious. It just demonstrates why it's a bit silly to get the return type directly from the member function pointer type instead of using std::invoke_result.