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

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

if (something)
{
    goto loop_end;
}

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

if (something)
{
    break;
}

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


обновление

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

обновление

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

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

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

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


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

однако, вы должны иметь в виду, что оба break и continue только в том, что они затрагивают только самый внутренний цикл. return С другой стороны, это ограничение не влияет.

в общем, если вы используете goto to выход определенной области, то вы можете рефакторинг с помощью другой функции и заявление. Вполне вероятно, что это облегчит чтение кода в качестве бонуса:

// 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();
}

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


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

я вижу множество людей, предложив break вместо goto. Но!--0--> не "лучше" (или "хуже"), чем goto.

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

основные моменты в статье Дийкстры:

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

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