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

ibps3vxo  于 2023-05-19  发布在  其他
关注(0)|答案(5)|浏览(195)

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引入了 requires-expression
需要第二个requires关键字背后的技术原因是什么?为什么我们不能只写:

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

(Note:请不要回答那个语法requires it)

ttcibm8c

ttcibm8c1#

这是因为语法需要它。是的
一个requires约束并不 * 必须 * 使用requires表达式。它可以使用任何或多或少的任意布尔常量表达式。因此,requires (foo)必须是合法的requires约束。
requiresexpression(测试某些事物是否遵循某些约束的东西)是一个独特的构造;它只是由相同的关键字引入。requires (foo f)将是一个有效的requires表达式的开始。
您想要的是,如果在接受约束的地方使用requires,您应该能够从requires子句中生成“约束+表达式”。
所以问题来了如果你把requires (foo)放到一个适合requires约束的地方...在解析器能够意识到这是一个requires constraint 而不是一个constraint+expression之前,解析器必须走多远?
想想这个:

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

如果foo是一个类型,那么(foo)是一个requires表达式的参数列表,并且{}中的所有内容都不是函数的主体,而是requires表达式的主体。否则,foorequires子句中的表达式。
那么,你可以说编译器应该首先弄清楚foo是什么。但是C++ * 真的 * 不喜欢解析一个标记序列的基本行为要求编译器在理解标记之前弄清楚这些标识符的含义。是的,C++是上下文敏感的,所以确实会发生这种情况。但委员会倾向于尽可能避免这种情况。
所以,是的,这是语法。

nbnkbykc

nbnkbykc2#

这种情况完全类似于noexcept(noexcept(...))。当然,这听起来更像是一件坏事,而不是一件好事,但让我解释一下。:)我们将从你已经知道的开始:
C++11有“noexcept-子句”和“noexcept-表达式”。它们做不同的事情。

  • 一个noexcept-子句说,“这个函数 * 应该是noexcept when... *(某个条件)。”它继续一个函数声明,接受一个布尔参数,并在声明的函数中引起行为变化。
  • 一个noexcept-表达式说,“编译器,* 请告诉我 *(某个表达式)是否是noexcept。”它本身就是一个布尔表达式。它对程序的行为没有“副作用”--它只是向编译器询问是/否问题的答案。“这表情是不是除了?”

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

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

  • 一个requires-子句说,“这个函数 * 应该在... *(某个条件)时参与重载解决。”它继续一个函数声明,接受一个布尔参数,并在声明的函数中引起行为变化。
  • 一个requires-expression说,“Compiler,please tell me whether(some set of expressions)is well-formed.”它本身就是一个布尔表达式。它对程序的行为没有“副作用”--它只是向编译器询问是/否问题的答案。“你这表情是不是很正式?”

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

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
bn31dyow

bn31dyow3#

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

4si2a6ki

4si2a6ki4#

我发现Andrew萨顿(Concepts的作者之一,他在gcc中实现了它)的a comment在这方面非常有帮助,所以我想我在这里引用它的几乎全部内容:
不久前,requires-expressions(第二个requires引入的短语)不允许出现在constraint-expressions(第一个requires引入的短语)中。它只能出现在概念定义中。事实上,这正是该文件中出现该主张的部分所提出的。
然而,在2016年,有人提议放宽这一限制[编者注:P0266 ]。请注意论文第4节第4段的删除线。因此诞生了需要需要。
说实话,我从来没有在GCC中实际实施过这种限制,所以它总是可能的。我认为沃尔特可能发现了这一点,并发现它很有用,导致了那篇论文。
为了避免有人认为我对写两次不敏感,我确实花了一些时间来确定是否可以简化。简短回答:不知道。
问题是在模板参数列表之后需要引入两个语法结构:非常常见的是约束表达式(如P && Q),偶尔还有语法要求(如requires (T a) { ... })。这叫做requires-expression。
第一个requires引入了约束。第二个requires引入了requires-expression。这就是语法的组成方式。我一点也不觉得困惑。
我曾一度试图将这些问题归结为一个单一的需求。不幸的是,这会导致一些非常困难的解析问题。你不能轻易地分辨,例如requires后面的(是表示嵌套的子表达式还是参数列表。我不相信这些语法有一个完美的消歧(参见统一初始化语法的基本原理;这个问题也存在)。
所以你做了个选择:make需要引入一个表达式(就像现在一样),或者让它引入一个参数化的需求列表。
我之所以选择当前的方法,是因为大多数时候(几乎100%的时候),我需要的不是requires-expression。在极其罕见的情况下,我确实需要一个特殊约束的requires-expression,我真的不介意写两次这个词。这是一个明显的迹象,表明我还没有为模板开发一个足够健全的抽象。(因为如果我有,它会有一个名字。
我可以选择将requires引入一个requires-expression。这实际上更糟,因为几乎所有的约束都将开始看起来像这样:

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

这里,第二个需求称为嵌套需求;它评估它的表达式(requires-expression块中的其他代码不被评估)。我认为这比现状更糟糕。现在,你可以在任何地方写两次。
我也可以使用更多的关键字。这本身就是一个问题--而且不仅仅是自行车脱落。也许有一种方法可以“重新分配”关键字来避免重复,但我还没有认真考虑过。但这并没有真正改变问题的本质。

ugmeyewa

ugmeyewa5#

因为你说的是一个事物A有一个需求B,而需求B有一个需求C。
A需要B,B又需要C。
“要求”条款本身要求一些东西。
你有东西A(需要B(需要C))。
Meh.:)

相关问题