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

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

Ответы

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

Все, кто является анти- goto, прямо или косвенно ссылаются на статью Edsger Dijkstra GoTo, которая считается вредной для обоснования их позиции. Слишком плохо, что статья Дейкстры практически не имеет никакого отношения к тому, как в наши дни используются выражения goto, и, следовательно, то, что говорится в статье, практически не применимо к современной сцене программирования. goto -less mem теперь граничит с религией, вплоть до своих писаний, продиктованных высоко, его первосвященниками и избегающими (или худшими) воспринимаемыми еретиками.

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

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

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

Я уже слышу поднятые песнопения культистов, когда они сталкиваются с еретиком. "Но, - повторяют они, - вы можете сделать код очень трудным для чтения с помощью goto в C.". О, да? Вы можете сделать код очень трудным для чтения без 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!

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

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

Имейте в виду, что вы проголосовали за меня с помощью одного -1 за другим, что я использовал goto в своем (неассемблерном) кодеке ровно 3 раза за последние 15-20 лет.

Я ожидаю поток возмущенных криков и -1 голосов затаив дыхание.

Ответ 3

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

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

Ответ 4

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

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

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

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

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


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

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

Ответ 5

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

<сильные > Примеры:

    // 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

Ответ 6

В С# switch statement doest not разрешить прохождение. Поэтому goto используется для переноса управления на конкретную метку case-переключателя или по умолчанию.

Например:

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-заявлении нет кода.

Ответ 7

#ifdef TONGUE_IN_CHEEK

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

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

#endif

Хорошо, так что это не имеет ничего общего с C goto. Более серьезно, я согласен с другими комментариями об использовании goto для очистки или для реализации устройства Duff или тому подобного. Это все об использовании, а не оскорблении.

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

Ответ 8

На протяжении многих лет я написал несколько строк ассемблера. В конечном счете, каждый язык высокого уровня компилируется до gotos. Хорошо, назовите их "ветками" или "прыжками" или что-то еще, но они есть. Может ли кто-нибудь писать goto-less ассемблер?

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

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

Ответ 9

Мне смешно, что некоторые люди пойдут так далеко, чтобы дать список случаев, когда 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)...
}

//элемент записывается в хеш-таблицу здесь

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

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

Ответ 10

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

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

Большая часть того, что я хочу сказать о goto, применима только к 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 изменения. Теперь вы можете решить, что вы можете просто инкапсулировать всю очистку в одну функцию; это не плохая идея. Но это означает, что вам нужно быть осторожным с указателями - если вы планируете освободить указатель в своей функции очистки, тогда нет способа установить его, а затем указать 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;
}

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

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


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

Ответ 11

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

Ответ 12

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

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

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

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

Ответ 13

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

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

Ответ 14

В модуле 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 и тот, который будет import 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
  ...
}

Ответ 15

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

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

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

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

Ответ 16

Точно так же никто никогда не реализовал инструкцию "COME FROM"....

Ответ 17

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

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

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

Ответ 18

Разумеется, 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" (мы предполагаем выполнение без parallelism, конечно)

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

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

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

Связанный материал можно найти в знаменитой статье от г-на Дейкстры "Случай с выражением GO TO"

Ответ 19

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

Однако существует обычная возможность получения поддержки на уровне компилятора и проверки общих конструкций/шаблонов: проще использовать их неправильно и вводить ошибки без проверки времени компиляции. Windows и Visual С++, но в режиме C предлагают обработку исключений через SEH/VEH именно по этой причине: исключения полезны даже вне языков ООП, то есть на языке процедур. Но компилятор не всегда может сохранить ваш бекон, даже если он предлагает синтаксическую поддержку исключений на языке. Рассмотрим в качестве примера последний случай знаменитую ошибку Apple SSL "goto fail", которая просто дублирует один переход с катастрофическими последствиями (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

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

struct Fail {};

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

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

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

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

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 С++/W4 (даже с /Ox ) не может найти недостижимый код в любом из них, и, как вы, вероятно, знаете, проблема поиска недостижимого кода в общем случае неразрешима. (Если вы мне не верите: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)

Как связанная проблема, C goto может использоваться для эмуляции исключений только внутри тела функции. Стандартная библиотека C предлагает функцию setjmp() и longjmp() для эмуляции нелокальных выходов/исключений, но они имеют некоторые серьезные недостатки по сравнению с другими языками. Статья в Wikipedia 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 (в зависимости от издания). Другое замечание состоит в том, что оба варианта использования включают только переходы. И получается, что издание MISRA C 2012 (в отличие от издания 2004 года) теперь разрешает переходы, если они только передовые.

Ответ 20

В Perl использование метки для "goto" из цикла - использование "последнего" оператора, похожего на break.

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

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

Ответ 21

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

Ответ 22

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

Ответ 23

Я использую 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;     
}

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

Ответ 24

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

int i;

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

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 не содержит никаких данных, он просто управляет потоком выполнения, который менее понятен, чем простая метка

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

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

again:
MOV P1,#11111110b
ACALL delay
loop:
  MOV A,P1
  RL A
  MOV P1,A
  ACALL delay
  JNB P1.5, again
SJMP loop