为什么我们需要要求要求?
Why do we require requires requires?
C++20 概念的一个角落是在某些情况下您必须编写 requires requires
。例如,这个例子来自 [expr.prim.req]/3:
A requires-expression can also be used in a requires-clause ([temp]) as a way of writing ad hoc constraints on template arguments such as the one below:
template<typename T>
requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
The first requires introduces the requires-clause, and the second introduces the requires-expression.
需要第二个 requires
关键字的技术原因是什么?为什么我们不能只允许写作:
template<typename T>
requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
(注意:请不要回答那个语法requires
而已)
我认为 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<T>
部分,你会得到:
template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
这解释了机制。 为什么 最好用一个例子来说明,如果我们将语言更改为接受单个 requires
作为 [=17= 的 shorthand,则会导致歧义].
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 不会尝试 link 步骤,在这种情况下会失败。
f 和 g 之间的唯一区别是 'requires' 的两倍。然而f和g之间的语义差异是巨大的:
- g 只是一个函数声明,f 是一个完整的定义
- f 只接受 bool,g 接受所有可转换为 void 的类型
- g 用它自己的(多余的括号)x 遮蔽 x,但是
- f 将全局 x 转换为给定类型 T
显然我们不希望编译器自动将一个更改为另一个。这可以通过为 requires
的两种含义使用单独的关键字来解决,但是 C++ 可能会尝试在不引入太多新关键字的情况下进行改进,因为这会破坏旧程序。
因为语法要求。确实如此。
requires
约束不必须 使用requires
表达式。它可以使用任何或多或少的任意布尔常量表达式。因此,requires (foo)
必须是合法的 requires
约束。
A requires
expression(测试某些事物是否遵循某些约束的东西)是一个独特的构造;它只是由相同的关键字引入的。 requires (foo f)
将是有效 requires
表达式的开头。
你想要的是,如果你在接受约束的地方使用requires
,你应该能够从requires
子句中得到一个"constraint+expression"。
所以问题来了:如果你把 requires (foo)
放到一个适合 requires 约束的地方......解析器必须走多远才能意识到这是一个 requires 约束而不是你想要的约束+表达式?
考虑一下:
void bar() requires (foo)
{
//stuff
}
如果foo
是一个类型,那么(foo)
就是一个requires表达式的参数列表,{}
里面的一切都不是函数体而是requires
表达式。否则,foo
是 requires
子句中的表达式。
嗯,你可以说编译器应该首先弄清楚 foo
是什么。但是 C++ 真的 不喜欢它,因为解析标记序列的基本行为要求编译器在理解标记之前弄清楚这些标识符的含义。是的,C++ 是上下文相关的,所以确实会发生这种情况。但委员会更愿意尽可能避免它。
所以是的,这是语法。
因为你说的是A事物有B需求,B需求有C需求
A 需要 B,B 又需要 C。
"requires" 子句本身需要一些东西。
你有东西 A(需要 B(需要 C))。
嗯。 :)
情况与noexcept(noexcept(...))
完全相似。当然,这听起来更像是坏事而不是好事,但让我解释一下。 :) 我们将从您已经知道的开始:
C++11 有“noexcept
-子句”和“noexcept
-表达式”。他们做不同的事情。
一个 noexcept
子句说,"This function should be noexcept when... (some condition)." 它继续一个函数声明,接受一个布尔参数,并导致声明的函数发生行为变化。
一个noexcept
-表达式表示,"Compiler, please tell me whether (some expression) is noexcept."它本身就是一个布尔表达式。它没有关于程序行为的 "side effects" — 它只是要求编译器回答 yes/no 问题。 "Is this expression 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
-子句”和“requires
-表达式”。他们做不同的事情。
一个 requires
子句说,"This function should participate in overload resolution when... (some condition)." 它继续一个函数声明,接受一个布尔参数,并导致声明的函数发生行为变化。
一个requires
-表达式表示,"Compiler, please tell me whether (some set of expressions) is well-formed."它本身就是一个布尔表达式。它没有关于程序行为的 "side effects" — 它只是要求编译器回答 yes/no 问题。 "Is this expression well-formed?"
我们可以在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
我发现 Andrew Sutton(Concepts 的作者之一,他在 gcc 中实现了它)的 a comment 在这方面很有帮助,所以我想我只是在这里引用它——整体:
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.
C++20 概念的一个角落是在某些情况下您必须编写 requires requires
。例如,这个例子来自 [expr.prim.req]/3:
A requires-expression can also be used in a requires-clause ([temp]) as a way of writing ad hoc constraints on template arguments such as the one below:
template<typename T> requires requires (T x) { x + x; } T add(T a, T b) { return a + b; }
The first requires introduces the requires-clause, and the second introduces the requires-expression.
需要第二个 requires
关键字的技术原因是什么?为什么我们不能只允许写作:
template<typename T>
requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
(注意:请不要回答那个语法requires
而已)
我认为 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<T>
部分,你会得到:
template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
这解释了机制。 为什么 最好用一个例子来说明,如果我们将语言更改为接受单个 requires
作为 [=17= 的 shorthand,则会导致歧义].
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 不会尝试 link 步骤,在这种情况下会失败。
f 和 g 之间的唯一区别是 'requires' 的两倍。然而f和g之间的语义差异是巨大的:
- g 只是一个函数声明,f 是一个完整的定义
- f 只接受 bool,g 接受所有可转换为 void 的类型
- g 用它自己的(多余的括号)x 遮蔽 x,但是
- f 将全局 x 转换为给定类型 T
显然我们不希望编译器自动将一个更改为另一个。这可以通过为 requires
的两种含义使用单独的关键字来解决,但是 C++ 可能会尝试在不引入太多新关键字的情况下进行改进,因为这会破坏旧程序。
因为语法要求。确实如此。
requires
约束不必须 使用requires
表达式。它可以使用任何或多或少的任意布尔常量表达式。因此,requires (foo)
必须是合法的 requires
约束。
A requires
expression(测试某些事物是否遵循某些约束的东西)是一个独特的构造;它只是由相同的关键字引入的。 requires (foo f)
将是有效 requires
表达式的开头。
你想要的是,如果你在接受约束的地方使用requires
,你应该能够从requires
子句中得到一个"constraint+expression"。
所以问题来了:如果你把 requires (foo)
放到一个适合 requires 约束的地方......解析器必须走多远才能意识到这是一个 requires 约束而不是你想要的约束+表达式?
考虑一下:
void bar() requires (foo)
{
//stuff
}
如果foo
是一个类型,那么(foo)
就是一个requires表达式的参数列表,{}
里面的一切都不是函数体而是requires
表达式。否则,foo
是 requires
子句中的表达式。
嗯,你可以说编译器应该首先弄清楚 foo
是什么。但是 C++ 真的 不喜欢它,因为解析标记序列的基本行为要求编译器在理解标记之前弄清楚这些标识符的含义。是的,C++ 是上下文相关的,所以确实会发生这种情况。但委员会更愿意尽可能避免它。
所以是的,这是语法。
因为你说的是A事物有B需求,B需求有C需求
A 需要 B,B 又需要 C。
"requires" 子句本身需要一些东西。
你有东西 A(需要 B(需要 C))。
嗯。 :)
情况与noexcept(noexcept(...))
完全相似。当然,这听起来更像是坏事而不是好事,但让我解释一下。 :) 我们将从您已经知道的开始:
C++11 有“noexcept
-子句”和“noexcept
-表达式”。他们做不同的事情。
一个
noexcept
子句说,"This function should be noexcept when... (some condition)." 它继续一个函数声明,接受一个布尔参数,并导致声明的函数发生行为变化。一个
noexcept
-表达式表示,"Compiler, please tell me whether (some expression) is noexcept."它本身就是一个布尔表达式。它没有关于程序行为的 "side effects" — 它只是要求编译器回答 yes/no 问题。 "Is this expression 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
-子句”和“requires
-表达式”。他们做不同的事情。
一个
requires
子句说,"This function should participate in overload resolution when... (some condition)." 它继续一个函数声明,接受一个布尔参数,并导致声明的函数发生行为变化。一个
requires
-表达式表示,"Compiler, please tell me whether (some set of expressions) is well-formed."它本身就是一个布尔表达式。它没有关于程序行为的 "side effects" — 它只是要求编译器回答 yes/no 问题。 "Is this expression well-formed?"
我们可以在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
我发现 Andrew Sutton(Concepts 的作者之一,他在 gcc 中实现了它)的 a comment 在这方面很有帮助,所以我想我只是在这里引用它——整体:
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 (likerequires (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.