1

I am experimenting with some lambda detection functionality.

What I am trying to achieve is to be able to detect if I can call a lambda F with an argument of type const C& returning a result convertible to bool.

The code below looks close to what I need, but does not behave as expected :

#include <string>
#include <iostream>
#include <type_traits>
#include <experimental/type_traits>

template <typename F, typename C>
using is_invocable_predicate = decltype(std::declval<F>()(std::declval<const C&>()));

template <typename F, typename C>
constexpr bool can_invoke_pred_v = std::experimental::is_detected_convertible_v<bool, is_invocable_predicate, F, C> ;

int main() {
    // this lambda expectedly can not be invoked with std::string argument
    auto lambda = [](auto x) -> bool { return x * x; };

    lambda(1); // works
    //lambda(std::string("abc")); // this obviously does not compile (as there is no operator * for std::string

    constexpr auto can_invoke_int = can_invoke_pred_v<decltype(lambda), int>;
    static_assert(can_invoke_int); // invocable with int

    // if I remove ->bool in lambda definition next line will not compile,
    // otherwise the assertion fails
    constexpr auto can_invoke_str = can_invoke_pred_v<decltype(lambda), std::string>;
    static_assert(not can_invoke_str); // expected to fail as I cannot invoke the lambda with string
    return 0;
}

If I remove -> bool (say I define lambda as auto lambda = [](auto x) { return x * x; }; ), then line static_assert(can_invoke_pred_v<decltype(lambda), std::string>); does not compile, i.e. instead of detecting that such lambda containing x*x expression cannot be generated for type std::string, it is generated and then I get compilation error.

test.cpp:14:41: error: no match for 'operator*' (operand types are 'std::__cxx11::basic_string<char>' and 'std::__cxx11::basic_string<char>')
   14 |     auto lambda = [](auto x) { return x * x; };
      |                                       ~~^~~

Is there a solution for this problem? Could someone explain what is happening here?

JeJo
  • 30,635
  • 6
  • 49
  • 88
  • SFINAE only happens from declaration, not block definition content. So `[](auto x) -> bool` is callable with any type, even if it produces hard error . – Jarod42 Jun 21 '23 at 18:58

2 Answers2

2
static_assert(not can_invoke_str); // expected to fail as I cannot invoke the lambda with string

Since you added not, I don't see why you'd expect it to fail. Instead, it failing now indicates that is_invocable_predicate thinks it can invoke your lambda with a std::string argument.

Your lambda is not SFINAE-friendly. SFINAE can't intercept an error originating inside the lambda body, so if the faulty body is examined at all, you get a hard error, which is what happened when you removed -> bool.

When the lambda body is not examined, can_invoke_str silently returns true, because nothing about the [](auto x) -> bool part indicates that std::string can't be passed to it.

Why is the body not examined when -> bool is there? It's not examined by default, but when the return type is not specified (or specified in terms of auto), the body has to be examined to determine the true return type.

How to fix this?

Option 1: [](auto x) -> decltype(x * x) { return x * x; }. Now SFINAE will examine the [](auto x) -> decltype(x * x) part, and substituting std::string into x * x will trigger SFINAE.

But this changes the return type from bool to whatever x * x returns.

Option 2: [](auto x) -> bool requires requires{x * x;} { return x * x; }

The first requires accepts a boolean expression on the right, which determines whether the function is callable with those template arguments or not. requires{x * x;} returns either true or false depending on x * x being valid.

This lets you specify any return type you want.

requires needs C++20.

Option 3: Pre-C++20 we used to do this:

template <typename T, typename...>
struct dependent_type_helper {using type = T;};
template <typename T, typename ...P>
using dependent_type = typename dependent_type_helper<T, P...>::type;
[](auto x) -> dependent_type<bool, decltype(x * x)> { return x * x; }

dependent_type<bool, decltype(x * x)> is just bool, but the second template argument has to be checked by SFINAE.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • I think you can do `template using dependent_type = T;` unless you're on a sufficiently old compiler (this was the `void_t` hack that used to require that extra layer of indirection and now just lets us define it as `template using void_t = void;`) – Barry Jun 21 '23 at 18:24
  • Found the issue: https://cplusplus.github.io/CWG/issues/1558.html – Barry Jun 21 '23 at 18:25
  • @Barry I've had Clang choke on `std::void_t` SFINAE a few times (because of the missing struct indirection), last time one or two versions ago I think. I don't have a testcase at hand, so instead of figuring it out, I just avoid it. – HolyBlackCat Jun 21 '23 at 18:37
  • 2
    @Barry Aha, still broken in trunk: https://gcc.godbolt.org/z/PdhKTEPxE MSVC chokes too. – HolyBlackCat Jun 21 '23 at 19:03
  • I made a mistake in the comment, I.e. I the last assertion is expected to pass – Dmitriy Kumshayev Jun 21 '23 at 20:57
  • The goal here is not to make lambdas SFINAE friendly but to detect if a lambda can be called. can_invoke_pred_v can be used to test any possible lambda or any type in general and it should just return a boolean value saying if an object of this type is callable with given argument type or not instead of giving computation error. Maybe that’s not possible but that’s what I am trying to understand. – Dmitriy Kumshayev Jun 21 '23 at 21:10
  • @DmitriyKumshayev Yes, it's impossible if the lambda doesn't cooperate (by being SFINAE-friendly). – HolyBlackCat Jun 21 '23 at 21:41
  • Arguably, your lambda advertises that it's callable for any type, and is bugged because it fails to fulfil that promise. – HolyBlackCat Jun 21 '23 at 21:42
  • @HolyBlackCat Thanks for explaining – Dmitriy Kumshayev Jun 21 '23 at 21:43
  • You also see a lot of `-> decltype((void) x * x, bool{})` for "SFINAE on `x*x`, otherwise `bool`" – Artyer Jun 21 '23 at 22:09
  • @Artyer That's slightly uncool because it relies on the return type being default-constructible. (Also it should be `decltype(void(x * x), bool{})`.) – HolyBlackCat Jun 21 '23 at 22:21
1

Not all errors in template substitution can be detected in C++.

In order to make life less complex for compiler writers, errors found while parsing the body of a function do not participate in SFINAE (substitution failure is not an error).

When you write

[](auto x){return x*x;}

this generates a class that looks roughly like:

struct anonymous_lambda {
  template<class T>
  auto operator()(T x)const{ return x*x; }
};

Anything errors found within the body of the operator() method are going to be hard errors, ones that cannot be recovered from. The compiler will omit the error, and stop compiling.

A certain subset of errors are "substitution failure" friendly (aka, SFINAE). While the standard describes them technically, they are basically errors that occur as part of the template "signature" as opposed to the template "body".

And if your auto lambda (or other template) doesn't do the work to make themselves SFINAE, there is no way to determine if it is safe to instantiate the template with specific types (or, pass certain types to it) without risking a hard error.

My classic way to do this is

#define RETURNS(...) ->decltype(__VA_ARGS__) { return __VA_ARGS__; }

which is used like:

[](auto x) RETURNS(x*x)

and generates a (single-statement) SFINAE-friendly lambda. It can also be used by functions by using the auto return type.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524