【问题标题】:SFINAE automatically check that function body compiles without explicit constraintsSFINAE 自动检查函数体是否在没有显式约束的情况下编译
【发布时间】:2018-11-23 06:47:08
【问题描述】:

如果函数体没有意义(即无法编译),我经常使用 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 将是一场噩梦。

你认为在 c++ 中添加requires function_body_compiles 有意义吗?还是我缺少一个基本问题?这会被滥用或误用有多严重?

【问题讨论】:

  • 这会使 SFINAE 对 T 的要求不清楚。必须检查整个构造函数体以确定特定的 T 是否适合。
  • 无法前向声明这样的模板。它会允许拼写错误或其他简单的拼写错误未被发现:假设函数体调用printf,但你忘记了#include &lt;stdio.h&gt;。突然,整个过载消失了!这会是什么:template&lt;typename T&gt; requires function_body_compiles T once(T t) { return rand() ? once(t) : t; }?当且仅当函数体编译时,函数体才会编译。
  • @RaymondChen 前向声明绝对是一个非常有效的观点,是的,循环依赖肯定是一个问题,但即使现在你也可以实现这一点。实际上,Nicolai 在 38:50 左右的 CppCon 谈话中谈到了它。虽然,我看到你的错别字,但我认为它很容易被检测到。您尝试调用一个函数,编译器会告诉您没有这样的函数,因为您实际要调用的函数由于拼写错误而被删除。唯一的问题是如果有另一个函数重载匹配,它是否经常发生?
  • 这不就是概念的全部内容吗? (而不是合同)
  • @Francis Cugler 只需要一个谓词的否定,例如require !std::is_same_v&lt;T, int&gt;。当然,你可以引入一个谓词或一个概念更好的名称,但绝对不需要新的关键字。

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


【解决方案1】:

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

这很难,因为它要求编译器能够编译几乎任意的 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;
}

我们完成了。

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

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

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

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

这将被编译器拒绝在编译模板时甚至在模板调用完成之前,因为概念IntegerScalable并不能保证你可以初始化一个T整数,也不能将T 与另一个&gt; 进行比较。另外,我认为以上需要移动构造。


今天你可以做一个 hack。

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

那么你的代码可以写成:

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

您将获得twice 的 SFINAE 友好版本。它也将尽可能地为 noexcept。

@Barry 提出了一个使用=&gt; 替换RETURNS 和其他一些东西的变体,但我已经一年没看到它移动了。

与此同时,RETURNS 完成了大部分繁重的工作。

【讨论】:

    【解决方案2】:

    Barry Revzin 提交了一个[proposal],这正是您所要求的,但在 lambda 表达式的上下文中。由于它需要构造 lambda,因此语法会有所不同:

    auto twice = [](auto t) => 2 * t; //sfinae friendly
    

    甚至:

    auto twice = 2 * $0;
    

    尽管如此,该提案的状态仍不确定。您可以查看[here]

    但是在构造函数的情况下,我不确定是否有办法应用这样的构造,即使提案被接受。然而,如果有人看到了 lambda 表达式的需求,那么在一般情况下可能会有语言发展的潜力。

    【讨论】:

    • 这很好,但是对于复杂的函数你不想写一个 lambda。
    • @tom 确切地说,现在我看不出你在当前标准措辞中想要什么的可能性,但是当委员会最终接受巴里的提议时,我认为大门会敞开来应用一些从您的角度来看语言改进更有趣...
    • 嗯,我想多了。这仅适用于单行 lambda,对吗?然后它并没有我最初想的那么有用。如果它是单行,您也可以再次将其写入尾随返回类型并获得所需的行为。这有点冗长,但可行。
    • @tom 是的,但表达式仍然可能复杂而广泛,复制它可能会很痛苦......我猜这就是提案背后的原始原因。当您想到它时 - 您的理想行为会将需求扩展到整个代码块的级别。
    • @tom 我想再次声明,在这里提到缩写的 lambda 是为了向您指出向该语言提出新功能的方向,因为目前没有可用的提议,我所知道的提议也不会适合你想要的语法。
    【解决方案3】:

    您可以在某种程度上使用 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
    }
    

    正如您在 cmets 中所指出的,不能使用辅助函数来避免将函数体写入两次,因为 直到实例化时才发现实现中的错误。但是,需要表达式受益于 优于 decltype(expr) 尾随返回类型的优势:

    • 它们不限于返回类型。
    • 可以有任意多的表达式。

    您想要的称为“概念定义检查”。 Bjarne Stroustrup 讨论了它的原因 论文P0557R0的概念设计中缺少 (第 8.2 节)。

    【讨论】:

    • 我不需要require,我可以很容易地做到auto twice(T t) -&gt; decltype(2*t) {...}。问题是你必须写两次函数体!这不适用于更复杂的功能。
    • @tom 好吧。你可以编写一个辅助函数,也许在一个详细的命名空间中,并在一个 requires 表达式中调用它。
    • 我对@9​​87654330@ 没有实际经验,但这真的有用吗?我知道这肯定不适用于decltype 技巧。
    • @tom。它确实有效。这是一个link 供您尝试。
    • 不,它不起作用。它不会从重载集中删除函数。为了测试这一点,我定义了没有requiretwice 函数,如果不满足require 则应该调用不受约束的版本。但是,调用 twice("hello") 会导致编译器错误,而不是调用不受约束的版本。看看:godbolt.org/z/LvMXQi
    猜你喜欢
    • 1970-01-01
    • 2021-12-30
    • 1970-01-01
    • 2019-04-29
    • 2011-01-08
    • 1970-01-01
    • 2016-06-01
    • 2018-05-30
    • 2021-03-24
    相关资源
    最近更新 更多