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.

  • 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... – JHBonarius Jan 03 '22 at 14:08
  • 1
    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) – AndyG Jan 03 '22 at 14:15
  • 2
    @JHBonarius, `static_assert` is needed just to test the type trait. – OverloadResolver Jan 03 '22 at 14:17
  • There already is `std::is_invocable`, e.g. `static_assert(!std::is_invocable_v);`, but that will not work for the deleted version... way do you want to have the ill-formed succeed? – JHBonarius Jan 03 '22 at 14:30
  • @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. – OverloadResolver Jan 03 '22 at 14:34
  • 1
    Eventually when we get reflection I think that what you want to do will be possible (heck there's already an `is_deleted` trait). – AndyG Jan 03 '22 at 14:43
  • @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 `= delete`d. – OverloadResolver Jan 03 '22 at 14:45
  • @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). – OverloadResolver Jan 03 '22 at 14:51
  • 1
    @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: https://www.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. – AndyG Jan 03 '22 at 14:55
  • 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. – AndyG Jan 03 '22 at 14:59

0 Answers0