SFINAE 自动检查函数体是否在没有明确约束的情况下编译
SFINAE automatically check that function body compiles without explicit constraints
如果函数体没有意义(即不编译),我经常使用 SFINAE 从重载集中删除函数。是否可以向 C++ 添加一个简单的 require
语句?
例如,我们有一个函数:
template <typename T>
T twice(T t) {
return 2 * t;
}
然后我得到:
twice(1.0);
twice("hello"); // Error: invalid operands of types ‘int’ and ‘const char*’ to binary ‘operator*’
我想收到一条错误消息,指出 const char *
类型的参数没有函数 twice
我很想写这样的东西:
template <typename T>
requires function_body_compiles
T twice(T t) {
return 2 * t;
}
然后我会得到
twice(1.0);
twice("hello"); // Error: no matching function for call to ‘twice2(const char [6])’
更多动机:我在看演讲The Nightmare of Move Semantics for Trivial Classes,他最后的 SFINAE 基本上是说:在编译时使用这个构造函数。对于更复杂的构造函数,编写正确的 SFINAE 将是一场噩梦。
您认为将 requires function_body_compiles
添加到 c++ 中是否有意义?还是我遗漏了一个基本问题?这会被滥用或误用到什么程度?
Barry Revzin 提交了一份 [proposal] 文件,完全符合您的要求,但在 lambda 表达式的上下文中。因为它需要构造 lambda,语法会有点不同:
auto twice = [](auto t) => 2 * t; //sfinae friendly
甚至:
auto twice = 2 * [=11=];
尽管如此,该提案的状态仍不确定。你可以查看一下[here].
但是 在构造函数的情况下,即使提案被接受,我也不确定是否有办法应用这样的构造。尽管如此,如果有人看到了 lambda 表达式的需求,那么在一般情况下可能会有语言开发的潜力。
您可以在某种程度上使用 requires-expressions (https://godbolt.org/z/6FDT45):
template <typename T> requires requires(T t) { { 2 * t } -> T; }
T twice(T t) {
return 2 * t;
}
int main()
{
twice(1.0);
twice("hello"); // Error: Constraints not satisfied
}
正如您在评论中指出的那样,不能使用辅助函数来避免将函数体编写两次,因为
直到实例化时才发现实现中的错误。但是,需要表达式受益于
优于 decltype(
expr)
尾随 return 类型:
- 它们不限于 return 类型。
- 表达式可以有任意多个。
您想要的是"concept definition checking"。 Bjarne Stroustrup 讨论了原因
论文中的概念设计缺失 P0557R0
(第 8.2 节)。
我们没有这个功能的最大原因是它很难。
这很难,因为它要求编译器能够编译几乎任意的 C++ 代码,出现错误,然后干净地退出。
现有的 C++ 编译器并非都设计用于执行此操作。事实上,MSVC 花了十年的大部分时间才获得合理兼容的 decltype
SFINAE 支持。
对全功能机构这样做会更难。
现在,即使这很容易,也有理由不这样做。它以一种非常可怕的方式混合了实现和接口。
C++ 委员会没有走这条路,而是朝着完全不同的方向前进。
概念是指您可以以合理的、通常是命名的方式表达对类型的要求。他们进来 c++20。
正如另一个答案提到的,
template <typename T> requires requires(T t) { { 2 * t } -> T; }
T twice(T t) {
return 2 * t;
}
是一种方法,但这种方法被认为是错误的形式。相反,你应该写一个概念“可以乘以一个整数并得到相同的类型”。
template<typename T>
concept IntegerScalable = requires(T t) {
{ 2 * t } -> T;
};
然后我们可以
template <IntegerScalable T>
T twice(T t) {
return 2 * t;
}
我们完成了。
期望的下一步称为“检查概念”。 In checked concepts, concept it converted into a set of compile-time interfaces for your type T
.
然后检查函数体以确保不对任何类型 T
不是概念要求的内容进行任何操作。
使用理论上的未来检查概念,
template <IntegerScalable T>
T twice(T t) {
T n = 7;
if (n > t) return n;
return 2 * t;
}
编译器在编译模板时甚至在调用模板之前就拒绝了,因为概念IntegerScalable
不能保证你可以用整数初始化 T
,也可以用 >
将一个 T
与另一个进行比较。另外我认为以上需要移动构造。
你今天可以做一个技巧。
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
那么你的代码可以写成:
template<class T>
auto twice(T t)
RETURNS( 2 * t )
您将获得 twice
的 SFINAE 友好版本。它也将尽其所能。
@Barry 提出了使用 =>
替换 RETURNS
和其他一些东西的变体,但我已经一年没看到它移动了。
与此同时,RETURNS
完成了大部分繁重的工作。
如果函数体没有意义(即不编译),我经常使用 SFINAE 从重载集中删除函数。是否可以向 C++ 添加一个简单的 require
语句?
例如,我们有一个函数:
template <typename T>
T twice(T t) {
return 2 * t;
}
然后我得到:
twice(1.0);
twice("hello"); // Error: invalid operands of types ‘int’ and ‘const char*’ to binary ‘operator*’
我想收到一条错误消息,指出 const char *
twice
我很想写这样的东西:
template <typename T>
requires function_body_compiles
T twice(T t) {
return 2 * t;
}
然后我会得到
twice(1.0);
twice("hello"); // Error: no matching function for call to ‘twice2(const char [6])’
更多动机:我在看演讲The Nightmare of Move Semantics for Trivial Classes,他最后的 SFINAE 基本上是说:在编译时使用这个构造函数。对于更复杂的构造函数,编写正确的 SFINAE 将是一场噩梦。
您认为将 requires function_body_compiles
添加到 c++ 中是否有意义?还是我遗漏了一个基本问题?这会被滥用或误用到什么程度?
Barry Revzin 提交了一份 [proposal] 文件,完全符合您的要求,但在 lambda 表达式的上下文中。因为它需要构造 lambda,语法会有点不同:
auto twice = [](auto t) => 2 * t; //sfinae friendly
甚至:
auto twice = 2 * [=11=];
尽管如此,该提案的状态仍不确定。你可以查看一下[here].
但是 在构造函数的情况下,即使提案被接受,我也不确定是否有办法应用这样的构造。尽管如此,如果有人看到了 lambda 表达式的需求,那么在一般情况下可能会有语言开发的潜力。
您可以在某种程度上使用 requires-expressions (https://godbolt.org/z/6FDT45):
template <typename T> requires requires(T t) { { 2 * t } -> T; }
T twice(T t) {
return 2 * t;
}
int main()
{
twice(1.0);
twice("hello"); // Error: Constraints not satisfied
}
正如您在评论中指出的那样,不能使用辅助函数来避免将函数体编写两次,因为
直到实例化时才发现实现中的错误。但是,需要表达式受益于
优于 decltype(
expr)
尾随 return 类型:
- 它们不限于 return 类型。
- 表达式可以有任意多个。
您想要的是"concept definition checking"。 Bjarne Stroustrup 讨论了原因 论文中的概念设计缺失 P0557R0 (第 8.2 节)。
我们没有这个功能的最大原因是它很难。
这很难,因为它要求编译器能够编译几乎任意的 C++ 代码,出现错误,然后干净地退出。
现有的 C++ 编译器并非都设计用于执行此操作。事实上,MSVC 花了十年的大部分时间才获得合理兼容的 decltype
SFINAE 支持。
对全功能机构这样做会更难。
现在,即使这很容易,也有理由不这样做。它以一种非常可怕的方式混合了实现和接口。
C++ 委员会没有走这条路,而是朝着完全不同的方向前进。
概念是指您可以以合理的、通常是命名的方式表达对类型的要求。他们进来 c++20。
正如另一个答案提到的,
template <typename T> requires requires(T t) { { 2 * t } -> T; }
T twice(T t) {
return 2 * t;
}
是一种方法,但这种方法被认为是错误的形式。相反,你应该写一个概念“可以乘以一个整数并得到相同的类型”。
template<typename T>
concept IntegerScalable = requires(T t) {
{ 2 * t } -> T;
};
然后我们可以
template <IntegerScalable T>
T twice(T t) {
return 2 * t;
}
我们完成了。
期望的下一步称为“检查概念”。 In checked concepts, concept it converted into a set of compile-time interfaces for your type T
.
然后检查函数体以确保不对任何类型 T
不是概念要求的内容进行任何操作。
使用理论上的未来检查概念,
template <IntegerScalable T>
T twice(T t) {
T n = 7;
if (n > t) return n;
return 2 * t;
}
编译器在编译模板时甚至在调用模板之前就拒绝了,因为概念IntegerScalable
不能保证你可以用整数初始化 T
,也可以用 >
将一个 T
与另一个进行比较。另外我认为以上需要移动构造。
你今天可以做一个技巧。
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
那么你的代码可以写成:
template<class T>
auto twice(T t)
RETURNS( 2 * t )
您将获得 twice
的 SFINAE 友好版本。它也将尽其所能。
@Barry 提出了使用 =>
替换 RETURNS
和其他一些东西的变体,但我已经一年没看到它移动了。
与此同时,RETURNS
完成了大部分繁重的工作。