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

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

27 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Примеры:

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

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

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

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

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

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


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

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

В операторе C# switch не допускается провал. Таким образом, goto используется для передачи управления определенной метке переключения или метке по умолчанию.

Например:

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

Изменить: есть одно исключение из правила "без провала". Прорыв разрешен, если в регистре нет кода.

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

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

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

#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 и тому подобное --- они имеют законное использование, но могут легко злоупотреблять. Например, выбрасывание исключения исключительно для того, чтобы избежать глубоко вложенной управляющей структуры при совершенно неисключительных обстоятельствах.)

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

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

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

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

Некоторые говорят, что нет причин для перехода в C++. Некоторые говорят, что в 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?
  }
}

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

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 не виновато. Обратите внимание, что метка имеет область действия функции в C++, поэтому она не загрязняет глобальную область видимости, как в чистой сборке, в которой перекрывающиеся циклы имеют свое место и очень распространены - как в следующем коде для 8051, где 7-сегментный дисплей подключен к P1. Программа зацикливает молниеносный сегмент вокруг:

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

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

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;

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

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было хорошим решением. И я не видел этого примера ни здесь, ни где-либо еще.

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

Это выглядело примерно так:

switch( x ) {
    
    case 1: case1() ; doStuffFor123() ; break ;
    case 2: case2() ; doStuffFor123() ; break ;
    case 3: case3() ; doStuffFor123() ; break ;
    
    case 4: case4() ; doStuffFor456() ; break ;
    case 5: case5() ; doStuffFor456() ; break ;
    case 6: case6() ; doStuffFor456() ; break ;
    
    case 7: case7() ; doStuffFor789() ; break ;
    case 8: case8() ; doStuffFor789() ; break ;
    case 9: case9() ; doStuffFor789() ; break ;
}

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

switch( x ) {
    
    case 1: case1() ; goto stuff123 ;
    case 2: case2() ; goto stuff123 ;
    case 3: case3() ; goto stuff123 ;
    
    case 4: case4() ; goto stuff456 ;
    case 5: case5() ; goto stuff456 ;
    case 6: case6() ; goto stuff456 ;
    
    case 7: case7() ; goto stuff789 ;
    case 8: case8() ; goto stuff789 ;
    case 9: case9() ; goto stuff789 ;
    
    stuff123: doStuffFor123() ; break ;
    stuff456: doStuffFor456() ; break ;
    stuff789: doStuffFor789() ; break ;
}

случаи с 1 по 3 все должны звонить doStuffFor123() и аналогично случаи с 4 по 6 должны были вызвать doStuffFor456() и т.п.

IMO gotos отлично подходят, если вы их правильно используете. В конце концов, любой код настолько понятен, насколько его пишут люди. С помощью gotos можно создавать спагетти-код, но это не означает, что gotos являются причиной спагетти-кода. Это мы, программисты. Я также могу создавать спагетти-код с функциями, если хочу. То же самое и с макросами

В модуле 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
  ...
}

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

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

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, но позволяет разорвать метку или продолжить метку. Согласно http://www.oracle.com/technetwork/java/simple-142616.html, это на самом деле наиболее распространенный вариант использования gotos в C (говорят, что 90%), но, по моему субъективному опыту, системный код имеет тенденцию чаще использовать gotos для обработки ошибок. Возможно, в научном коде или там, где ОС предлагает обработку исключений (Windows), многоуровневые выходы являются доминирующим вариантом использования. Они не дают никаких подробностей относительно контекста своего опроса.

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

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

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

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

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

Время от времени он пригодится для посимвольной обработки строк.

Представьте себе что-то вроде этого примера printf-esque:

for cur_char, next_char in sliding_window(input_string) {
    if cur_char == '%' {
        if next_char == '%' {
            cur_char_index += 1
            goto handle_literal
        }
        # Some additional logic
        if chars_should_be_handled_literally() {
            goto handle_literal
        }
        # Handle the format
    }
    # some other control characters
    else {
      handle_literal:
        # Complicated logic here
        # Maybe it's writing to an array for some OpenGL calls later or something,
        # all while modifying a bunch of local variables declared outside the loop
    }
}

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

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

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

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

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

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 без контроля значения А.

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

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

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

Эдсгер Дейкстра (Edsger Dijkstra), ученый-компьютерщик, внесший значительный вклад в этой области, также был известен своей критикой использования 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;     
}

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

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

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

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

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

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

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

Другие вопросы по тегам