为什么我们需要require require ?

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

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

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

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

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

21655 次浏览

我认为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++尝试在不引入太多新关键字的情况下进行进化,因为这会破坏旧程序。

这是因为语法要求它。它的功能。

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

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

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

那么问题来了:如果你把requires (foo)放到一个适合require约束的地方……解析器要走多远才能意识到这是一个需要约束而不是你想要的约束+表达式?

考虑一下:

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

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

好吧,你可以说编译器应该先找出foo是什么。但是c++ 真的不喜欢解析一系列标记的基本行为要求编译器在理解这些标记之前先弄清楚这些标识符的含义。是的,c++是上下文敏感的,所以这种情况确实会发生。但委员会倾向于尽可能避免使用它。

是的,这是语法问题。

因为你在说一个东西a有一个需求B,而需求B有一个需求C。

A需要B, B又需要C。

"requires"从句本身要求某事。

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

咩。:)

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

c++ 11有“__abc0 -clause”和“noexcept-expressions”。他们做不同的事情。

  • __abc0 -子句说:“这个函数应该是noexcept当…(某些条件)."它继续进行函数声明,接受一个布尔参数,并在声明的函数中引起行为更改。

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

我们可以将__abc0 -表达式嵌套在__abc0 -子句中,但我们通常认为这样做是糟糕的风格。

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

noexcept-expression封装在type-trait中被认为是更好的样式。

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工作草案有“__abc0 -clause”和“requires-expressions”。他们做不同的事情。

  • __abc0 -子句说:“这个函数应该参与重载解析,当…(某些条件)."它继续进行函数声明,接受一个布尔参数,并在声明的函数中引起行为更改。

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

我们可以将__abc0 -表达式嵌套在__abc0 -子句中,但我们通常认为这样做是糟糕的风格。

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

requires-expression封装在type-trait中被认为是更好的样式…

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(概念的作者之一,他在gcc中实现了它)的一个评论在这方面非常有帮助,所以我想我只是在这里引用它的几乎完整:

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

然而,在2016年,有人提议放宽这一限制[编者注:P0266]。请注意文件第4节第4段的删去。因此出生就需要需要。

说实话,我从来没有在GCC中实现过这个限制,所以它一直都是可行的。我认为沃尔特可能发现了这一点并发现它很有用,从而得出了那篇论文。

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

问题是在模板形参列表之后需要引入两个语法结构:通常是约束表达式(如P && Q),偶尔是语法需求(如requires (T a) { ... })。这叫做需求表达式。

第一个要求引入了约束。第二个require介绍了require表达式。这只是语法构成的方式。我一点也不觉得困惑。

有一次,我试着把这些分解成一个要求。不幸的是,这会导致一些非常困难的解析问题。你不容易分辨,例如require后面的(表示嵌套子表达式还是参数列表。我不相信这些语法可以完美地消除歧义(参见统一初始化语法的基本原理;这个问题也存在)。

因此,您做出了一个选择:让require引入一个表达式(就像现在这样),还是让它引入一个参数化的需求列表。

我之所以选择当前的方法,是因为在大多数情况下(几乎100%的情况下),我想要的不是require -expression。在非常罕见的情况下,我确实需要一个特殊约束的需求表达式,我真的不介意把这个词写两遍。这是一个明显的迹象,表明我还没有为模板开发一个足够合理的抽象。(因为如果我有,它就有名字了。)

我本可以选择让require引入一个require表达式。这实际上更糟糕,因为实际上你所有的约束都将开始看起来像这样:

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

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

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