Vittorio Romeo
Vittorio Romeo

Reputation: 93264

Getting SFINAE to work with `is_callable` on an overloaded function object

Consider the following function object l:

auto l = [](auto x){ x.foo(); };

I can successfully static_assert that it is callable with a type that has a .foo() member function:

struct Foo { void foo(); };
static_assert(std::is_callable<decltype(l)(Foo)>{});

I would now like std::is_callable to evaluate to std::false_type if I use a type that does not have a .foo() member function:

static_assert(!std::is_callable<decltype(l)(int)>{});

Unfortunately, the above static_assert results in a compilation error:

prog.cc: In instantiation of '<lambda(auto:1)> [with auto:1 = int]':
prog.cc:8:44:   required by substitution of 'template<class TF, class ... Ts> 
                struct is_callable<TF(Ts ...), 
                    std::void_t<decltype (declval<TF>()((declval<Ts>)()...))> 
                > [with TF = <lambda(auto:1)>; Ts = {int}]'
prog.cc:17:50:   required from here
prog.cc:12:24: error: request for member 'foo' in 'x', which is of non-class type 'int'
 auto l = [](auto x){ x.foo(); };
                      ~~^~~

I also tried implementing my own is_callable by using std::void_t as follows, getting the same compilation error:

template <typename, typename = void>
struct is_callable : std::false_type { };

template <typename TF, class... Ts>
struct is_callable<TF(Ts...),
    std::void_t<decltype(std::declval<TF>()(std::declval<Ts>()...))>>
    : std::true_type { };

I was under the impression that the std::false_type fallback would be chosen if the expression inside std::void_t<decltype(/* ... */)> was not valid due to SFINAE.

live example on wandbox (C++14 compliant)

Upvotes: 3

Views: 385

Answers (2)

Vittorio Romeo
Vittorio Romeo

Reputation: 93264

Why is this SFINAE not taking place here, resulting in a compilation error?

Because l is callable with everything - the compilation error occurs when its body is instantiated, which happens outside of the "isolated environment" where SFINAE is taking place.

Constraining the lambda as shown by Guillaume Racicot's answer will fix the issue:

auto l = [](auto x) -> void_t<decltype(x.foo())> { x.foo(); };

(Note that void_t is being used here as x.foo() is not being returned by the lambda.)


How can I achieve my desired behavior? (i.e. evaluate to std::false_type if calling the overloaded function object is ill-formed)

If the function objects' signatures are not properly constrained, there is no way of achieving your desired behavior.

Being able to do that would require compilers to implement some sort of "speculative compilation" which is undesirable due to the difficulty of implementation.

More information in this article: "Diagnosable validity".

There is no way to check it from within C++, and there is not going to be any: it has been clearly laid out, that compiler vendors cannot be forced to implement a “speculative compilation” and backtracking from arbitrarily deep template instantiation failures.

And in this discussion: "Asserting an expected compile-time failure".

Upvotes: 1

Guillaume Racicot
Guillaume Racicot

Reputation: 41760

Your lambda does not constrain it's​ arguments with sfinae. Your lambda is callable with anything, but triggers hard compilation errors when instantiated with these arguments.

To get the desired effect, put constraints on the return type:

auto l = [](auto x) -> void_t<decltype(x.foo())> { x.foo(); };

That way, the is_callable trait will yield the correct result.

Upvotes: 5

Related Questions