Detecting if a generic lambda with certain arguments is invocable

118 Views Asked by At

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?

2

There are 2 best solutions below

10
HolyBlackCat On
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.

0
Yakk - Adam Nevraumont On

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.