为什么我们要求要求?

Bar*_*rry 155 c++ c++-concepts c++20

C++ 20概念的一个角落是,在某些情况下你必须编写requires requires.例如,[expr.prim.req]/3中的这个例子:

需要表达也可以在使用需要子句([温度])作为写在模板参数特设约束,如下面的一个的一种方法:

template<typename T>
  requires requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }
Run Code Online (Sandbox Code Playgroud)

第一个需要引入requires子句,第二个需要引入requires-expression.

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

template<typename T>
  requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }
Run Code Online (Sandbox Code Playgroud)

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

Nic*_*las 77

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

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

requires 表达是一种独特的构建体(即测试某些事情是否遵循一定的约束条件的东西); 它只是由相同的关键字引入.requires (foo f)将是有效requires表达式的开始.

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

所以这里有一个问题:如果你requires (foo)进入一个适合需求约束的地方......解析器必须走多远才能意识到这是一个需要约束而不是约束+表达你想要它的方式成为?

考虑一下:

void bar() requires (foo)
{
  //stuff
}
Run Code Online (Sandbox Code Playgroud)

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

好吧,你可以说编译器应该只知道什么foo是第一个.但是,当解析一系列令牌的基本行为要求编译器在识别令牌之前弄清楚这些标识符的含义时,C++ 真的不喜欢它.是的,C++是上下文敏感的,所以这确实发生了.但委员会倾向于尽可能避免它.

是的,这是语法.

  • @RobertAndrzejuk:当然,requires-expression可能使用了不同的关键字.但是关键字在C++中具有巨大的成本,因为它们有可能破坏使用已成为关键字的标识符的任何程序.概念提案已经引入了两个关键词:`concept`和`requires`.引入第三个,当第二个能够涵盖两个没有语法问题和几个面向用户的问题的情况时,只是浪费.毕竟,唯一的视觉问题是关键字碰巧重复了两次. (6认同)
  • @Quentin:C++语法肯定存在上下文依赖的情况.但委员会确实试图尽量减少这一点,他们肯定不喜欢增加*更多*. (3认同)
  • @RobertAndrzejuk:如果在`<>`模板参数之后或在函数参数列表之后出现`requires`,那么它是一个requires子句.如果表达式有效,则出现`requires`,那么它就是一个require-expression.这可以通过解析树的结构来确定,而不是解析树的*内容*(标识符如何定义的细节将是树的内容). (3认同)
  • @RobertAndrzejuk无论如何内联约束都是不好的做法,因为你没有得到包含,好像你已经写了一个概念.因此,为这种低使用不推荐的功能采用标识符并不是一个好主意. (3认同)
  • 有一个类型但没有名字的参数列表是否有意义? (2认同)
  • 通常,在不知道哪些名称是类型而哪些名称是模板的情况下,您根本无法解析C ++,因此我不喜欢给出的特定示例。但是不明确之处很容易找到:这与最烦人的解析非常相似。 (2认同)

Quu*_*one 58

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

C++ 11有" noexcept-clauses"和"-expressions" noexcept.他们做不同的事情.

  • A noexcept-clause说," 当......(某些情况),这个功能应该是不存在的." 它继续进行函数声明,获取布尔参数,并在声明的函数中引起行为更改.

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

我们可以noexcept-clause中嵌套noexcept-expression,但我们通常认为这样做是不好的.

template<class T>
void incr(T t) noexcept(noexcept(++t));  // NOT SO HOT
Run Code Online (Sandbox Code Playgroud)

noexcept-expression 封装在类型特征中被认为是更好的风格.

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
Run Code Online (Sandbox Code Playgroud)

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

  • A requires-clause说,"这个函数应该在......(某些条件)时参与重载解析." 它继续进行函数声明,获取布尔参数,并在声明的函数中引起行为更改.

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

我们可以requires-clause中嵌套requires-expression,但我们通常认为这样做是不好的.

template<class T>
void incr(T t) requires (requires(T t) { ++t; });  // NOT SO HOT
Run Code Online (Sandbox Code Playgroud)

requires-expression 封装在类型特征中被认为是更好的风格......

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
Run Code Online (Sandbox Code Playgroud)

......或(C++ 2a Working Draft)概念.

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
Run Code Online (Sandbox Code Playgroud)


The*_*ist 15

我认为cppreference的概念页面解释了这一点.我可以用"数学"解释一下,为什么这个必须是这样的:

如果要定义概念,请执行以下操作:

template<typename T>
concept Addable = requires (T x) { x + x; }; // requires-expression
Run Code Online (Sandbox Code Playgroud)

如果要声明使用该概念的函数,请执行以下操作:

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

现在,如果你不想单独定义这个概念,我想你所要做的就是一些替代.取这部分requires (T x) { x + x; };并更换Addable<T>部件,你会得到:

template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
Run Code Online (Sandbox Code Playgroud)

这就是你要问的问题.

  • 我不认为问题是要求的.这或多或少地解释了语法. (4认同)
  • 如果我们真的在这里发现语法歧义,那么,我会咬人.https://godbolt.org/z/i6n8kM`templaten <class T> void f(T)需要require(T(x)){(void)x; 如果删除其中一个`requires`,则表示不同的东西. (3认同)
  • @NathanOliver:因为你强迫编译器将一个构造解释为另一个构造.`require`-as-constraint子句不必*是`requires`-expression.这只是它的一种可能用途. (2认同)
  • @TheQuantumPhysicist我对评论的看法是这个答案只是解释了语法.不是我们必须要求的实际技术原因.他们可以在语法中添加一些内容,以允许`template <typename T> require(T x){x + x; 但是他们没有.巴里想知道他们为什么没有 (2认同)

Bar*_*rry 10

我发现评论从安德鲁·萨顿(观念作者之一,谁实现了它在海湾合作委员会),以在这方面非常有帮助的,所以我想我只是引用它在这里的大致整体:

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

然而,在2016年,有一项提议放宽该限制[编者注:P0266 ].请注意该文件第4节第4段的删除线.因此诞生需要.

说实话,我从来没有在GCC中实际实施过这种限制,所以它一直是可能的.我认为沃尔特可能已经发现了这一点,并发现它很有用,导致了这篇论文.

为了避免任何人认为我对写作不敏感需要两次,我确实花了一些时间来确定是否可以简化.简答:不.

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

第一个要求引入约束.第二个要求介绍requires-expression.这就是语法组成的方式.我觉得它根本不会让人感到困惑.

我曾试图将这些折叠成一个单一的要求.不幸的是,这会导致一些非常困难的解析问题.例如,在需求(之后是否表示嵌套的子表达式或参数列表时,您无法轻易判断.我不相信这些语法有完美的消歧(参见统一初始化语法的基本原理;这个问题也存在).

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

我选择当前的方法是因为大部分时间(在近100%的时间内),我想要的不是需求表达式.在非常罕见的情况下,我确实想要一个针对临时约束的需求表达式,我真的不介意两次写这个单词.这是一个明显的指标,我没有为模板开发出足够完善的抽象.(因为如果我有,它会有一个名字.)

我本可以选择使需求引入一个require-expression.这实际上更糟糕,因为几乎所有的约束都会开始如下所示:

template<typename T>
  requires { requires Eq<T>; }
void f(T a, T b);
Run Code Online (Sandbox Code Playgroud)

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

我也可以使用更多关键字.这本身就是一个问题 - 而且不只是自行车脱落.可能有一种方法可以"重新分配"关键字以避免重复,但我没有给出那种严肃的想法.但这并没有真正改变问题的本质.