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

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

Ответы [ 24 ]

7 голосов
/ 23 августа 2008

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

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 перед тем, как взламывать ассемблер.

6 голосов
/ 23 августа 2008

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

4 голосов
/ 02 января 2014

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

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

3 голосов
/ 16 сентября 2008

В модуле 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
  ...
}
2 голосов
/ 16 сентября 2008

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

2 голосов
/ 04 января 2012

Конечно, 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.

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

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

2 голосов
/ 11 мая 2010

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

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

for (stepfailed=0 ; ! stepfailed ; /*empty*/)
2 голосов
/ 23 августа 2008

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

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

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

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

2 голосов
/ 14 июля 2014

1) Наиболее распространенное использование goto, о котором я знаю, это эмуляция обработки исключений в языках, которые этого не предлагают, а именно в C. (Код, приведенный выше в Nuclear, является именно этим.) Посмотрите на исходный код Linux и вы увидите, что таким образом использовался базилик гото; согласно короткому опросу, проведенному в 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 не позволяет цели, которые компилятор не может легко определить, например, gotos по вычисляемым адресам (**), компилятору не сложнее найти недоступный код внутри функции с gotos, чем использовать Dijkstra одобренный код.

(**) Сноска. Переход к вычисленным номерам строк возможен в некоторых версиях Basic, например, GOTO 10 * x, где x - переменная. Весьма странно, что в Фортране «computed goto» относится к конструкции, которая эквивалентна выражению switch в C. Стандартный C не допускает вычисляемые goto в языке, а только goto для статически / синтаксически объявленных меток. GNU C, однако, имеет расширение для получения адреса метки (унарный, префикс && оператор), а также позволяет перейти к переменной типа 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 C ++ / 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, но позволяет разорвать метку или продолжить метку. Согласно http://www.oracle.com/technetwork/java/simple-142616.html, это на самом деле самый распространенный вариант использования gotos в C (90% говорят), но в моем субъективном опыте системный код имеет тенденцию чаще использовать gotos для обработки ошибок. Возможно, в научном коде или там, где ОС предлагает обработку исключений (Windows), многоуровневые выходы являются доминирующим вариантом использования. Они не дают никаких подробностей относительно контекста своего опроса.

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

1 голос
/ 20 ноября 2013

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

Перейти к версии:

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

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

...