首页 > 解决方案 > SFINAE 自动检查函数体是否在没有显式约束的情况下编译

问题描述

如果函数体没有意义(即无法编译),我经常使用 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*’

我想得到一个错误,说没有twice类型参数的函数const char *

我很想写一些类似的东西:

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++ 中有意义吗?还是我缺少一个基本问题?这会被滥用或误用有多严重?

标签: c++templatesc++20definition-checking

解决方案


我们没有这个功能的最大原因是它很难。

这很难,因为它要求编译器能够编译几乎任意的 C++ 代码,得到错误,然后干净地退出。

现有的 C++ 编译器并非全部设计用于执行此操作。事实上,MSVC 花了十年的大部分时间才获得合理合规的decltypeSFINAE 支持。

为全功能机构这样做会更加困难。


现在,即使这很容易,也有理由不这样做。它以一种非常可怕的方式混合了实现和接口。

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

我们完成了。

所需的下一步称为“检查概念”。在检查的概念中,它转换为您的类型的一组编译时接口的概念T

然后检查函数的主体以确保不对任何T不是概念要求的类型进行任何操作。

使用理论上的未来检查概念,

template <IntegerScalable T>
T twice(T t) {
  T n = 7;
  if (n > t) return n;
  return 2 * t;
}

即使在调用模板完成之前编译模板时,编译器也会拒绝这一点,因为这个概念IntegerScalable不能保证您可以T用整数初始化 a ,也不能保证您可以T>. 另外,我认为以上需要移动构造。


您今天可以做一个 hack。

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

那么你的代码可以写成:

template<class T>
auto twice(T t)
RETURNS( 2 * t )

您将获得 SFINAE 友好版本的twice. 它也将尽其所能。

@Barry提出了这种=>用于替换RETURNS和其他一些东西的变体,但是自从我看到它移动以来已经有一年了。

同时,RETURNS承担大部分繁重的工作。


推荐阅读