使用goto干净地退出循环

时间:2012-10-09 02:41:53

标签: c++ while-loop goto

我有一个关于在C ++中使用goto语句的问题。我理解这个话题是有争议的,并且对任何广泛的建议或论点都不感兴趣(我通常偏离使用goto)。相反,我有一个特定的情况,并想要了解我的解决方案,它使用goto语句,是否是一个好的。我不会称自己是C ++的新手,但也不会将自己归类为专业级程序员。生成我的问题的代码部分一旦启动就会在无限循环中旋转。伪代码中线程的一般流程如下:

void ControlLoop::main_loop()
{
    InitializeAndCheckHardware(pHardware) //pHardware is a pointer given from outside
    //The main loop
    while (m_bIsRunning)
    {
        simulated_time += time_increment; //this will probably be += 0.001 seconds
        ReadSensorData();
        if (data_is_bad) {
            m_bIsRunning = false;
            goto loop_end;
        }    
        ApplyFilterToData();
        ComputeControllerOutput();
        SendOutputToHardware();
        ProcessPendingEvents();

        while ( GetWallClockTime() < simulated_time ) {}
        if ( end_condition_is_satisified ) m_bIsRunning = false;
    }
    loop_end:
    DeInitializeHardware(pHardware);
}

pHardware指针从ControlLoop对象外部传入并具有多态类型,因此对我来说使用RAII并在main_loop中创建和破坏硬件接口本身没有多大意义。我想我可以让pHardware创建一个临时对象,代表硬件的“会话”或“使用”,可以在main_loop退出时自动清理,但我不确定这个想法是否会让某些人更清楚否则我的意图是什么。循环中只有三种方式:第一种方法是从外部硬件读取坏数据;第二个是如果ProcessPendingEvents()指示用户启动的中止,这只会导致m_bIsRunning变为false;最后一个是如果在循环的底部满足结束条件。我还应该注意,main_loop可以在ControlLoop对象的生命周期内多次启动和完成,因此它应该在m_bIsRunning = false之后干净地退出。

另外,我意识到我可以在这里使用break关键字,但是main_loop中的大多数伪代码函数调用并没有真正封装为函数,只是因为它们需要有很多参数,或者它们都需要访问成员变量。在我看来,这两种情况都比让简单地将main_loop作为一个更长的函数更容易混淆,而且由于big while循环的长度,像goto loop_end这样的语句似乎对我来说更清晰。

现在提出一个问题:如果您在自己的代码中编写它,这个解决方案会让您感到不舒服吗?它对我来说确实有些不对劲,但之后我从未在C ++代码中使用过goto语句 - 因此我请求专家提供帮助。我还缺少哪些其他基本想法可以使这些代码更清晰?

感谢。

7 个答案:

答案 0 :(得分:5)

在面向对象的开发中,避免使用goto是非常可靠的事情。

在您的情况下,为什么不使用break退出循环?

while (true)
{
    if (condition_is_met)
    {
        // cleanup
        break;
    }
}

关于你的问题:你使用goto会让我感到不舒服。 break可读性较差的唯一原因是您不能成为强大的C ++开发人员。对于任何经验丰富的类似C语言的开发人员,break都会更好地阅读,并提供比goto更清晰的解决方案。

特别是,我根本不同意

if (something)
{
    goto loop_end;
}

更具可读性
if (something)
{
    break;
}

字面意思是内置语法相同的东西。

答案 1 :(得分:3)

<强>更新

如果你主要担心的是while循环太长,那么你的目标应该是缩短它,C ++是一种OO语言,而OO则用于分割小块和组件,即使在一般的非OO语言中我们一般仍然认为我们应该将一个方法/循环分解为一个小方法,并使其简单易读。如果一个循环中有300行,那么无论是break / goto还是没有真正节省你的时间就没有了吗?

<强>更新

我并不反对 goto ,但我不会像你一样使用它,我更喜欢使用 break ,一般来说,他看到了一个< strong> break 在那里,他知道这意味着转到最后,并且凭借 m_bIsRunning = false ,他可以很容易地意识到它实际上是在几秒钟内退出循环。是的,转到可以节省几秒钟的时间来理解它,但也可能让人们对您的代码感到紧张。

我可以想象我正在使用goto将退出两级循环:

while(running) 
{
    ...
    while(runnning2)
    {
        if(bad_data)
        {
            goto loop_end;
        }
    }
    ...
}
loop_end:

答案 2 :(得分:3)

使用你的那个,奇怪的条件导致循环提前破坏我只会使用breakgoto不需要break

但是,如果那些函数调用中的任何都会抛出异常,或者如果你最终需要多个break,我宁愿使用RAII样式的容器,这就是确切的事情。析构函数是为了。您始终执行对DeInitializeHardware的调用,因此......

// todo: add error checking if needed
class HardwareWrapper {
public:
    HardwareWrapper(Hardware *pH) 
      : _pHardware(pH) { 
        InitializeAndCheckHardware(_pHardware);
    }

    ~HardwareWrapper() {
        DeInitializeHardware(_pHardware);
    }

    const Hardware *getHardware() const {
        return _pHardware;
    }

    const Hardware *operator->() const {
        return _pHardware;
    }

    const Hardware& operator*() const {
        return *_pHardware;
    }

private:
    Hardware *_pHardware;
    // if you don't want to allow copies...
    HardwareWrapper(const HardwareWrapper &other);
    HardwareWrapper& operator=(const HardwareWrapper &other);
}

// ...

void ControlLoop::main_loop()
{
    HardwareWrapper hw(pHardware);
    // code
}

现在,无论发生什么情况,当该函数返回时,您将始终调用DeInitializeHardware

答案 3 :(得分:1)

不应使用goto,而应使用break;来转义循环。

答案 4 :(得分:1)

gotobreakcontinuereturn有多种替代方案,视情况而定。

但是,您需要记住,breakcontinue都受到限制,因为它们只会影响最内层循环。另一方面,return不受此限制的影响。

通常,如果您使用goto 退出特定范围,则可以使用其他函数和return语句进行重构。它可能会使代码更容易阅读作为奖励:

// Original
void foo() {
    DoSetup();
    while (...) {
        for (;;) {
            if () {
                goto X;
            }
        }
    }
    label X: DoTearDown();
}

// Refactored
void foo_in() {
    while (...) {
        for (;;) {
            if () {
                return;
            }
        }
    }
}

void foo() {
    DoSetup();
    foo_in();
    DoTearDown();
}

注意:如果你的功能体不能舒服地放在你的屏幕上,你就错了。

答案 5 :(得分:0)

break是一个选项时,Goto不是退出循环的好习惯。

此外,在复杂的例程中,最好只放置一个退出逻辑(带清理)。 Goto有时用于跳转到返回逻辑。

来自QEMU vmdk块驱动程序的示例:

static int vmdk_open(BlockDriverState *bs, int flags)
{
    int ret;
    BDRVVmdkState *s = bs->opaque;

    if (vmdk_open_sparse(bs, bs->file, flags) == 0) {
        s->desc_offset = 0x200;
    } else {
        ret = vmdk_open_desc_file(bs, flags, 0);
        if (ret) {
            goto fail;
        }
    }
    /* try to open parent images, if exist */
    ret = vmdk_parent_open(bs);
    if (ret) {
        goto fail;
    }
    s->parent_cid = vmdk_read_cid(bs, 1);
    qemu_co_mutex_init(&s->lock);

    /* Disable migration when VMDK images are used */
    error_set(&s->migration_blocker,
              QERR_BLOCK_FORMAT_FEATURE_NOT_SUPPORTED,
              "vmdk", bs->device_name, "live migration");
    migrate_add_blocker(s->migration_blocker);

    return 0;

fail:
    vmdk_free_extents(bs);
    return ret;
}

答案 6 :(得分:0)

我看到很多人建议break而不是goto。但breakgoto相比并非“更好”(或“更差”)。

goto的调查有效地开始于1968年Dijkstra的"Go To Considered Harmful" paper,当时意大利面条代码是规则,而块结构ifwhile陈述之类的东西仍然存在被认为是尖端的。 ALGOL 60拥有它们,但它本质上是学术界使用的一种研究语言(参见今天的ML);当时占主导地位的语言之一的Fortran不会再获得9年了!

Dijkstra论文的主要观点是:

  1. 人类擅长空间推理,块结构程序利用这一点,因为在“空间”(程序代码)中彼此靠近的程序动作在彼此附近描述;
  2. 如果你以各种形式避免goto,那么就可以知道关于程序中每个词汇位置的变量的可能状态。特别是,在while循环结束时,您知道该循环的条件必须为false。这对调试很有用。 (Dijkstra并没有这么说,但你可以推断它。)
  3. break,就像goto(和早期return一样,以及例外...),减少(1)并消除(2)。当然,使用break通常可以避免为while条件编写复杂的逻辑,从而在可理解性方面获得净收益 - 而goto则完全相同。