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++ 委员会没有走这条路,而是朝着完全不同的方向前进。

概念是指您可以以合理的、通常是命名的方式表达对类型的要求。他们进来

正如另一个答案提到的,

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 完成了大部分繁重的工作。