一个似乎是无限循环的'for'循环

82

我现在正在调试一些代码,而我遇到了这一行:

for (std::size_t j = M; j <= M; --j)

(由我正在度假的老板撰写。)

对我来说,这看起来非常奇怪。

它是干什么的?对我来说,它看起来像一个无限循环。


31
size_t 是无符号的,因此在尝试将零作为循环结束标志时,保证会绕回到其最大值。然而,这段代码仍然很糟糕。 - BoBTFish
4
如果size_t的最大值恰好是M,你是否仍然认为这很聪明? - Thorsten Dittmar
8
@Barmar “不经常出现”并不等同于“从不出现”,因此存在概率大于零的可能性。这意味着当有更简单的方法时,这种解决方案是愚蠢的。 - Thorsten Dittmar
38
我不明白为什么会被踩。这个问题很清楚 - 我看不出有什么需要改进的地方。对于一个新用户来说,这个问题实际上是 有趣的 - Bathsheba
17
#define TRUE FALSE and go on holiday.将被翻译为:将TRUE定义为FALSE,然后去度假。` - Leo Heinsaar
显示剩余9条评论
2个回答

68

std::size_t 是 C++ 标准保证的一个 unsigned 类型。如果你从 0 减去一个 unsigned 类型,标准保证这样做的结果是该类型的最大值。

这个被包裹的值总是大于等于 M1,所以循环会终止。

因此,对于 unsigned 类型,j <= M 是一种方便的表示“运行循环到 0 然后停止”的方式。

还有其他选择,比如将 j 运行得更多,甚至使用滑动运算符 for (std::size_t j = M + 1; j --> 0; ){,虽然这些选择可能更清晰,但需要输入更多内容。不过,我想另外一个缺点(除了在首次检查时产生的困惑效应之外)是它不能很好地移植到没有无符号类型的语言,比如 Java。

还要注意,你的老板选择的方案“借用”了一个可能的值从 unsigned 集合中:在这种情况下,设置为 std::numeric_limits<std::size_t>::max()M 将没有正确的行为。 事实上,在这种情况下,循环是无限的。(你是否观察到了这一点?)你应该在代码中插入一个注释来说明这一点,并可能在特定条件下进行断言。


1 假设 M 不是 std::numeric_limits<std::size_t>::max()


7
我责怪天气。 - Hatted Rooster
3
如果M等于std::numeric_limits<std::size_t>::max(),那么M+1会变成零,而for (std::size_t j = M + 1; j --> 0; )循环就不会执行。 - Pete Kirkham
51
不要称它为“滑动运算符”。在C++中并没有这种运算符,这只是一种看起来聪明却含混不清的表达方式。 - You
26
因为它不是一个运算符。它是两个运算符,带有奇怪的空格。如果你一定要问与面试者无关的问题,就称其为习语吧(但最好询问他们如何实现功能,而不是询问“聪明”的语法)。 - You
1
假设 size_t 是64位,除非优化器可以消除循环,否则需要几百年才能观察到边缘情况下的错误行为。 - Carsten S
显示剩余5条评论

27

你的老板大概是想从M倒数到0(包括0),并对每个数字执行一些操作。

不幸的是,存在一种边缘情况,会导致你进入一个无限循环,即M是您可以拥有的最大size_t值的情况。虽然当您从零减去无符号值时,它会执行定义良好的操作,但我认为代码本身是松散思维的例子,尤其是因为有一个完全可行的解决方案,而没有老板尝试中的缺陷。

更安全的变体(在我的意见中也更易读,同时保持紧密的范围限制)将是:

{
    std::size_t j = M;
    do {
        doSomethingWith(j);
    } while (j-- != 0);
}

以以下代码为例:

#include <iostream>
#include <cstdint>
#include <climits>
int main (void) {
    uint32_t quant = 0;
    unsigned short us = USHRT_MAX;
    std::cout << "Starting at " << us;
    do {
        quant++;
    } while (us-- != 0);
    std::cout << ", we would loop " << quant << " times.\n";
    return 0;
}

这个代码基本上使用 unsigned short 做了同样的事情,你可以看到它处理了每一个值:

Starting at 65535, we would loop 65536 times.

将上面代码中的do..while循环替换为你的老板基本上做的事情会导致无限循环。尝试一下:

for (unsigned int us2 = us; us2 <= us; --us2) {
    quant++;
}

7
现在,边际情况是 0。为什么不使用 for(size_t j = M; j-- != 0; ) - LogicStuff
是的,这确实存在一个角落情况,可能比 numeric_limits<size_t>::max() 更频繁。 - Bathsheba
7
@Logic等人,我提供的方法中没有边缘情况,我已添加代码以显示此点。而使用您提出的解决方案,如果初始值为M == 0,则元素0将不会被处理,因此确实存在边缘情况。使用我的后检查do..while方法完全消除了边缘情况。如果您尝试将其设置为M == 1,您会发现它会处理1和0。同样,如果以max_size_t(无论它是什么)开始,则会成功地从该点开始递减,直到包括零。 - paxdiablo
有趣的是,这个案例激发了使用一些人会说是“非结构化代码”的动机,因为它使用了一个“exitif”,即 { j =M; for(;;){ f(j); if( j == 0 )break; j -= 1; } }。如果嵌套的情况变得复杂,甚至可能需要用goto替换break,因为C语言没有命名循环。如果“结构化”意味着“容易对块进行推理”,那么它就是结构化的(布局有助于!),并且如果它意味着“容易通过推理关于前置条件和后置条件进行正式验证”。虽然在这种情况下j--可以工作,但当需要更复杂的转换时,exitif风格可能是合理的。 - PJTraill
@PJTraill 我无法确定你所说的是结构化的还是非结构化的。对于 do-while 的推理很简单。它并没有以任何更不结构化的方式“使用”退出,就像 while-do 一样。 - philipxy
@philipxy:如果我说话有点晦涩,很抱歉。我想到了一个同事对一种语言(DECTPU)的反应,其中包含一个 exitif 关键字:他用荷兰语不满地说:“但这不是结构化的!” 这让我思考人们所谓的“结构化”是什么意思,以及为什么 Dijkstra 等人引入了这个概念。在这种情况下,我建议某些人 - 如我的同事 - 可能会称我的评论中的 C 片段为“非结构化”,尽管它有其优点。我没有将 do…whilewhile…do 进行对比。 - PJTraill

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