英文:
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
是否有效返回true
或false
。
这允许您指定任何返回类型。
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
> 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.
答案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<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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论