6

我有一个关于在 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 变为假;最后一个是在循环的底部是否满足结束条件。m_bIsRunning = false然后。

另外,我意识到我可以在这里使用 break 关键字,但是 main_loop 中的大多数伪代码函数调用并没有真正封装为函数,仅仅是因为它们要么需要有很多参数,要么都需要访问成员变量。在我看来,这两种情况都会比简单地将 main_loop 保留为更长的函数更令人困惑,并且由于大 while 循环的长度,这样的语句goto loop_end对我来说似乎更清晰。

现在的问题是:如果你用自己的代码编写这个解决方案,会不会让你感到不舒服?我确实觉得有点不对劲,但是我以前从未在 C++ 代码中使用过 goto 语句——因此我请求专家的帮助。还有其他我遗漏的基本想法可以使这段代码更清晰吗?

谢谢。

4

7 回答 7

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

它从字面上说与内置语法相同的东西。

于 2012-10-09T02:44:34.273 回答
3

更新

如果您主要关心的是 while 循环太长,那么您应该着眼于使其更短,C++ 是一种 OO 语言,而 OO 用于将事物拆分为小块和组件,即使在一般的非 OO 语言中,我们通常仍然认为我们应该将一个方法/循环分成小一个,并使其简短易读。如果一个循环中有 300 行,那么无论 break/goto 并不能真正节省您的时间,不是吗?

更新

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

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

while(running) 
{
    ...
    while(runnning2)
    {
        if(bad_data)
        {
            goto loop_end;
        }
    }
    ...
}
loop_end:
于 2012-10-09T02:51:27.120 回答
3

对于导致循环提前中断的单一条件,我只需使用break. 不需要goto就是break为了这个。

但是,如果这些函数调用中的任何一个可能引发异常,或者如果您最终需要多个breaks,我更喜欢 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在该函数返回时调用。

于 2012-10-09T02:58:15.183 回答
1

而不是使用goto,你应该使用break;来逃避循环。

于 2012-10-09T02:44:53.577 回答
1

:有几种替代方法gotobreak具体continue取决于return具体情况。

但是,您需要记住,两者break都是continue有限的,因为它们只影响最内部的循环。return另一方面不受此限制的影响。

通常,如果您使用 a退出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();
}

注意:如果您的函数体不能舒适地放在屏幕上,则说明您做错了。

于 2012-10-09T06:44:16.050 回答
0

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

此外,在复杂的例程中,最好只在最后放置一个退出逻辑(带有清理)。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;
}
于 2012-10-09T03:01:40.420 回答
0

我看到很多人建议break而不是goto. 但是break并不比goto.

goto早在 1968 年, Dijkstra 的“Go To Considered Harmful”论文就开始有效地进行调查,当时意大利面条代码是规则,块结构ifwhile语句之类的东西仍然被认为是尖端的。ALGOL 60 有它们,但它本质上是学术界使用的一种研究语言(参见今天的 ML);Fortran,当时的主流语言之一,再过 9 年都不会得到它们!

Dijkstra 论文的要点是:

  1. 人类擅长空间推理,而块结构程序利用了这一点,因为在时间上彼此靠近的程序动作在“空间”(程序代码)中被描述为彼此靠近;
  2. 如果你避免goto各种形式,那么就有可能了解程序中每个词汇位置的变量可能状态。特别是,在while循环结束时,您知道该循环的条件必须为假。这对于调试很有用。(Dijkstra 并没有完全这么说,但你可以推断出来。)

break, 就像goto(and early returns, and exceptions...) 一样,减少 (1) 并消除 (2)。当然,break经常使用可以避免为while条件编写复杂的逻辑,从而获得可理解性的净收益——这同样适用于goto.

于 2012-10-09T06:53:14.647 回答