A type trait to detect functors using C++17?
Asked Answered
T

1

9

Problem description:

C++17 introduces std::invocable<F, Args...>, which is nice to detect if a type... is invocable with the given arguments. However, would there be a way to do it for any arguments for functors (because combinations of the existing traits of the standard library already allow to detect functions, function pointers, function references, member functions...)?

In other words, how to implement the following type trait?

template <class F>
struct is_functor {
    static constexpr bool value = /*using F::operator() in derived class works*/;
};

Example of use:

#include <iostream>
#include <type_traits>

struct class0 {
    void f();
    void g();
};

struct class1 {
    void f();
    void g();
    void operator()(int);
};

struct class2 {
    void operator()(int);
    void operator()(double);
    void operator()(double, double) const noexcept;
};

struct class3 {
    template <class... Args> constexpr int operator()(Args&&...);
    template <class... Args> constexpr int operator()(Args&&...) const;
};

union union0 {
    unsigned int x;
    unsigned long long int y;
    template <class... Args> constexpr int operator()(Args&&...);
    template <class... Args> constexpr int operator()(Args&&...) const;
};

struct final_class final {
    template <class... Args> constexpr int operator()(Args&&...);
    template <class... Args> constexpr int operator()(Args&&...) const;
};

int main(int argc, char* argv[]) {
     std::cout << is_functor<int>::value;
     std::cout << is_functor<class0>::value;
     std::cout << is_functor<class1>::value;
     std::cout << is_functor<class2>::value;
     std::cout << is_functor<class3>::value;
     std::cout << is_functor<union0>::value;
     std::cout << is_functor<final_class>::value << std::endl;
     return 0;
}

should output 001111X. In an ideal world, X should be 1, but I don't think it's doable in C++17 (see bonus section).


Edit:

This post seems to present a strategy that solves the problem. However, would there be a better/more elegant way to do it in C++17?


Bonus:

And as a bonus, would there be a way to make it work on final types (but that's completely optional and probably not doable)?

Trollop answered 8/3, 2018 at 6:1 Comment(10)
So you want to see if a type is generally callable, without needing to specify any arguments?Illustrative
Yes, given that the standard library already allow to do it easily for non-functors.Trollop
Why you need this? Whether something is generally callable is not useful information.Lotte
The question is not whether it is useful, but whether it is doable. Plus, it is useful because I am buidling a library to create overload sets, and I need to know if I can inherit from a class and do using F::operator(); inside my class.Trollop
Possible duplicate of find out if a C++ object is callableLotte
@liliscent Oh, that's great! However, would there be a more compact/more elegant way to do it in C++17?Trollop
@liliscent See the EDIT I just added.Trollop
@Trollop dupe vote retracted. However, I doubt it's possible...Lotte
@Trollop You could use a type that converts to any type as in this example but it will fail if a template operator() constrains its template parameter with SFINAE or concepts.Freya
What are you looking for now? "Better/more elegant" is completely vague. Working with final also conflicts with your use case of using F::operator();Lithographer
S
3

Building on my answer to my answer to this qustion, i was able to solve your problem, including the bonus one :-)

The following is the code posted in the other thread plus some little tweaks to get a special value when an object can't be called. The code needs c++17, so currently no MSVC...

#include<utility>

constexpr size_t max_arity = 10;

struct variadic_t
{
};


struct not_callable_t
{
};

namespace detail
{
    // it is templated, to be able to create a
    // "sequence" of arbitrary_t's of given size and
    // hece, to 'simulate' an arbitrary function signature.
    template <size_t>
    struct arbitrary_t
    {
        // this type casts implicitly to anything,
        // thus, it can represent an arbitrary type.
        template <typename T>
        operator T&& ();

        template <typename T>
        operator T& ();
    };

    template <typename F, size_t... Is,
                typename U = decltype(std::declval<F>()(arbitrary_t<Is>{}...))>
    constexpr auto test_signature(std::index_sequence<Is...>)
    {
        return std::integral_constant<size_t, sizeof...(Is)>{};
    }

    template <size_t I, typename F>
    constexpr auto arity_impl(int) -> decltype(test_signature<F>(std::make_index_sequence<I>{}))
    {
        return {};
    }


    template <size_t I, typename F, std::enable_if_t<(I == 0), int> = 0>
    constexpr auto arity_impl(...) {
        return not_callable_t{};
    }

    template <size_t I, typename F, std::enable_if_t<(I > 0), int> = 0>
    constexpr auto arity_impl(...)
    {
        // try the int overload which will only work,
        // if F takes I-1 arguments. Otherwise this
        // overload will be selected and we'll try it 
        // with one element less.
        return arity_impl<I - 1, F>(0);
    }

    template <typename F, size_t MaxArity = 10>
    constexpr auto arity_impl()
    {
        // start checking function signatures with max_arity + 1 elements
        constexpr auto tmp = arity_impl<MaxArity + 1, F>(0);
        if constexpr(std::is_same_v<std::decay_t<decltype(tmp)>, not_callable_t>) {
            return not_callable_t{};
        }
        else if constexpr (tmp == MaxArity + 1)
        {
            // if that works, F is considered variadic
            return variadic_t{};
        }
        else
        {
            // if not, tmp will be the correct arity of F
            return tmp;
        }
    }
}

template <typename F, size_t MaxArity = max_arity>
constexpr auto arity(F&& f) { return detail::arity_impl<std::decay_t<F>, MaxArity>(); }

template <typename F, size_t MaxArity = max_arity>
constexpr auto arity_v = detail::arity_impl<std::decay_t<F>, MaxArity>();

template <typename F, size_t MaxArity = max_arity>
constexpr bool is_variadic_v = std::is_same_v<std::decay_t<decltype(arity_v<F, MaxArity>)>, variadic_t>;

// HERE'S THE IS_FUNCTOR

template<typename T>
constexpr bool is_functor_v = !std::is_same_v<std::decay_t<decltype(arity_v<T>)>, not_callable_t>;

Given the classes in yout question, the following compiles sucessfully (you can even use variadic lambdas:

constexpr auto lambda_func = [](auto...){};

void test_is_functor() {
    static_assert(!is_functor_v<int>);
    static_assert(!is_functor_v<class0>);
    static_assert(is_functor_v<class1>);
    static_assert(is_functor_v<class2>);
    static_assert(is_functor_v<class3>);
    static_assert(is_functor_v<union0>);
    static_assert(is_functor_v<final_class>);
    static_assert(is_functor_v<decltype(lambda_func)>);
}

See also a running example here.

Synonymous answered 22/3, 2018 at 23:31 Comment(3)
This fails with a functor with overloaded function call with its highest arity. It also fails with SFINAE turning off some function calls.Lithographer
That is correct, indeed... Any suggestions how to fix this?Synonymous
No, the linked post is the best I know.Lithographer

© 2022 - 2024 — McMap. All rights reserved.