为什么我们需要“需要需要”?

217
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-clause,第二个要求引入了requires-expression
为什么需要第二个requires关键字的技术原因是什么?为什么我们不能只允许编写:
template<typename T>
  requires (T x) { x + x; }
    T add(T a, T b) { return a + b; }

(注:请不要回答语法“要求”这个问题)
(注意:请不要回答语法“要求”这个问题)

33
建议:“有什么需要需要需要的吗?”更严肃地说,我有一种预感,这背后的原因与“noexcept(noexcept(...))”相同。 - Quentin
3
使用 noexcept 会存在歧义。noexcept(f()) 的含义可能是,如果 f() 的结果为真或者 f()noexcept 的话,则表示该函数是 noexcept 的。 - Passer By
@PasserBy - 嗯,那这里不是同样的原因吗?一个 requires 子句可以有任何 constexpr 谓词,对吧? - StoryTeller - Unslander Monica
16
在我看来,“requires”这两个词是同音异义词:它们的外观、拼写和发音相同,但内在含义不同。如果我要提出一个修复建议,我会建议重新命名其中一个。 - YSC
@StoryTeller 看起来是这样的,正如 NicolBolas 所展示的那样,尽管程度较小。 - Passer By
显示剩余4条评论
5个回答

116

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

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

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

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

所以问题来了:如果将 requires (foo) 放入适用于 requires 约束的位置……在解析器意识到这是一个 requires 约束 而不是你想要的约束+表达式之前,它需要走多远?

考虑以下情况:

void bar() requires (foo)
{
  //stuff
}
如果foo是一种类型,则(foo)是一个requires表达式的参数列表,{}中的所有内容都不是函数体而是该requires表达式的主体。否则,foorequires子句中的一个表达式。
嗯,你可以说编译器应该先弄清楚foo是什么。但是C++非常不喜欢解析一系列标记的基本行为需要编译器在弄清这些标识符的含义之前就能理解这些标记。是的,C++是上下文相关的,所以确实会发生这种情况。但委员会更喜欢在可能的情况下避免这种情况。
所以,没错,这就是语法问题。

2
在参数列表中有类型但没有名称,这是否有意义? - NathanOliver
3
@Quentin:C++语法中肯定存在一些依赖于上下文的情况。但委员会确实努力将其最小化,并且绝对不喜欢增加更多。 - Nicol Bolas
3
如果 requires 出现在一组尖括号 <> 的模板参数或函数参数列表之后,则它是一个 requires 从句。如果 requires 出现在表达式可以出现的位置,则它是一个 requires 表达式。这可以通过解析树的结构来确定,而不是解析树的 内容(标识符如何定义的具体细节将是树的内容)。 - Nicol Bolas
9
@RobertAndrzejuk: 当然,requires-expression本可以使用不同的关键字。但是在C++中,关键字有着巨大的代价,因为它们有可能会破坏任何使用已成为关键字的标识符的程序。概念提案已经引入了两个关键字:conceptrequires。引入第三个关键字,当第二个关键字能够覆盖两种情况,并且几乎没有语法问题和用户界面问题时,就是浪费的行为。毕竟,唯一的视觉问题是该关键字恰好重复两次。 - Nicol Bolas
4
@RobertAndrzejuk 这种内联约束的做法本身就不好,因为你无法像编写概念一样得到包容性。因此,为这种很少使用的特性取一个标识符并不是一个好主意。 - Rakete1111
显示剩余12条评论

86
这种情况与noexcept(noexcept(...))完全类似。当然,这听起来更像是一件坏事而不是一件好事,但是让我解释一下。 :) 我们从你已经知道的开始:
C++11有“noexcept子句”和“noexcept表达式”。它们有不同的作用。
- noexcept子句表示:“当(某个条件)时,此函数应该是noexcept的。”它放在函数声明中,接受一个布尔参数,并导致被声明的函数的行为发生变化。 - 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-expression表示:“编译器,请告诉我是否(一组表达式)是良好的形式。” 它本身是一个布尔表达式。它对程序的行为没有“副作用”——它只是询问编译器一个是/否问题:“这个表达式是否良好?”
我们可以将一个requires-expression嵌套在requires-clause中,但我们通常认为这样做是不好的风格。
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

1
我并不完全认同这个观点。noexcept 的问题在于 noexcept(f()) 可以被解释为我们用来设置规范的布尔值,或者检查 f() 是否为 noexcept。而 requires 没有这种歧义,因为它检查有效性的表达式必须已经用 {} 引入。之后,这个论点基本上是“语法如此规定”。 - Barry
@Barry:请看这个评论。看起来{}是可选的。 - Eric
1
@Eric,{}不是可选的,这并不是该注释所展示的内容。然而,这是一个很好的注释,展示了解析歧义。可能会接受该注释(附有一些解释)作为独立的答案。 - Barry
1
requires is_nothrow_incrable_v<T>; should be requires is_incrable_v<T>; - Ruslan
is_incrementible??? 显然,“can be incremented”没有一个被广泛接受的英语单词,但我猜这个更正确吧? - Daniel Russell
抱歉,应该基于标准的std::incrementable_traits使用is_incrementable,即用字母'a'而不是'i'。 - Daniel Russell

32

我认为cppreference的概念页面可以解释这个问题。我可以用“数学”的方式来解释为什么必须这样做:

如果你想定义一个概念,你需要这样做:

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<T> 部分,你就可以得到:

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

在Godbolt中查看编译器警告,但请注意,Godbolt不会尝试链接步骤,在这种情况下会失败。

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

  • g是一个函数声明,f是一个完整的定义
  • f只接受bool,g接受可以转换为void的任何类型
  • g用自己的(过多括号化的)x隐藏了x,但是
  • f将全局x转换为给定的类型T

显然我们不希望编译器自动将一个变成另一个。这可以通过为'requires'的两个含义使用单独的关键字来解决,但是当可能时,C++尝试在不引入太多新关键字的情况下演化,因为这会破坏旧程序。


4
我认为问题询问的不是这个。这更多地解释了语法。 - Passer By
2
@NathanOliver:因为你强制编译器将一个结构解释为另一个结构。requires-as-constraint子句不一定要是requires-expression。那只是它的一种可能用法。 - Nicol Bolas
2
@TheQuantumPhysicist 我在评论中想表达的是,这个答案只是解释了语法,而没有解释我们需要使用requires requires的实际技术原因。他们本可以通过修改语法来允许template<typename T> requires (T x) { x + x; },但他们没有这样做。Barry 想知道为什么他们没有这样做。 - NathanOliver
7
如果我们真的在寻找语法歧义,那好吧,我来试试。https://godbolt.org/z/i6n8kM 如果你移除其中一个 requirestemplate<class T> void f(T) requires requires(T (x)) { (void)x; }; 的含义将会有所不同。 - Quuxplusone
1
@TamaMcGlinn:我认为你已经明白了。在我上面发布的godbolt中,带有一个requiresf是一个定义:它被限制在(T(x))上,即(bool(42))(因为Tbool),即true。它的主体是{ (void)x; },带有一个多余的尾随;。带有requires requiresg是一个声明,受到requires (T (x)) { (void)x; }的约束,对于所有的T都可以(除了cv-qualified void可怕的类型);并且它没有主体。 - Quuxplusone
显示剩余7条评论

19

我发现Andrew Sutton(概念的作者之一,他在gcc中实现了它)的评论在这方面非常有帮助,所以我想在此引用其中几乎全部内容:

Not so long ago requires-expressions (the phrase introduced by the second requires) was not allowed in constraint-expressions (the phrase introduced by the first requires). It could only appear in concept definitions. In fact, this is exactly what is proposed in the section of that paper where that claim appears.

However, in 2016, there was a proposal to relax that restriction [Editor's note: P0266]. Note the strikethrough of paragraph 4 in section 4 of the paper. And thus was born requires requires.

To tell the truth, I had never actually implemented that restriction in GCC, so it had always been possible. I think that Walter may have discovered that and found it useful, leading to that paper.

Lest anybody think that I wasn't sensitive to writing requires twice, I did spend some time trying to determine if that could be simplified. Short answer: no.

The problem is that there are two grammatical constructs that need to introduced after a template parameter list: very commonly a constraint expression (like P && Q) and occasionally syntactic requirements (like requires (T a) { ... }). That's called a requires-expression.

The first requires introduces the constraint. The second requires introduces the requires-expression. That's just the way the grammar composes. I don't find it confusing at all.

I tried, at one point, to collapse these to a single requires. Unfortunately, that leads to some seriously difficult parsing problems. You can't easily tell, for example if a ( after the requires denotes a nested subexpression or a parameter-list. I don't believe that there is a perfect disambiguation of those syntaxes (see the rationale for uniform initialization syntax; this problem is there too).

So you make a choice: make requires introduce an expression (as it does now) or make it introduce a parameterized list of requirements.

I chose the current approach because most of the time (as in nearly 100% of the time), I want something other than a requires-expression. And in the exceedingly rare case I did want a requires-expression for ad hoc constraints, I really don't mind writing the word twice. It's a an obvious indicator that I haven't developed a sufficiently sound abstraction for the template. (Because if I had, it would have a name.)

I could have chosen to make the requires introduce a requires-expression. That's actually worse, because practically all of your constraints would start to look like this:

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

Here, the 2nd requires is called a nested-requirement; it evaluates its expression (other code in the block of the requires-expression is not evaluated). I think this is way worse than the status quo. Now, you get to write requires twice everywhere.

I could also have used more keywords. This is a problem in its own right---and it's not just bike shedding. There might be a way to "redistribute" keywords to avoid the duplication, but I haven't given that serious thought. But that doesn't really change the essence of the problem.


-14

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

事物A需要B,而B又需要C。

"需要"条款本身也需要某些东西。

你有一个事物A(需要B(需要C))。

嗯。:)


5
根据其他答案,第一个和第二个“requires”在概念上并不相同(一个是子句,一个是表达式)。实际上,如果我理解正确,在requires (requires (T x) { x + x; })中的两组括号具有非常不同的含义(外部括号是可选的,并始终包含布尔constexpr;内部括号是引入requires表达式的强制部分,_不允许_实际表达式)。 - Max Langhof
3
@MaxLanghof 你是在说需求不同吗? :D - Lightness Races in Orbit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接