Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Checking callable template parameter types

EDIT: The approach outlined in the question is problematic for a few reasons. In the end I solved this by going about it a different way, see my answer below.

I have some template classes where the template parameter is expected to be a callable matching a certain signature. If the user supplies a template argument which is either not callable or does not match the expected signature then compilation fails deep inside the callback machinery and the resulting error messages are very hard to decipher. Instead, I'd like to be able to use static_assert to provide a nice, easy to understand error message up front if the given template parameter is invalid. Unfortunately, this seems rather difficult to do.

I'm using the following setup:

#include <type_traits>

namespace detail {

template <typename Function, typename Sig>
struct check_function
{
    static constexpr bool value =
        std::is_convertible<Function, typename std::decay<Sig>::type>::value;
};

template <typename, typename>
struct check_functor;

template <typename Functor, typename Ret, typename... Args>
struct check_functor<Functor, Ret(Args...)>
{
    typedef Ret (Functor::*Memfn) (Args...);

    static constexpr bool value =
        check_function<decltype(&Functor::operator()), Memfn>::value;
};

} // end namespace detail

template <typename Func, typename Sig>
struct check_function_signature
{
    using Type =
        typename std::conditional<
                     std::is_function<Func>::value,
                     detail::check_function<Func, Sig>,
                     detail::check_functor<Func, Sig>>::type;

    static constexpr bool value = Type::value;
};

i.e. if Func is a function pointer type, it is directly compared to the required signature, and otherwise it is assumed to be a functor and its operator() is compared instead.

This seems to work for simple functions and user-defined functors, but for some reason I can't understand it fails for lambdas (tested with Clang 3.4 and g++ 4.8):

int f(int i);

struct g
{
    int operator() (int i) { return i; }
};

int main()
{
    static_assert(check_function_signature<decltype(f), int(int)>::value,
                  "Fine");

    static_assert(check_function_signature<g, int(int)>::value,
                  "Also fine");

    auto l = [](int i) { return i; };
    static_assert(check_function_signature<decltype(l), int(int)>::value,
                  "Fails");
}

My understanding is that the standard requires that lambdas are implemented as anonymous functors equivalent to g above, so I can't understand why the former works but the latter does not.

So, in summary, my questions:

  • Is the approach I've used here actually reasonable, or have I made an obvious mistake?
  • Why does this seem to work for user-defined functors but fail for compiler-defined ones (i.e. lambdas)?
  • Is there a fix/workaround so that lambdas can be checked in this manner?
  • Are there any other improvements I could make to this code? (Probably lots...)

Thanks in advance, this is pushing the limits of my template metaprogramming knowledge so any advice would be gratefully received.

like image 760
Tristan Brindle Avatar asked Sep 01 '14 09:09

Tristan Brindle


2 Answers

(Answering my own question as I figured out better way to achieve what I wanted and thought I'd share it.)

Based on the responses, in particular the linked answer in remaybel's comment, I ended up with a massive amount of code that stripped away the class type from a functor's operator(), and checked each argument type against the required signature. However, this turned out to not work too well, as the requirement to get a member pointer to T::operator() meant it failed if there were multiple overloads of operator() or if it was defined as a template. I'm also not sure it handled argument conversions correctly in all cases, and there are lots of things that are tricky to get right.

After thinking about it some more, I realised what I actually wanted to do was to try to construct a function call with my required argument types, and fail if such a call couldn't be made. A little bit of hacking later, and I came up with this:

template <typename, typename, typename = void>
struct check_signature : std::false_type {};

template <typename Func, typename Ret, typename... Args>
struct check_signature<Func, Ret(Args...),
    typename std::enable_if<
        std::is_convertible<
            decltype(std::declval<Func>()(std::declval<Args>()...)),
            Ret
        >::value, void>::type>
    : std::true_type {};

This constructs a "dummy" function call using declval for both the callable itself and the arguments, and checks that the result can be converted to the required type. If such a call is invalid, SFINAE kicks in and the partial specialisation is rejected.

This is shorter and (IMO) a lot more elegant than what I was trying to do before. It also works correctly for every callable I've tried to throw at it.

Nonetheless, as I said in the original question this is pushing the limits of my metaprogramming knowledge, so if there are any suggestions for how I can improve this code then please let me know.

like image 124
Tristan Brindle Avatar answered Sep 30 '22 07:09

Tristan Brindle


You miss the const specifier in operator ().

With:

template <typename Functor, typename Ret, typename... Args>
struct check_functor<Functor, Ret(Args...)>
{
    typedef Ret (Functor::*Memfn) (Args...) const; // const added here

    static constexpr bool value =
        check_function<decltype(&Functor::operator()), Memfn>::value;
};

The check is correct for the (non mutable) lambda (but not for your custom mutable functor). Else you have to make your lambda mutable:

auto l = [](int i) mutable { return i; };
like image 27
Jarod42 Avatar answered Sep 30 '22 07:09

Jarod42