How to check whether, given the argument types, an implicit use of `operator ()` would result in exactly one best viable candidate?
Asked Answered
L

0

6

As I understand it, the outcome of a function name usage might be one of the following:

  1. There are no (best) viable functions — overload resolution fails. The suboutcomes are:
    1. There are no candidates.
    2. There are some candidates, just none are viable.
  2. There is exactly one best viable function — overload resolution succeeds. The selected overload is then either
    1. OK — the overall call is well-formed.
    2. not OK (= deleted, protected/private or, perhaps, something else) — the overall call is ill-formed.
  3. There are more than one best viable functions — overload resolution fails with ambiguity.

The question is: How to reliably tell apart outcome #2.2 (at least some of its cases) from outcomes #1.2 and #3 (at least one of them) in the case of implicit usage of operator () (i.e. c(a...)) by means of a type trait that accepts the types of the arguments (including c) to be used in the call?

(I'm not interested in outcomes #1.1 and #2.1 as I know that #1.1 does not hold in my particular use case and #2.1 is easily detectable through SFINAE.)


A specific example. How to implement a type trait that looks something like the following

/// Would `c(a...)` result in exactly one best viable candidate?
/// (Where `decltype(c)`, `decltype(a)...` are `C`, `A...`, respectively.)
template<class C, typename... A>
inline constexpr bool has_exactly_one_best_viable_call_candidate;

so the following asserts hold?

struct WithNoViable {
    void operator ()(void *);
};

struct WithDeleted {
    void operator ()(long) = delete;
};

struct WithAmbiguity {
    void operator ()(long);
    void operator ()(long long);
};

static_assert(!has_exactly_one_best_viable_call_candidate<WithNoViable, int>);
static_assert( has_exactly_one_best_viable_call_candidate<WithDeleted, int>);
static_assert(!has_exactly_one_best_viable_call_candidate<WithAmbiguity, int>);

Note that in general nothing is known about the types of parameters nor arguments.

Langille answered 3/1, 2022 at 13:52 Comment(10)
Haha, you're making it extra difficult by using implicit integer promotion. As if C++ overload resolution isn't confusing enough on it's own. Anyhow, would you need the static_assert? The compilation would just fail if no- or ambiguous overloads are found...Kirstiekirstin
The standard doesn't require any different behavior between ill-formed scenarios (except perhaps the no diagnostic required ones, which is even worse). So I don't think it's possible for you to make a distinction between two scenarios where compilation fails (ambiguity vs deleted or something else)Hawks
@JHBonarius, static_assert is needed just to test the type trait.Langille
There already is std::is_invocable, e.g. static_assert(!std::is_invocable_v<WithNoViable, int>);, but that will not work for the deleted version... way do you want to have the ill-formed succeed?Kirstiekirstin
@AndyG, I could mixed-in an additional operator ()(long) overload through multiple inheritance and then check if the resulting type is callable with long: in case of WithNoViable the call would be OK, but with the other two it would be ambiguous. Perhaps, a similar type-knowledge-independent technique exists to solve my original problem.Langille
Eventually when we get reflection I think that what you want to do will be possible (heck there's already an is_deleted trait).Hawks
@JHBonarius, the bigger picture here is: I'm trying to design a wrapper class as much overload resolution transparent (w.r.t. a callable object wrapped inside it) as possible. I would use the required type trait to SFINAE an overload of Wrapper::operator () to be = deleted.Langille
@AndyG, Yeah, if we had reflection and source code generation, I'd use them to copy each overload from the wrapped class and inject it into the wrapper (with the modification that the wrapper requires).Langille
@OverloadResolver: Wrapper classes are simply very unreasonably hard to write properly in vanilla C++ (especially pre-C++20). I always think back to Sy Brand's talk from CppCon 2018: youtube.com/watch?v=J4A2B9eexiw about this. I was coincidentally working on my own wrappers at the time and came to many of the same conclusions his talk did. I imagine that one day that the MetaClasses proposal will solve all our proxy class woes.Hawks
That said, perhaps you should re-think your approach for now. Instead of attempting to detect the specific failure scenarios in order to dispatch to the proper invocable, perhaps it's best (for now) to simply check whether each invocable would work one after another (perhaps inside of some kind of if constexpr context) and dispatch to the first one that works.Hawks

© 2022 - 2024 — McMap. All rights reserved.