Выгодно ли когда-нибудь использовать "goto" на языке, который поддерживает циклы и функции?Если да, то почему?

StackOverflow https://stackoverflow.com/questions/24451

Вопрос

У меня давно сложилось впечатление, что goto никогда не следует использовать, если это возможно.Просматривая на днях libavcodec (который написан на C), я заметил его многократное использование.Выгодно ли когда-нибудь использовать goto на языке, который поддерживает циклы и функции?Если да, то почему?

Это было полезно?

Решение

Есть несколько причин для использования оператора "goto", о которых я знаю (некоторые уже говорили об этом):

Чистый выход из функции

Часто в функции вы можете распределять ресурсы, и вам нужно завершить работу в нескольких местах.Программисты могут упростить свой код, поместив код очистки ресурсов в конец функции, и все "точки выхода" функции будут помечены меткой очистки.Таким образом, вам не нужно писать код очистки в каждой "точке выхода" функции.

Выход из вложенных циклов

Если вы находитесь во вложенном цикле и вам нужно выйти из ВСЕ циклы, goto могут сделать это намного чище и проще, чем операторы break и if-проверки.

Низкоуровневые улучшения производительности

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

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

Другие советы

Все, кто настроен против-goto цитирует, прямо или косвенно, Эдсгера Дейкстру ГоТо Считается Вредным статья, обосновывающая их позицию.Очень жаль, что статья Дейкстры практически ничего что делать с тем, как goto в наши дни используются инструкции, и поэтому то, о чем говорится в статье, практически неприменимо к современной сцене программирования.Тот Самый goto- теперь меньше мемов граничит с религией, вплоть до ее священных писаний, продиктованных свыше, ее первосвященников и избегания (или того хуже) предполагаемых еретиков.

Давайте рассмотрим статью Дейкстры в контексте, чтобы пролить немного света на эту тему.

Когда Дейкстра писал свою статью, популярными языками того времени были неструктурированные процедурные, такие как BASIC, FORTRAN (более ранние диалекты) и различные языки ассемблера.Это было довольно распространенным явлением для людей, использующих языки более высокого уровня, чтобы перепрыгнуть по всей их кодовой базе в искривленных потоках выполнения, которые породили термин "спагетти-код".Вы можете убедиться в этом, перейдя к классическая игра Trek автор сценария - Майк Мэйфилд, пытающийся понять, как все работает.Потратьте несколько минут, чтобы просмотреть это.

ЭТО это "необузданное использование инструкции go to", против которого Дийкстра выступал в своей статье в 1968 году. ЭТО это среда, в которой он жил, которая побудила его написать эту статью.Возможность переходить в любое удобное для вас место в вашем коде в любой момент, которая вам нравилась, была тем, что он критиковал и требовал прекратить.Сравнивая это с анемичными способностями goto на C или других подобных более современных языках это просто смешно.

Я уже слышу громкие песнопения культистов, когда они стоят лицом к лицу с еретиком."Но, - будут скандировать они, - вы можете сделать код очень трудным для чтения с помощью goto в "С". "Ах да?Вы можете сделать код очень трудным для чтения без goto также хорошо.Как этот:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

Ни один goto на виду, значит, это должно быть легко читаемо, не так ли?Или как насчет этого:

a[900];     b;c;d=1     ;e=1;f;     g;h;O;      main(k,
l)char*     *l;{g=      atoi(*      ++l);       for(k=
0;k*k<      g;b=k       ++>>1)      ;for(h=     0;h*h<=
g;++h);     --h;c=(     (h+=g>h     *(h+1))     -1)>>1;
while(d     <=g){       ++O;for     (f=0;f<     O&&d<=g
;++f)a[     b<<5|c]     =d++,b+=    e;for(      f=0;f<O
&&d<=g;     ++f)a[b     <<5|c]=     d++,c+=     e;e= -e
;}for(c     =0;c<h;     ++c){       for(b=0     ;b<k;++
b){if(b     <k/2)a[     b<<5|c]     ^=a[(k      -(b+1))
<<5|c]^=    a[b<<5      |c]^=a[     (k-(b+1     ))<<5|c]
;printf(    a[b<<5|c    ]?"%-4d"    :"    "     ,a[b<<5
|c]);}      putchar(    '\n');}}    /*Mike      Laman*/

НЕТ goto там тоже.Поэтому он должен быть удобочитаемым.

К чему я клоню с этими примерами?Это не языковые особенности, которые делают код нечитаемым, не поддерживаемым.Это делает не синтаксис.Это из-за плохих программистов.А плохие программисты, как вы можете видеть из приведенного выше пункта, могут создавать Любой языковая функция нечитабельна и непригодна для использования.Как в for петли там, наверху.(Ты ведь можешь их видеть, верно?)

Справедливости ради, некоторыми языковыми конструкциями легче злоупотреблять, чем другими.Однако, если вы программист на C, я бы гораздо внимательнее изучил примерно 50% случаев использования #define задолго до того, как я отправился в крестовый поход против goto!

Итак, для тех, кто потрудился дочитать до этого места, следует отметить несколько ключевых моментов.

  1. Статья Дейкстры о goto statements был написан для среды программирования, в которой goto был лот более потенциально опасный, чем в большинстве современных языков, которые не являются ассемблерными.
  2. Автоматически отбрасывая все виды использования goto потому что это примерно так же рационально, как сказать: "Однажды я пытался повеселиться, но мне это не понравилось, и теперь я против этого".
  3. Существуют законные способы применения современных (малокровных) goto отчетность в коде, который не может быть адекватно заменена другими конструкциями.
  4. Конечно, существуют случаи незаконного использования одних и тех же утверждений.
  5. Существуют также незаконные виды использования современных управляющих операторов, таких как "godo"мерзость, где всегда-ложь do цикл разорван из-за использования break вместо goto.Это часто хуже, чем разумное использование goto.

Слепое следование лучшим практикам - это не самая лучшая практика.Идея избегать goto инструкции как основная форма управления потоком предназначены для того, чтобы избежать создания нечитаемого кода-спагетти.Если их использовать экономно в нужных местах, иногда они могут быть самым простым и ясным способом выражения идеи.Уолтер Брайт, создатель компилятора Zortech C ++ и языка программирования D, использует их часто, но разумно.Даже с учетом goto тем не менее, его код по-прежнему прекрасно читаем.

Итог:Избегая goto ради того, чтобы избежать goto это бессмысленно.Чего вы действительно хотите избежать, так это создания нечитаемого кода.Если ваш goto-загруженный код читаем, значит, в нем нет ничего плохого.

С тех пор как goto затрудняет рассуждения о потоке выполнения программы1 (он же.“спагетти-код”), goto обычно используется только для компенсации отсутствующих функций:Использование goto на самом деле это может быть приемлемо, но только в том случае, если язык не предлагает более структурированный вариант для достижения той же цели.Возьмем пример Сомнения:

Правило с goto, которое мы используем, заключается в том, что goto подходит для перехода к единственной точке очистки выхода в функции.

Это верно, но только если язык не допускает структурированную обработку исключений с помощью кода очистки (например, RAII или finally), который выполняет ту же работу лучше (поскольку он специально создан для этого), или когда есть веская причина не использовать структурированную обработку исключений (но у вас никогда не будет такого случая, кроме как на очень низком уровне).

В большинстве других языков единственное приемлемое использование goto заключается в выходе из вложенных циклов.И даже там почти всегда лучше перевести внешний цикл в собственный метод и использовать return вместо этого.

Кроме этого, goto это признак того, что в конкретный фрагмент кода было вложено недостаточно внимания.


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

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

Что ж, есть одна вещь, которая всегда хуже, чем goto's;странное использование других операторов programflow, чтобы избежать перехода:

Примеры:

    // 1
    try{
      ...
      throw NoErrorException;
      ...
    } catch (const NoErrorException& noe){
      // This is the worst
    } 


    // 2
    do {
      ...break; 
      ...break;
    } while (false);


    // 3
    for(int i = 0;...) { 
      bool restartOuter = false;
      for (int j = 0;...) {
        if (...)
          restartOuter = true;
      if (restartOuter) {
        i = -1;
      }
    }

etc
etc

В C# переключатель заявление не допускайте провала.Итак гото используется для передачи управления на конкретную этикетку корпуса выключателя или на По умолчанию label.

Например:

switch(value)
{
  case 0:
    Console.Writeln("In case 0");
    goto case 1;
  case 1:
    Console.Writeln("In case 1");
    goto case 2;
  case 2:
    Console.Writeln("In case 2");
    goto default;
  default:
    Console.Writeln("In default");
    break;
}

Редактировать:Есть одно исключение из правила "без провалов".Резервный вариант разрешен, если оператор case не содержит кода.

#ifdef TONGUE_IN_CHEEK

Perl имеет goto это позволяет вам реализовывать хвостовые вызовы бедняков.:-П

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

Ладно, значит, это не имеет никакого отношения к C goto.Если говорить более серьезно, я согласен с другими комментариями по поводу использования goto для очистки или для реализации Устройство Даффа, или что-то подобное.Все дело в использовании, а не в злоупотреблении.

(Тот же комментарий может быть применим к longjmp, исключения, call/cc, и тому подобное - они имеют законное применение, но ими легко можно злоупотреблять.Например, выбрасывание исключения исключительно для того, чтобы избежать глубоко вложенной структуры управления, при совершенно не исключительных обстоятельствах.)

За эти годы я написал более нескольких строк на ассемблере.В конечном счете, каждый язык высокого уровня компилируется вплоть до gotos.Ладно, называйте их "ответвлениями", или "прыжками", или как угодно еще, но это goto.Кто-нибудь может написать ассемблер без перехода?

Теперь, конечно, вы можете указать программисту на Fortran, C или BASIC, что запустить riot с gotos - это рецепт приготовления спагетти болоньезе.Однако ответ заключается не в том, чтобы избегать их, а в том, чтобы использовать их осторожно.

Нож можно использовать для приготовления пищи, освобождения кого-то или убийства.Обходимся ли мы без ножей из-за страха перед последними?Аналогично, goto:при небрежном использовании это мешает, при осторожном использовании это помогает.

Взгляните на Когда Использовать Goto При программировании на C:

Хотя использование goto почти всегда является плохой практикой программирования (конечно, вы можете найти лучший способ сделать XYZ), бывают случаи, когда это действительно неплохой выбор.Кто-то может даже возразить, что, когда это полезно, это лучший выбор.

Большая часть того, что я должен сказать о goto, на самом деле применима только к C.Если вы используете C ++, нет веских причин использовать goto вместо исключений.Однако в C у вас нет возможностей механизма обработки исключений, поэтому, если вы хотите отделить обработку ошибок от остальной логики вашей программы и избежать многократного перезаписи кода очистки по всему вашему коду, то goto может быть хорошим выбором.

Что я имею в виду?Возможно, у вас есть какой-то код, который выглядит примерно так:

int big_function()
{
    /* do some work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* clean up*/
    return [success];
}

Это нормально, пока вы не поймете, что вам нужно изменить код очистки.Затем вы должны пройти через это и внести 4 изменения.Теперь вы можете решить, что можете просто инкапсулировать всю очистку в одну функцию;это неплохая идея.Но это означает, что вам нужно быть осторожным с указателями - если вы планируете освободить указатель в своей функции очистки, нет способа установить для него значение then point равным NULL, если вы не передадите указатель на указатель.Во многих случаях вы все равно больше не будете использовать этот указатель, так что это не может быть серьезной проблемой.С другой стороны, если вы добавите новый указатель, дескриптор файла или другую вещь, требующую очистки, вам нужно будет снова изменить свою функцию очистки;и затем вам нужно будет изменить аргументы этой функции.

Используя goto, это будет

int big_function()
{
    int ret_val = [success];
    /* do some work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
end:
    /* clean up*/
    return ret_val;
}

Преимущество здесь в том, что ваш код, следующий за end, имеет доступ ко всему, что ему понадобится для выполнения очистки, и вам удалось значительно сократить количество точек изменения.Еще одним преимуществом является то, что вы перешли от нескольких точек выхода для своей функции к одной;нет никаких шансов, что вы случайно вернетесь из функции без очистки.

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


Одним словом, goto всегда следует использовать экономно и в качестве последнего средства - но для этого есть время и место.Вопрос должен быть не "обязательно ли вам это использовать", а "является ли это лучшим выбором" для его использования.

Одна из причин, по которой goto плох, помимо стиля кодирования, заключается в том, что вы можете использовать его для создания перекрывающиеся, но не вложенный петли:

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

Это создало бы причудливую, но, возможно, законную структуру потока управления, где возможна последовательность типа (a, b, c, b, a, b, a, b, ...), что делает хакеров-компиляторов несчастными.Очевидно, существует ряд хитроумных приемов оптимизации, которые полагаются на то, что этот тип структуры не возникает.(Я должен проверить свою копию книги дракона ...) Результатом этого может (при использовании некоторых компиляторов) быть то, что другие оптимизации не выполняются для кода, который содержит gotos.

Это могло бы быть полезно, если бы вы знать это просто "о, кстати" убеждает компилятор выдавать более быстрый код.Лично я бы предпочел попытаться объяснить компилятору, что вероятно, а что нет, прежде чем использовать такой трюк, как goto, но, возможно, я также мог бы попробовать goto перед взломом ассемблера.

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

Чтобы проиллюстрировать это, я приведу вам пример, который здесь еще никто не показывал:

Сегодня я писал код для вставки элемента в хэш-таблицу.Хэш-таблица - это кэш предыдущих вычислений, который может быть перезаписан по желанию (влияющий на производительность, но не на корректность).

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

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

Теперь, если бы я не использовал goto, как бы выглядел этот код?

Что - то вроде этого:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

Это выглядело бы все хуже и хуже, если бы добавлялось больше проходов, в то время как версия с goto всегда сохраняет один и тот же уровень отступа и позволяет избежать использования ложных операторов if, результат которых подразумевается выполнением предыдущего цикла.

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

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

Наиболее вдумчивым и тщательным обсуждением операторов goto, их законного использования и альтернативных конструкций, которые могут использоваться вместо "добродетельных операторов goto", но которыми можно злоупотреблять так же легко, как и операторами goto, является статья Дональда Кнута "Структурированное программирование с операторами goto", в декабрьских компьютерных обзорах 1974 года (том 6, №4.п.п.261 - 301).

Неудивительно, что некоторые аспекты этой статьи 39-летней давности датированы:Увеличение вычислительной мощности на порядки сделало некоторые улучшения производительности Knuth незаметными для задач среднего размера, и с тех пор были изобретены новые конструкции на языке программирования.(Например, блоки try-catch включают конструкцию Зана, хотя они редко используются таким образом.) Но Кнут охватывает все стороны аргументации, и его следует обязательно прочитать, прежде чем кто-либо снова поднимет этот вопрос.

В модуле Perl иногда требуется создавать подпрограммы или замыкания "на лету".Дело в том, что после того, как вы создали подпрограмму, как вы к ней доберетесь?Вы могли бы просто вызвать его, но тогда, если подпрограмма использует caller() это будет не так полезно, как могло бы быть.Именно там находится goto &subroutine вариации могут быть полезны.

Вот краткий пример:

sub AUTOLOAD{
  my($self) = @_;
  my $name = $AUTOLOAD;
  $name =~ s/.*:://;

  *{$name} = my($sub) = sub{
    # the body of the closure
  }

  goto $sub;

  # nothing after the goto will ever be executed.
}

Вы также можете использовать эту форму goto обеспечить элементарную форму оптимизации конечных вызовов.

sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;

  $tally *= $n--;
  @_ = ($n,$tally);
  goto &factorial;
}

( В Perl 5 версия 16 это было бы лучше записать как goto __SUB__; )

Существует модуль, который будет импортировать tail модификатор и тот, который будет импортировать recur если вам не нравится использовать эту форму goto.

use Sub::Call::Tail;
sub AUTOLOAD {
  ...
  tail &$sub( @_ );
}

use Sub::Call::Recur;
sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;
  recur( $n-1, $tally * $n );
}

Большинство других причин для использования goto лучше использовать другие ключевые слова.

Нравится redoвводим немного кода:

LABEL: ;
...
goto LABEL if $x;
{
  ...
  redo if $x;
}

Или отправляясь в last фрагмент кода из нескольких мест:

goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
  last if $x;
  ...
  last if $y
  ...
}

Если да, то почему?

C не имеет многоуровневого / помеченного разрыва, и не все потоки управления могут быть легко смоделированы с помощью примитивов итерации и принятия решений C.goto в значительной степени способствуют устранению этих недостатков.

Иногда понятнее использовать какую-либо переменную flag для осуществления своего рода псевдо-многоуровневого разрыва, но это не всегда превосходит goto (по крайней мере, goto позволяет легко определить, куда переходит управление, в отличие от переменной flag), а иногда вы просто не хотите платить цену производительности за флаги / другие искажения, чтобы избежать goto.

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

Точно так же никто никогда не реализовывал оператор "COME FROM"....

Я нахожу использование do{} while (false) совершенно отвратительным.Вполне возможно, что это убедило бы меня в необходимости в каком-то странном случае, но никогда в том, что это чистый разумный код.

Если вам необходимо выполнить какой-то такой цикл, почему бы не сделать зависимость от переменной flag явной?

for (stepfailed=0 ; ! stepfailed ; /*empty*/)

Конечно, можно использовать GOTO, но есть одна более важная вещь, чем стиль кода, или читабельность кода, которую вы должны иметь в виду при его использовании: внутренний код может быть не таким надежным, как вы думаете.

Например, посмотрите на следующие два фрагмента кода:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

Эквивалентный код с помощью GOTO

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

Первое, о чем мы думаем, это то, что результатом обоих битов кода будет это "Значение A:0" (мы, конечно, предполагаем выполнение без параллелизма)

Это неверно:в первом примере A всегда будет равно 0, но во втором примере (с оператором GOTO) A может не быть 0.Почему?

Причина в том, что из другой точки программы я могу вставить GOTO FINAL без контроля значения A.

Этот пример очень очевиден, но по мере усложнения программ сложность восприятия подобных вещей возрастает.

Соответствующий материал можно найти в знаменитой статье мистераDijkstra "Дело против заявления "ПЕРЕЙТИ К делу""

1) Наиболее распространенное использование goto, о котором я знаю, - это эмуляция обработки исключений на языках, которые ее не предлагают, а именно на C.(Код, приведенный Nuclear выше, именно такой.) Посмотрите на исходный код Linux, и вы увидите множество goto, используемых таким образом;согласно быстрому опросу, проведенному в 2013 году, в коде Linux насчитывалось около 100 000 goto: http://blog.regehr.org/archives/894.Использование Goto даже упоминается в руководстве по стилю кодирования Linux: https://www.kernel.org/doc/Documentation/CodingStyle.Точно так же, как объектно-ориентированное программирование эмулируется с использованием структур, заполненных указателями на функции, goto имеет свое место в программировании на языке Си.Так кто же прав:Дейкстра или Линус (и все программисты ядра Linux)?Это теория противв основном тренируйтесь.

Однако существует обычная ошибка из-за отсутствия поддержки на уровне компилятора и проверок на наличие распространенных конструкций / шаблонов:проще использовать их неправильно и вносить ошибки без проверок во время компиляции.Windows и Visual C ++, но в режиме C предлагают обработку исключений через SEH / VEH именно по этой причине:исключения полезны даже за пределами языков ООП, т.е.на процедурном языке.Но компилятор не всегда может сохранить ваш бекон, даже если он предлагает синтаксическую поддержку исключений в языке.Рассмотрим в качестве примера последнего случая знаменитую ошибку Apple SSL "goto fail", которая просто дублировала один goto с катастрофическими последствиями (https://www.imperialviolet.org/2014/02/22/applebug.html):

if (something())
  goto fail;
  goto fail; // copypasta bug
printf("Never reached\n");
fail:
  // control jumps here

У вас может возникнуть точно такая же ошибка, используя исключения, поддерживаемые компилятором, напримерв C++:

struct Fail {};

try {
  if (something())
    throw Fail();
    throw Fail(); // copypasta bug
  printf("Never reached\n");
}
catch (Fail&) {
  // control jumps here
}

Но обоих вариантов ошибки можно избежать, если компилятор проанализирует и предупредит вас о недоступности кода.Например, при компиляции с Visual C ++ на уровне предупреждения /W4 ошибка обнаруживается в обоих случаях.Java, например, запрещает недоступный код (там, где он может его найти!) по довольно веской причине:скорее всего, это ошибка в коде среднестатистического Джо.Пока конструкция goto не допускает целевых объектов, которые компилятор не может легко вычислить, таких как goto для вычисляемых адресов (**), компилятору не сложнее найти недоступный код внутри функции с goto, чем с использованием кода, одобренного Дейкстрой.

(**) Сноска:Переходы к вычисляемым номерам строк возможны в некоторых версиях Basic, напримерGOTO 10*x, где x - переменная.Довольно запутанно, что в Fortran "вычисляемый goto" относится к конструкции, которая эквивалентна оператору switch в C.Стандартный C не допускает вычисляемых goto в языке, а только goto для статически / синтаксически объявленных меток.Однако GNU C имеет расширение для получения адреса метки (унарный оператор с префиксом &&), а также допускает переход к переменной типа void*.Видишь https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html подробнее об этой малоизвестной подтеме.Остальная часть этого поста не касается этой малопонятной функции GNU C.

Стандарт C (т.е.не вычисляется) goto обычно не являются причиной, по которой недоступный код не может быть найден во время компиляции.Обычной причиной является логический код, подобный следующему.Данный

int computation1() {
  return 1;
}

int computation2() {
  return computation1();
}

Компилятору так же сложно найти недоступный код в любой из следующих 3 конструкций:

void tough1() {
  if (computation1() != computation2())
    printf("Unreachable\n");
}

void tough2() {
  if (computation1() == computation2())
    goto out;
  printf("Unreachable\n");
out:;
}

struct Out{};

void tough3() {
  try {
    if (computation1() == computation2())
      throw Out();
    printf("Unreachable\n");
  }
  catch (Out&) {
  }
}

(Извините за мой стиль кодирования, связанный со скобками, но я постарался сделать примеры как можно более компактными.)

Visual C ++ / W4 (даже с /Ox) не может найти недоступный код ни в одном из них, и, как вы, вероятно, знаете, проблема поиска недоступного кода вообще неразрешима.(Если вы мне в этом не верите: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)

Как связанная с этим проблема, C goto может использоваться для эмуляции исключений только внутри тела функции.Стандартная библиотека C предлагает пару функций setjmp() и longjmp() для эмуляции нелокальных выходов / исключений, но у них есть некоторые серьезные недостатки по сравнению с тем, что предлагают другие языки.Статья в Википедии http://en.wikipedia.org/wiki/Setjmp.h довольно хорошо объясняет этот последний вопрос.Эта пара функций также работает в Windows (http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx), но вряд ли кто-то использует их там, потому что SEH / VEH превосходят их.Даже в Unix, я думаю, setjmp и longjmp используются очень редко.

2) Я думаю, что вторым по распространенности использованием goto в C является реализация многоуровневого разрыва или многоуровневого продолжения, что также является довольно бесспорным вариантом использования.Напомним, что Java не допускает метку goto, но допускает метку break или continue.Согласно http://www.oracle.com/technetwork/java/simple-142616.html, на самом деле это наиболее распространенный вариант использования gotos в C (говорят, в 90% случаев), но, по моему субъективному опыту, системный код, как правило, чаще использует gotos для обработки ошибок.Возможно, в научном коде или там, где ОС предлагает обработку исключений (Windows), тогда многоуровневые выходы являются доминирующим вариантом использования.На самом деле они не сообщают никаких подробностей относительно контекста своего опроса.

Отредактировано для добавления:оказывается, эти два шаблона использования можно найти в книге C Кернигана и Ричи, примерно на странице 60 (в зависимости от издания).Еще одна вещь, которую следует отметить, - это то, что оба варианта использования включают только прямые goto.И оказывается, что версия MISRA C 2012 (в отличие от версии 2004) теперь разрешает goto, при условии, что они являются только передовыми.

В Perl используется метка для "goto" из цикла - с использованием оператора "last", который аналогичен break .

Это позволяет лучше контролировать вложенные циклы.

Традиционный переход этикетка поддерживается тоже, но я не уверен, что есть слишком много случаев, когда это единственный способ достичь того, чего вы хотите - подпрограмм и циклов должно быть достаточно для большинства случаев.

Проблема с "goto" и наиболее важным аргументом движения за "программирование без goto" заключается в том, что если вы используете его слишком часто, ваш код, хотя он может вести себя корректно, становится нечитаемым, не поддерживаемым, недоступным для просмотра и т.д.В 99,99% случаев "goto" приводит к спагетти-коду.Лично я не могу придумать ни одной веской причины, по которой я бы использовал "goto".

Эдсгер Дейкстра, специалист по информатике, внесший большой вклад в эту область, также был известен критикой использования GoTo.Там есть короткая статья о его аргументации по Википедия.

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

версия, отличная от goto:

int doSomething (struct my_complicated_stuff *ctx)    
{
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
        db_disconnect(conn);
        return -1;      
        }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        free(temp_data);    
        db_disconnect(conn);    
        return -2;
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        pthread_mutex_unlock(ctx->mutex);
        free(temp_data);
        db_disconnect(conn);        
        return -3;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -4;  
    }

    if (ctx->something_else->additional_check) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -5;  
    }


    pthread_mutex_unlock(ctx->mutex);
    free(temp_data);    
    db_disconnect(conn);    
    return 0;     
}

версия goto:

int doSomething_goto (struct my_complicated_stuff *ctx)
{
    int ret=0;
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
            ret=-1;
           goto exit_db;   
          }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        ret=-2;
        goto exit_freetmp;      
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        ret=-3;
        goto exit;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
        ret=-4;
        goto exit_freekey; 
    }

    if (ctx->something_else->additional_check) {
        ret=-5;
        goto exit_freekey;  
    }

exit_freekey:
    rsa_free(key);
exit:    
    pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
    free(temp_data);        
exit_db:
    db_disconnect(conn);    
    return ret;     
}

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

Некоторые говорят, что в C ++ нет причин для goto.Некоторые говорят, что в 99% случаев есть лучшие альтернативы. Это не рассуждения, а просто иррациональные впечатления. Вот наглядный пример, где goto приводит к хорошему коду, что-то вроде расширенного цикла do-while:

int i;

PROMPT_INSERT_NUMBER:
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    goto PROMPT_INSERT_NUMBER;          
  }

std::cout << "your number is " << i;

Сравните это с кодом без перехода:

int i;

bool loop;
do {
  loop = false;
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    loop = true;          
  }
} while(loop);

std::cout << "your number is " << i;

Я вижу эти различия:

  • вложенный {} блок необходим (хотя do {...} while выглядит более знакомо)
  • дополнительный loop необходима переменная, используемая в четырех местах
  • требуется больше времени, чтобы прочитать и понять работу с loop
  • тот самый loop не содержит никаких данных, он просто управляет потоком выполнения, что менее понятно, чем простая метка

Есть еще один пример

void sort(int* array, int length) {
SORT:
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    goto SORT; // it is very easy to understand this code, right?
  }
}

Теперь давайте избавимся от "злого" goto:

void sort(int* array, int length) {
  bool seemslegit;
  do {
    seemslegit = true;
    for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
      swap(data[i], data[i+1]);
      seemslegit = false;
    }
  } while(!seemslegit);
}

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

void sort(int* array, int length) {
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    i = -1; // it works, but WTF on the first glance
  }
}

Дело в том, что goto можно легко использовать не по назначению, но сам goto ни в чем не виноват.Обратите внимание, что label имеет область действия функции в C ++, поэтому она не загрязняет глобальную область видимости, как в чистой сборке, в которой перекрывающиеся петли имеют свое место и являются очень распространенными - как в следующем коде для 8051, где 7-сегментный дисплей подключен к P1.Программа зацикливает сегмент lightning по кругу:

; P1 states loops
; 11111110 <-
; 11111101  |
; 11111011  |
; 11110111  |
; 11101111  |
; 11011111  |
; |_________|

init_roll_state:
    MOV P1,#11111110b
    ACALL delay
next_roll_state:
    MOV A,P1
    RL A
    MOV P1,A
    ACALL delay
    JNB P1.5, init_roll_state
    SJMP next_roll_state

Есть еще одно преимущество:goto может служить в качестве именованных циклов, условий и других потоков:

if(valid) {
  do { // while(loop)

// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket

  } while(loop);
} // if(valid)

Или вы можете использовать эквивалентный goto с отступом, так что вам не нужны комментарии, если вы правильно выберете название метки:

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top