Использование goto для чистого выхода из цикла

У меня есть вопрос об использовании оператора goto в C++. Я понимаю, что эта тема противоречива, и меня не интересуют какие-либо широкие советы или аргументы (я обычно отказываюсь от использования 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 становится ложным; и последнее, если конечное условие выполняется в нижней части цикла. Следует также отметить, что main_loop можно запускать и завершать несколько раз в течение срока службы объекта ControlLoop, поэтому он должен завершаться чисто с помощью m_bIsRunning = false после этого.

Кроме того, я понимаю, что мог бы использовать ключевое слово break здесь, но большинство из этих вызовов функций псевдокода внутри main_loop на самом деле не инкапсулированы как функции, просто потому, что им нужно было бы либо иметь много аргументов, либо им всем был бы необходим доступ к переменным-членам. На мой взгляд, оба этих случая были бы более запутанными, чем просто оставить main_loop более длинной функцией, а из-за длины большого цикла while оператор вроде goto loop_end кажется, читается понятнее для меня.

Теперь вопрос: не доставит ли вам неудобства такое решение, если вы напишите его в своем собственном коде? Мне кажется, что это немного неправильно, но я никогда раньше не использовал выражение goto в коде C++ - отсюда мой запрос о помощи от экспертов. Существуют ли другие основные идеи, которые мне не хватает, которые могли бы сделать этот код более понятным?

Благодарю.

7 ответов

Решение

С вашим единственным условием, которое вызывает преждевременное прерывание цикла, я бы просто использовал 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 когда эта функция вернется.

Избегать использования goto это довольно солидная вещь в объектно-ориентированной разработке в целом.

В вашем случае, почему бы просто не использовать break выйти из цикла?

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

Что касается вашего вопроса: ваше использование goto сделало бы меня неудобным. Единственная причина, по которой break менее читабельным является ваш доступ к тому, чтобы не быть сильным разработчиком C++. Любой опытный разработчик языка C-like, break оба будут читать лучше, а также обеспечат более чистое решение, чем goto,

В частности, я просто не согласен с тем, что

if (something)
{
    goto loop_end;
}

более читабельно, чем

if (something)
{
    break;
}

который буквально говорит то же самое со встроенным синтаксисом.

ОБНОВИТЬ

Если ваша основная проблема заключается в том, что цикл while слишком длинный, вы должны стремиться сделать его короче, C++ - это ОО-язык, а ОО - для разделения вещей на мелкие части и компоненты, даже в целом не-ОО-языке, который, как правило, мы по-прежнему считаем должен разбить метод / цикл на маленький и сделать его коротким для чтения. Если в цикле 300 строк, независимо от того, что break / goto действительно не экономит ваше время, не так ли?

ОБНОВИТЬ

Я не против goto, но я не буду использовать его здесь, как вы, я предпочитаю просто использовать break, как правило, разработчику, который видел там перерыв, он знает, что это означает goto до конца, и с этим m_bIsRunning = false, он может легко осознавать, что он действительно выходит из цикла в течение нескольких секунд. Да, goto может сэкономить время на секунды, чтобы понять это, но также может заставить людей нервничать по поводу вашего кода.

Я могу себе представить, что я использую goto, чтобы выйти из двухуровневого цикла:

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

Вместо того, чтобы использовать goto, вы должны использовать break; чтобы избежать петель.

Есть несколько альтернатив goto: break, continue а также return в зависимости от ситуации.

Тем не менее, вы должны иметь в виду, что оба break а также continue ограничены в том, что они влияют только на самый внутренний цикл. 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();
}

Примечание: если ваше тело функции не может удобно разместиться на экране, вы делаете это неправильно.

Я вижу множество людей, предлагающих break вместо goto, Но break нет "лучше" (или "хуже"), чем goto,

Инквизиция против goto фактически начался с статьи Дейкстры "Идем к вредному" еще в 1968 году, когда правилом спагетти был код, а такие вещи, как блочная структура if а также while Заявления все еще считались передовыми. У ALGOL 60 они были, но по сути это был исследовательский язык, используемый учеными (см. ML сегодня); Фортран, один из доминирующих языков того времени, не получал их еще 9 лет!

Основные моменты в работе Дейкстры:

  1. Люди хороши в пространственном мышлении, и программы с блочной структурой извлекают из этого выгоду, потому что действия программ, которые происходят рядом друг с другом во времени, описываются рядом друг с другом в "пространстве" (программный код);
  2. Если вы избегаете goto во всех его различных формах можно узнать вещи о возможных состояниях переменных в каждой лексической позиции в программе. В частности, в конце while цикл, вы знаете, что условие этого цикла должно быть ложным. Это полезно для отладки. (Дейкстра не совсем так говорит, но вы можете сделать вывод.)

break, как goto (и рано return s и исключения...), уменьшает (1) и исключает (2). Конечно, используя break часто позволяет избежать написания запутанной логики для while условие, что дает вам чистый выигрыш в понимании - и точно так же относится к goto,

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;
}
Другие вопросы по тегам