【问题标题】:Why do we require requires requires?为什么我们需要需要需要?
【发布时间】:2019-06-09 14:10:47
【问题描述】:

C++20 概念的一个角落是在某些情况下您必须编写requires requires。比如这个例子来自[expr.prim.req]/3

requires-expression 也可以在 requires-clause ([temp]) 中使用,作为在模板参数上编写临时约束的一种方式,例如下面:

template<typename T>
  requires requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }

第一个 requires 引入 requires-clause,第二个引入 requires-expression

需要第二个 requires 关键字背后的技术原因是什么?为什么我们不能只允许写作:

template<typename T>
  requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }

(注意:请不要回答那个语法requires吧)

【问题讨论】:

  • 似乎requires可能用于模板类的方法,它会“模棱两可”template &lt;typename T&gt; struct S {void f(T t) requires requires (T x) {x + x;} { t + t;} };
  • 建议:“有什么需要需要的吗?”。更严重的是,我有一种预感,这与 noexcept(noexcept(...)) 背后的原因相同。
  • 他们说“第一个 requires 引入了 requires-clause,第二个引入了 requires-expression。”,但这与他们上面给出的语法不兼容
  • @Quentin 与noexcept 存在歧义。如果f() 计算结果为真或f()noexceptnoexcept(f()) 可能意味着noexcept
  • 这两个requires在我看来是同音异义词:它们看起来一样,拼写一样,闻起来一样,但本质上是不同的。如果我建议修复,我建议重命名其中一个。

标签: c++ c++-concepts c++20


【解决方案1】:

这是因为语法需要它。确实如此。

requires 约束不必必须使用requires 表达式。它可以使用任何或多或少的任意布尔常量表达式。因此,requires (foo) 必须是合法的requires 约束。

requires表达式(测试某些事物是否遵循某些约束的东西)是一个独特的构造;它只是由相同的关键字引入的。 requires (foo f) 将是有效 requires 表达式的开头。

您想要的是,如果您在接受约束的地方使用requires,您应该能够从requires 子句中创建一个“约束+表达式”。

所以问题来了:如果你把requires (foo) 放到一个适合需求约束的地方......解析器必须走多远才能意识到这是一个需求约束 em> 而不是你想要的约束+表达式?

考虑一下:

void bar() requires (foo)
{
  //stuff
}

如果foo 是一个类型,那么(foo) 是一个requirements 表达式的参数列表,并且{} 中的所有内容都不是函数的主体,而是requires 表达式的主体。否则,foorequires 子句中的表达式。

好吧,你可以说编译器应该首先弄清楚foo 是什么。但是 C++ 真的 不喜欢它,因为解析标记序列的基本行为需要编译器在理解标记之前弄清楚这些标识符的含义。是的,C++ 是上下文相关的,所以这确实发生了。但委员会倾向于尽可能避免这种情况。

所以是的,这是语法。

【讨论】:

  • 有一个类型但没有名称的参数列表有意义吗?
  • @Quentin:C++ 语法中肯定存在上下文相关的情况。但委员会确实试图尽量减少这一点,他们绝对不喜欢添加更多
  • @RobertAndrzejuk:如果requires 出现在&lt;&gt; 一组模板参数或函数参数列表之后,那么它就是一个要求子句。如果requires 出现在表达式有效的位置,则它是一个需要表达式。这可以由解析树的结构决定,而不是解析树的内容(标识符如何定义的细节将是树的内容)。
  • @RobertAndrzejuk:当然,requires-expression 可以使用不同的关键字。但是关键字在 C++ 中具有巨大的成本,因为它们有可能破坏任何使用已成为关键字的标识符的程序。概念提案已经引入了两个关键字:conceptrequires。引入第三个,当第二个能够涵盖两种情况而没有语法问题和很少面对用户的问题时,只是浪费。毕竟,唯一的视觉问题是关键字恰好重复了两次。
  • @RobertAndrzejuk 无论如何,内联这样的约束都是不好的做法,因为你不会像写了一个概念那样得到包容。因此,为如此低使用率的不推荐功能使用标识符并不是一个好主意。
【解决方案2】:

情况与noexcept(noexcept(...)) 完全相同。当然,这听起来更像是一件坏事而不是一件好事,但让我解释一下。 :) 我们将从你已经知道的开始:

C++11 有“noexcept-clauses”和“noexcept-expressions”。他们做不同的事情。

  • noexcept-clause 说,“这个函数应该是 noexcept when...(某些条件)。”它继续一个函数声明,接受一个布尔参数,并导致声明函数的行为发生变化。

  • noexcept-表达式说:“编译器,请告诉我(某些表达式)是否为 noexcept。”它本身就是一个布尔表达式。它对程序的行为没有“副作用”——它只是向编译器询问是/否问题的答案。 “这个表达式是 noexcept 吗?”

我们可以noexcept-表达式嵌套在noexcept-子句中,但我们通常认为这样做不好。

template<class T>
void incr(T t) noexcept(noexcept(++t));  // NOT SO HOT

noexcept-表达式封装在类型特征中被认为是更好的样式。

template<class T> inline constexpr bool is_nothrow_incrable_v =
    noexcept(++std::declval<T&>());  // BETTER, PART 1

template<class T>
void incr(T t) noexcept(is_nothrow_incrable_v<T>);  // BETTER, PART 2

C++2a 工作草案有“requires-clauses”和“requires-expressions”。他们做不同的事情。

  • requires-clause 说:“此函数应在...(某些条件)时参与重载决议。”它继续一个函数声明,接受一个布尔参数,并导致声明函数的行为发生变化。

  • requires-表达式说:“编译器,请告诉我(某些表达式)是否格式正确。”它本身就是一个布尔表达式。它对程序的行为没有“副作用”——它只是向编译器询问是/否问题的答案。 “这个表达式格式正确吗?”

我们可以requires-表达式嵌套在requires-子句中,但我们通常认为这样做不好。

template<class T>
void incr(T t) requires (requires(T t) { ++t; });  // NOT SO HOT

requires-表达式封装在类型特征中被认为是更好的样式...

template<class T> inline constexpr bool is_incrable_v =
    requires(T t) { ++t; };  // BETTER, PART 1

template<class T>
void incr(T t) requires is_incrable_v<T>;  // BETTER, PART 2

...或在(C++2a 工作草案)概念中。

template<class T> concept Incrable =
    requires(T t) { ++t; };  // BETTER, PART 1

template<class T>
void incr(T t) requires Incrable<T>;  // BETTER, PART 2

【讨论】:

  • 我真的不认同这个论点。 noexcept 的问题是 noexcept(f()) 可能意味着或者f()解释为我们用来设置规范的布尔值检查@987654345是否@ 是 noexceptrequires 没有这种歧义,因为它检查有效性的表达式已经必须与{}s 一起引入。在那之后,论点基本上是“语法是这样说的”。
  • @Barry:见this comment。看来{} 是可选的。
  • @Eric {} 不是可选的,这不是评论所显示的。然而,这是一个很好的评论,展示了解析歧义。可能会接受该评论(带有一些解释)作为独立答案
  • requires is_nothrow_incrable_v&lt;T&gt;; 应该是requires is_incrable_v&lt;T&gt;;
  • is_incrementible???显然,“可以递增”没有一个完善的英文单词,但我在这里猜测这是更正确的???
【解决方案3】:

我认为cppreference's concepts page 解释了这一点。我可以用“数学”来解释,为什么一定是这样的:

如果你想定义一个概念,你可以这样做:

template<typename T>
concept Addable = requires (T x) { x + x; }; // requires-expression

如果你想声明一个使用该概念的函数,你可以这样做:

template<typename T> requires Addable<T> // requires-clause, not requires-expression
T add(T a, T b) { return a + b; }

现在,如果您不想单独定义概念,我想您所要做的就是进行一些替换。将这部分requires (T x) { x + x; }; 替换为Addable&lt;T&gt; 部分,你会得到:

template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }

其中解释了机制。 为什么最好用一个示例来说明,如果我们将语言更改为接受单个requires 作为requires requires 的简写,则会导致歧义。

constexpr int x = 42;

template<class T>
void f(T) requires(T (x)) { (void)x; };

template<class T>
void g(T) requires requires(T (x)) { (void)x; };

int main(){
    g<bool>(0);
}

View in Godbolt to see the compiler warnings,但请注意,Godbolt 不会尝试链接步骤,在这种情况下会失败。

f 和 g 之间的唯一区别是 'requires' 加倍。然而 f 和 g 之间的语义差异是巨大的:

  • g 只是一个函数声明,f 是一个完整的定义
  • f 只接受 bool,g 接受所有可转换为 void 的类型
  • g 用它自己的(用括号括起来的)x 遮蔽 x,但是
  • f 将全局 x 转换为给定类型 T

显然我们不希望编译器自动将其中一个更改为另一个。这可以通过对requires 的两种含义使用单独的关键字来解决,但在可能的情况下,C++ 会尝试在不引入太多新关键字的情况下发展,因为这会破坏旧程序。

【讨论】:

  • 我不认为这是问题所要求的。这或多或少是在解释语法。
  • @NathanOliver:因为你强迫编译器将一个结构解释为另一个结构。 requires-as-constraint 子句必须requires-表达式。这只是它的一种可能用途。
  • @TheQuantumPhysicist 我的评论得到的是这个答案只是解释了语法。不是我们必须requires requires 的实际技术原因。他们本可以在语法中添加一些内容以允许template&lt;typename T&gt; requires (T x) { x + x; },但他们没有。巴里想知道他们为什么不这样做
  • 如果我们真的在这里玩 find-the-grammar-ambiguity,好吧,我会咬的。 godbolt.org/z/i6n8kM template&lt;class T&gt; void f(T) requires requires(T (x)) { (void)x; }; 如果删除requireses 之一,则意味着不同。
  • @TamaMcGlinn:我想你已经明白了。在the godbolt I posted above 中,带有一个requiresf 是一个定义:它受限于(T(x)),即(bool(42))(因为Tbool),即true。它的主体是{ (void)x; },带有多余的尾随;。带有requires requiresg 是受requires (T (x)) { (void)x; } 约束的声明,这对所有T 都是可以的(除了cv 限定的voidarguably 可憎类型);它没有身体。
【解决方案4】:

我发现 Andrew Sutton(Concepts 的作者之一,他在 gcc 中实现它)的a comment 在这方面很有帮助,所以我想我只是在这里几乎全部引用它:

不久前,约束表达式(第一个 requires 引入的短语)中不允许使用 requires-expressions(第二个 requires 引入的短语)。它只能出现在概念定义中。事实上,这正是该论文中出现该声明的部分所提出的内容。

但是,在 2016 年,有人提出放宽该限制 [编者注:P0266]。请注意论文第 4 节中第 4 段的删除线。就这样诞生了需求需求。

说实话,我从来没有在 GCC 中真正实现过这个限制,所以它一直是可能的。我认为 Walter 可能已经发现了这一点并发现它很有用,从而导致了那篇论文。

为了避免有人认为我对写作要求不敏感,我确实花了一些时间来确定是否可以简化。简短的回答:没有。

问题是在模板参数列表之后需要引入两个语法结构:非常常见的约束表达式(如P &amp;&amp; Q)和偶尔的语法要求(如requires (T a) { ... })。这称为需求表达式。

第一个要求引入了约束。第二个 requires 引入了 requires-expression。这就是语法的构成方式。我一点也不觉得它令人困惑。

我曾尝试将这些合并为一个单独的需求。不幸的是,这会导致一些非常困难的解析问题。您无法轻易判断,例如,require 之后的 ( 是否表示嵌套子表达式或参数列表。我不相信这些语法可以完美消除歧义(请参阅统一初始化语法的基本原理;这个问题也存在)。

所以你做出选择:make requires 引入一个表达式(就像现在一样)或者让它引入一个参数化的需求列表。

我选择当前的方法是因为大多数时候(几乎 100% 的时间),我想要的不是需求表达式。在极其罕见的情况下,我确实想要一个特殊约束的需求表达式,我真的不介意把这个词写两次。这是一个明显的指标,表明我还没有为模板开发出足够完善的抽象。 (因为如果我有,它就会有名字。)

我可以选择让 requires 引入一个 requires-expression。这实际上更糟,因为实际上您的所有约束都开始看起来像这样:

template<typename T>
  requires { requires Eq<T>; }
void f(T a, T b);

这里,第二个需求称为嵌套需求;它评估其表达式(不评估 requires-expression 块中的其他代码)。我认为这比现状更糟糕。现在,你可以在任何地方写两次 requires 了。

我也可以使用更多的关键字。这本身就是一个问题——不仅仅是自行车脱落。可能有一种方法可以“重新分配”关键字以避免重复,但我没有认真考虑过。但这并不能真正改变问题的本质。

【讨论】:

    【解决方案5】:

    因为你是说一个事物 A 有一个需求 B,而需求 B 有一个需求 C。

    A 需要 B,B 反过来又需要 C。

    “requires”子句本身需要一些东西。

    你有东西A(需要B(需要C))。

    嗯。 :)

    【讨论】:

    • 但根据其他答案,第一个和第二个requires 在概念上不是同一件事(一个是子句,一个是表达式)。事实上,如果我理解正确的话,requires (requires (T x) { x + x; }) 中的两组() 具有非常不同的含义(外部是可选的,并且总是包含一个布尔常量表达式;内部是引入 requires 表达式和 的强制部分不允许实际表达)。
    • @MaxLanghof 你是说要求不同吗? :D
    猜你喜欢
    • 2014-06-18
    • 2017-02-26
    • 2011-04-03
    • 2017-07-27
    • 2020-09-21
    • 2020-03-09
    • 2018-12-24
    • 2012-04-08
    • 1970-01-01
    相关资源
    最近更新 更多