1

In an attempt to rewrite a predicate combinator like this

auto constexpr all = [](auto const&... predicates){
    return [predicates...](auto const&... x){
        return (predicates(x...) && ...);
    };
};

(little generalization of this) in a way that it would give meaningful errors when fed with non-predicates/predicates with different arities/arguments, I started writing something like this:

template<typename T, typename = void>
struct IsPredicate : public std::false_type {};

template<typename T>
struct IsPredicate<T, std::enable_if_t<std::is_same_v<bool, return_type_of_callable_T>, void>>
  : public std::true_type {};

and then I stared at it for a while... How do I even check what is the return type of a function, if I don't even know how to call it?

I see this:

  • I couldn't even pass decltype(overloaded_predicate_function) to IsPredicate, because template type deduction can't occur with an overloaded name,
  • even if I only talk of function objects, the problem of the first bullet point could apply to operator(), in case it is overloaded.

So my question is: is it even possible to determine the return type of an arbitrary callable?

I'm mostly interested in a C++17 answer, but, why not?, I'd also like to know what C++20's concept offer in this respect.

Enlico
  • 23,259
  • 6
  • 48
  • 102

3 Answers3

4

So my question is: is it even possible to determine the return type of an arbitrary callable?

No. You can only do this in very narrow circumstances:

  • the callable is a pointer to member data / pointer to member function
  • the callable is a pointer/reference to function
  • the callable is a function object with a single non-overloaded function call operator that is not a template and no conversion functions to function pointers/reference

That's it. If you have a function object whose call operator is either overloaded or a template, you can't really figure out what its return type is. Its return type could depend on its parameter type, and you may not have a way of knowing what the parameter types could be. Maybe it's a call operator template that only accepts a few specific types that you have no way of knowing about, but it is a predicate for those types?

The best you can do is defer checking until you know what what arguments are. And then C++20 already has the concept for you (predicate):

inline constexpr auto all = []<typename... Ps>(Ps const&... predicates){
    return [=]<typename... Xs>(Xs const&... x)
        requires (std::predicate<Ps const&, Xs const&...> && ...)
    {
        return (std::invoke(predicates, x...) && ...);
    };
};

Note that you should use std::invoke to allow for pointers to members as predicates as well (and this is what std::predicate checks for).

T.C.
  • 133,968
  • 17
  • 288
  • 421
Barry
  • 286,269
  • 29
  • 621
  • 977
  • Complementing my question, I would say that as regards those ambiguities (_return type could depend on..._), I guess that I would be interested in determining if the specific `name` is a predicate, regardless of how it's called, i.e. if it returns a `bool` in every single one of its instantiations/overloads. Maybe this is just a non-sense requirement because a client code could, for instance, specialize a template with a non-`bool` return type? – Enlico Apr 05 '21 at 19:39
  • 1
    @Enlico You can't know that without actually potentially instantiating templates, which may fail. – Barry Apr 05 '21 at 20:03
1

You cannot determine the return type of a callable without specifying the argument types (usually done by providing actual arguments) because the return type could depend on the argument types. "decltype(potential_predicate)" won't work but "decltype(potential_predicate(args...))" is another matter.

The first will be the type of the callable itself (whether pointer-to-function or a class type or whatever) while the second will produce the return type of the callable expression.

SoronelHaetir
  • 14,104
  • 1
  • 12
  • 23
0

Can't give you a C++17 answer, but since you also asked for concepts:

The requires expression states that the () operator is overloaded and returns a bool. I think a predicate in the classical sense takes two arguments, but the concept can be easily extended to fo fulfill that requirement as well.

template<typename T>
concept IsPredicate =
    requires(T a) {
        { a() } -> std::same_as<bool>;
    };
flowit
  • 1,382
  • 1
  • 10
  • 36
  • There's no classical sense needed here. The predicate "is positive?" is unary, the predicate is "are equal?" is (at least) binary, the predicate "are these 3D vectors coplanar?" is (at least) ternary. Predicate is anything returning a bool, which expresses whether or not a condition on its arguments is verified or not. – Enlico Apr 05 '21 at 19:32
  • @flowit your `concept` definition is incorrectly using a pre-standardization syntax. The proper C++20 syntax for a `concept` definition is not able to specify the exact type being returned from an expression (e.g. `-> bool`), it can only specify a `concept` that it satisfies (e.g. `-> std::same_as` or `-> std::convertible_to`) – Human-Compiler Apr 05 '21 at 23:47