检测具有特定参数的通用lambda是否可调用

huangapple go评论130阅读模式
英文:

Detecting if a generic lambda with certain arguments is invocable

问题

我正在尝试使用一些 lambda 检测功能。

我试图实现的目标是能够检测是否可以使用类型 const C& 的参数调用 lambda 函数 F,并返回可转换为 bool 的结果。

以下代码看起来接近我需要的内容,但表现不如预期:

#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() {
    // 这个 lambda 预期不能使用 std::string 参数调用
    auto lambda = [](auto x) -> bool { return x * x; };

    lambda(1); // 正常工作
    //lambda(std::string("abc")); // 显然无法编译通过(因为 std::string 没有 * 运算符)

    constexpr auto can_invoke_int = can_invoke_pred_v<decltype(lambda), int>;
    static_assert(can_invoke_int); // 可以使用 int 调用

    // 如果我移除 lambda 定义中的 ->bool,下一行将不会编译通过,
    // 否则断言将失败
    constexpr auto can_invoke_str = can_invoke_pred_v<decltype(lambda), std::string>;
    static_assert(not can_invoke_str); // 预期失败,因为我无法使用字符串调用 lambda
    return 0;
}

如果我移除 -> bool(例如,将 lambda 定义为 auto lambda = [](auto x) { return x * x; }; ),那么这一行 static_assert(can_invoke_pred_v<decltype(lambda), std::string>); 不会编译通过。也就是说,不是检测到无法为 std::string 类型生成包含 x*x 表达式的 lambda,而是生成了 lambda,然后出现编译错误。

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; };
      |                                       ~~^~~

这个问题是否有解决方案?有人能解释这里发生了什么吗?

英文:

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?

答案1

得分: 2

static_assert(!can_invoke_str); // 预期失败,因为我不能使用字符串调用 lambda 表达式

由于您添加了not,我不明白为什么您会期望它失败。相反,它现在失败表明is_invocable_predicate 认为它可以调用带有std::string参数的 lambda 表达式。

您的 lambda 表达式不符合 SFINAE 的友好性。SFINAE 无法拦截 lambda 体内部发生的错误,因此如果检查 lambda 体中的错误,就会产生硬错误,这就是当您删除-> bool时发生的情况。

当不检查 lambda 体时,can_invoke_str 静默返回 true,因为[](auto x) -> bool的部分并未表明无法将std::string传递给它。

为什么-> bool在那里时不检查 lambda 体?默认情况下不会检查 lambda 体,但当没有指定返回类型(或以auto的形式指定)时,必须检查 lambda 体以确定真正的返回类型。

如何修复这个问题?

选项1:[](auto x) -> decltype(x * x) { return x * x; }。现在 SFINAE 将检查[](auto x) -> decltype(x * x)部分,将std::string代入x * x将触发 SFINAE。

但这会将返回类型从bool更改为x * x 返回的任何类型。

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

第一个requires接受右侧的布尔表达式,确定该函数是否可用于这些模板参数。requires{x * x;}根据x * x是否有效返回truefalse

这允许您指定任何返回类型。

requires 需要 C++20。

选项3:在 C++20 之前,我们通常会这样做:

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)>只是bool,但必须由 SFINAE 检查第二个模板参数。

英文:

> cpp
&gt; static_assert(not can_invoke_str); // expected to fail as I cannot invoke the lambda with string
&gt;

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 -&gt; bool.

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

Why is the body not examined when -&gt; 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) -&gt; decltype(x * x) { return x * x; }. Now SFINAE will examine the [](auto x) -&gt; 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) -&gt; 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 &lt;typename T, typename...&gt;
struct dependent_type_helper {using type = T;};
template &lt;typename T, typename ...P&gt;
using dependent_type = typename dependent_type_helper&lt;T, P...&gt;::type;
[](auto x) -&gt; dependent_type&lt;bool, decltype(x * x)&gt; { return x * x; }

dependent_type&lt;bool, decltype(x * x)&gt; is just bool, but the second template argument has to be checked by SFINAE.

答案2

得分: 1

不是所有模板替换中的错误都可以在C++中检测到。

为了使编译器编写更简单,当在函数体解析过程中发现错误时,这些错误不会参与SFINAE(替换失败不是错误)。

当您编写

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

这会生成一个类,大致如下:

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

operator()方法的主体中发现的任何错误都将是严重错误,无法恢复。编译器将忽略该错误并停止编译。

某些错误的子集是“替换失败”友好的(也称为SFINAE)。尽管标准在技术上描述了它们,但它们基本上是作为模板的“签名”的一部分而发生的错误,而不是模板的“主体”。

如果您的auto lambda(或其他模板)不进行工作以使其成为SFINAE,那么没有办法确定是否可以使用特定类型实例化模板(或将特定类型传递给它),而不会发生严重错误。

我通常的方法是

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

这样使用:

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

并生成一个(单语句)SFINAE友好的lambda。它也可以通过使用auto返回类型来供函数使用。

英文:

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&lt;class T&gt;
  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(...) -&gt;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.

huangapple
  • 本文由 发表于 2023年6月22日 01:26:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/76525790.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定