声明的螺旋规则——何时会出错?

22

我最近学到了用螺旋法则来分解复杂的声明,这些声明本应该使用一系列typedefs来编写。然而,下面的评论让我感到惊讶:

一个经常引用的简化规则,仅适用于少数几种简单的情况。

我并不认为 void (*signal(int, void (*fp)(int)))(int); 是一个“简单的情况”。这让我更加担忧。

因此,我的问题是:在哪些情况下使用这个规则是正确的,哪些情况下使用它会出错?


3
我建议你在出错时向 Vorac 寻求帮助;我不知道它会在什么情况下出错。 - Jonathan Leffler
@Jonathan Leffler,所以没有已知的问题,螺旋规则从未出错? - Vorac
1
好的 - 所以是James Kanze对1994年发布的文章有看法,而不是你;对于错误的归属感到抱歉。我不知道James反对它的理由是什么;我不知道在哪种情况下它会失败(但我没有认真研究过)。这并不意味着没有这样的情况,但我认为像这样未经解释、未经证实的评论只是一些无用之言。 - Jonathan Leffler
1
@JonathanLeffler即使在一些简单的声明中,例如int* a[][10];,也会出现错误。否则,您重新定义“螺旋”,最终得到的结果甚至比实际定义更糟糕。 - James Kanze
3
int* a[][10] 这个例子中,右左法则 是适用的。你知道还有哪些情况下这个规则是无效的,例如螺旋规则吗?基本上我正在试图找到一条支配所有规则的规则。 - legends2k
“右左法则”(Right-left rule),上述链接无法访问。 - legends2k
4个回答

23

基本上说,这个规则根本不起作用,要不就是通过重新定义螺旋的含义来实现(如果是这样的话,那就没有意义了)。例如:

int* a[10][15];

螺旋法给出的是一个指向大小为15的int数组的指针数组[10],这是错误的。在你提到的情况下,它也不适用;事实上,在signal的情况下,甚至不清楚应从哪里开始螺旋。

一般来说,更容易找到规则失败的例子,而不是它起作用的例子。

我经常想说解析C++声明很简单,但尝试过复杂声明的人不会相信我。另一方面,它并不像有时所说的那样难。秘诀是将声明视为表达式,但运算符较少,并且有一个非常简单的优先级规则:右侧所有运算符的优先级高于左侧所有运算符。在没有括号的情况下,这意味着首先处理右侧的所有内容,然后处理左侧的所有内容,并且像在任何其他表达式中一样处理括号。实际的困难不在于语法本身,而在于它导致了一些非常复杂和反直觉的声明,特别是涉及函数返回值和指向函数的指针的情况:第一个右侧,然后左侧的规则意味着特定级别的运算符通常相距很远,例如:

int (*f( /* lots of parameters */ ))[10];
这里展开式的最后一个术语是int[10], 但将[10]放在完整函数说明之后(至少对我来说)非常不自然,每次都需要停下来仔细思考。 (可能是这种逻辑上相邻的部分扩散的趋势导致了螺旋规则。问题在于,在没有括号的情况下,它们并不总是扩散-任何时候看到[i][j],规则是向右移动,然后再向右移动,而不是螺旋。)
现在我们正在从表达式的角度考虑声明:当表达式变得太复杂以至于难以阅读时,该怎么办?您引入中间变量以使其更易于阅读。 在声明的情况下,“中间变量”是typedef。 特别是,我认为任何时候返回类型的一部分出现在函数参数之后(以及许多其他时间),您都应该使用typedef使声明更简单。(但是这是一个“说话容易,做起来难”的规则。恐怕我偶尔会使用一些非常复杂的声明。)

没有括号的情况下,这意味着先处理左侧的所有内容,然后再处理右侧的所有内容。难道不应该是相反的吗?正如你自己所说,右侧的所有运算符优先于左侧的所有运算符。因此应该是:先处理右侧,然后处理左侧。 - AnT stands with Russia
@AnT 是的。我肯定打错了那个语句。我会纠正它,谢谢。(而且到现在为止没有人注意到这个错误,真让人惊讶。) - James Kanze
如果你眯起眼睛看,这就是一个螺旋形:先处理所有右边的内容,然后再处理左边的内容,在处理括号时与任何其他表达式一样。 - creanion

9
螺旋规则实际上是一种过于复杂的看待方式。实际规则要简单得多:“声明从其名称开始,然后按照顺序阅读。”
postfix is higher precedence than prefix.

就是这样。这是你需要记住的所有内容。当你有括号来覆盖后缀优先于前缀时,会出现“复杂”情况,但你只需要找到匹配的括号,然后首先查看括号内的内容,如果不完整,则拉入下一个级别的内容,先处理后缀。

所以看看你的复杂例子:

void (*signal(int, void (*fp)(int)))(int);

我们可以从任何名称开始,找出它表示的含义。如果你从“int”开始,那么你就完成了 - “int”是一种类型,你可以通过自己理解它。
如果你从“fp”开始,fp不是一种类型,而是一个被声明为某些东西的名称。因此,看看括号中的第一组:
                        (*fp)

在处理后缀之前,没有后缀。然后,前缀*表示指针。指向什么?还不完整,因此要查看另一个级别。

                   void (*fp)(int)

后缀first是“带有int参数的函数”,前缀是“返回void”。因此,我们有fn是“指向带有int参数且返回void的函数的指针”。
如果我们启动一个signal,第一级有一个后缀(函数)和一个前缀(返回指针)。需要看出下一级指向的是什么(返回void的函数)。因此,我们最终得到“带两个参数(int和指向函数的指针)且返回一个带有一个参数(int)且返回void的函数指针的函数”。

6
规则是正确的。然而,应用它时必须非常小心。
建议在C99+声明中更正式地应用它。
这里最重要的事情是要认识到所有声明的以下递归结构(为简单起见,constvolatilestaticexterninlinestructuniontypedef已从图中删除,但可以轻松添加回来):
base-type [derived-part1: *'s] [object] [derived-part2: []'s or ()]

没错,就是这样,四部分。
where

  base-type is one of the following (I'm using a bit compressed notation):
    void
    [signed/unsigned] char
    [signed/unsigned] short [int]
    signed/unsigned [int]
    [signed/unsigned] long [long] [int]
    float
    [long] double
    etc

  object is
      an identifier
    OR
      ([derived-part1: *'s] [object] [derived-part2: []'s or ()])

  * is *, denotes a reference/pointer and can be repeated
  [] in derived-part2 denotes bracketed array dimensions and can be repeated
  () in derived-part2 denotes parenthesized function parameters delimited with ,'s
  [] elsewhere denotes an optional part
  () elsewhere denotes parentheses

一旦你解析了所有的4个部分,

[object]就是[derived-part2(包含/返回)][derived-part2(指向)]base-type1

如果有递归,你可以在递归栈的底部找到你的object(如果有的话),它将是最内层的一个,并且你需要回溯并收集和组合每个递归级别的派生部分,以获得完整的声明。

在解析过程中,你可能会将[object]移动到[derived-part2]之后(如果有的话)。这将给你一个线性化的、易于理解的声明(见上文1)。

因此,在

char* (**(*foo[3][5])(void))[7][9];

你会得到以下内容:
  1. base-type = char
  2. 第一层: derived-part1 = *object = (**(*foo[3][5])(void))derived-part2 = [7][9]
  3. 第二层: derived-part1 = **object = (*foo[3][5])derived-part2 = (void)
  4. 第三层: derived-part1 = *object = fooderived-part2 = [3][5]

从这里开始:

  1. 第三层: * [3][5] foo
  2. 第二层: ** (void) * [3][5] foo
  3. 第一层: * [7][9] ** (void) * [3][5] foo
  4. 最终,char * [7][9] ** (void) * [3][5] foo

现在,从右往左阅读:

foo 是一个由 3 个数组组成的数组,每个数组中有 5 个指向函数(不带参数)返回一个指向包含了 7 个数组的数组,每个数组中有 9 个指向字符的指针。

你也可以在过程中反转每个 derived-part2 中的数组维度。

这就是螺旋规则。

很容易看到螺旋形状。你从左边深入嵌套的 [object],然后从右边浮出来,只注意到上一层还有另一对左右,以此类推。


你可能会提到,当你递归调用时(我会使用嵌套这个术语,但本质上是一样的),声明的嵌套部分不包含你的“基础部分”(除了函数参数,在其中你有一个完整的声明嵌套在另一个声明中)。 - James Kanze
对于你和我来说很明显,但对于那些在解析声明时遇到问题的人来说,可能值得一提。 - James Kanze
@JamesKanze 您怎么认为文本允许在声明中的其他位置添加“基本类型”(除了函数参数列表或具有“sizeof(type)”的数组维度表达式)? - Alexey Frunze
因为你正在进行递归,并且没有提供任何其他声明的语法。 - James Kanze
@JamesKanze 在 where 部分有什么不清楚的吗?还是你忽略了它? - Alexey Frunze
显示剩余2条评论

1

E.g.:

int * a[][5];

这不是指向int数组的指针数组。

5
那么这是什么呢? - yapkm01
声明a为一个5个元素的指针数组的数组,每个元素是一个整型指针。 - creanion

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