5

Is there a way to detect deleted functions after overload selection (over no viable or ambiguous overloads)?

void foo();
void foo(double) = delete;
void foo(std::string);

void foo(char, int);
void foo(int, char);

static_assert(!foo_is_deleted<>);             // exist as not deleted
static_assert( foo_is_deleted<double>);       // explicitly deleted
static_assert( foo_is_deleted<float>);        // foo(4.2f) selects foo(double) which is deleted.
static_assert(!foo_is_deleted<const char*>);  // foo("..") selects foo(std::string) which is not deleted.
static_assert(!foo_is_deleted<std::vector<int>>); // No viable overload, so not deleted
static_assert(!foo_is_deleted<char, char>);   // ambiguous overload, so not deleted
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Isn't that [`std::is_invocable`](https://en.cppreference.com/w/cpp/types/is_invocable)? – Nelfeal Nov 25 '22 at 17:21
  • @Nelfeal: `foo(std::vector{})` is not invocable, but is not deleted. – Jarod42 Nov 25 '22 at 17:24
  • There's an [`is_deleted`](https://en.cppreference.com/w/cpp/experimental/reflect#Callable_operations) listed in reflection TS, maybe you could search info about that. – Bob__ Nov 25 '22 at 18:00
  • 1
    I don't think it's possible since ["constructing a pointer to a deleted function, and even the use of a deleted function in an unevaluated expression"](https://en.cppreference.com/w/cpp/language/function#Deleted_functions) is ill-formed. You can't even use tricks like [this](https://stackoverflow.com/a/35945967/3854570) to check if the function is declared, because the deleted function will get picked. You need reflection for this. – Nelfeal Nov 25 '22 at 18:04
  • 4
    Out of curiosity, how could this be used? – Jeff Garrett Nov 25 '22 at 19:24
  • I'm also curious as to what use case you could have for this. I can understand needing `std::is_invocable`, I can somewhat understand needing to know if a specific function (with a known name or signature) is defined, but I can't imagine why you'd ever need to know if a function is explicitly deleted. – Nelfeal Nov 25 '22 at 20:02
  • @JeffGarrett: it is mostly to forward a `= delete`. whereas forwarding reference would be error prone (especially for constructor) and incomplete (as template, so disallow `{..}` deduction). – Jarod42 Nov 26 '22 at 13:12
  • Ok, so that's almost the better question... How to transparently-as-possible forward to an overload set? What do you mean by a forwarding reference is error prone? To allow braced-init-list, you must write the arguments as you note so you have to know them and you can't generically forward. You're missing one which is non-movable, non-copyable types which can be passed by value to functions but not forwarded, as far as I know. The material difference of sfinae vs delete on the forwarder should only become an issue if there are multiple source overloads (e.g. copy/move constructor). – Jeff Garrett Nov 26 '22 at 15:33

1 Answers1

3

The following isn't correct since it can't differentiate an ambiguous overload resolution from a deleted overload result. And I can't think of a way to differentiate that.

I'll leave my previous answer up for reference below.


Maybe something like this (https://godbolt.org/z/hTsq5rYnq):

namespace foo_is_deleted_impl {
    template<typename...>
    void foo(...);
}

template<typename... Args>
inline constexpr auto foo_is_deleted = []{
    auto a = requires { foo(std::declval<Args>()...); };
    using namespace foo_is_deleted_impl;
    auto b = requires { foo(std::declval<Args>()...); };
    return !(a || b);
}();

The idea is to first test whether overload resolution succeeds with a usable candidate (meaning non-deleted) for a.

Then I make the overload foo_is_deleted_impl::foo visible to unqualified name lookup inside the lambda with using namespace and repeat the test for b. The overload is declared in such a way that I think it is impossible for it to be better candidate than any other overload (If someone spots a case I missed, let me know).

If a is true, then a deleted overload surely wasn't taken, so I return false.

If a is not true, but b is, then overload resolution must have had failed for a, because the overload chosen for b wouldn't be better than an overload chosen for a, and so again false is returned.

If both a and b are false, then there are two possibilities: Either b failed because one of the non-foo_is_deleted_impl::foo overloads was chosen and is deleted, in which case I return true, or it failed because overload resolution became ambiguous with foo_is_deleted_impl::foo. That means however that overload resolution for a did find a viable candidate and so its result should be returned.

For all of this it is important that relative to one-another with respect to scope foo, foo_is_deleted_impl and foo_is_deleted are placed as they are. The placement of foo_is_deleted below the declarations of foo is also important because only ADL will lookup from the point of instantiation.

This also assumes that foo is an overload set of free function (templates) called with unqualified name. It should work with ADL.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Fail with ambiguous calls though [Demo](https://godbolt.org/z/5vrsEfeaG) :/ – Jarod42 Nov 26 '22 at 13:37
  • @Jarod42 Oh right. I forgot to consider that. Not sure that part can be resolved. – user17732522 Nov 26 '22 at 16:52
  • What could be the purpose of this exercise? The information that foo_is_deleted determines is statically already known before calling it. Why would one want to execute that at runtime? – habrewning Nov 26 '22 at 17:49
  • @habrewning Nothing is executed at runtime in my answer. It is just an attempt at reflection with what the language currently offers. Hopefully we will get proper reflection at some point. – user17732522 Nov 26 '22 at 18:06
  • Ah, ok. I was wrong. You should say it in your answer. Because the construct []{ ... }(); is not very easy to understand. And for most people it will a surprise that such calculations can be done at compile time. Is this standard compliant or is the compiler just tolerant. On https://en.cppreference.com/w/cpp/language/constant_expression there are 37 rules about constant expressions. Is your solution one of this. Just wondering. – habrewning Nov 26 '22 at 18:22
  • Is reflection the right word? In Java reflection is at runtime. – habrewning Nov 26 '22 at 18:23
  • @habrewning I'll admit that I use a few unusual techniques here, but it is mostly just to make the answer more concise. For example the lambda call `[]{/*...*/}()` could be replaced by a normal function call and `foo_is_deleted` itself should be a conventional type trait, i.e. a `struct` inherited from `std::bool_constant`, but OP's use in the `static_assert` wouldn't have matched that without (minor) modification. – user17732522 Nov 26 '22 at 20:13
  • @habrewning The whole thing happens at compile time not because of the lambda, but because of the `constexpr` on the variable. That always (effectively) guarantees compile-time evaluation or will fail to compile if it can't. – user17732522 Nov 26 '22 at 20:13
  • @habrewning Reflection in C++ would be compile-time reflection. It is not intended that all of the information that could be reflected should be available at run-time. There is already RTTI which contains some information about types at runtime, but that is often also considered undesirable. There shouldn't be any runtime cost to such a feature. – user17732522 Nov 26 '22 at 20:15
  • @habrewning See https://en.cppreference.com/w/cpp/experimental/reflect for an experimental design of reflection in C++. The one that standard C++ gets may however be designed very differently. (There are some competing approaches and I haven't read up on which one the standards committee currently favors.) – user17732522 Nov 26 '22 at 20:20