Приклади хороших гото в C або C ++ [закрито]


79

У цій темі ми розглянемо приклади ефективного використання gotoC або C ++. Це надихається відповіддю, яку люди проголосували, бо думали, що я жартую.

Короткий зміст (мітка змінена з оригінальної, щоб зробити намір ще чіткішим):

infinite_loop:

    // code goes here

goto infinite_loop;

Чому це краще, ніж альтернативи:

  • Це конкретно. goto- це мовна конструкція, яка викликає безумовну гілку. Альтернативи залежать від використання конструкцій, що підтримують умовні гілки, з виродженим завжди істинним станом.
  • Етикетка документує намір без зайвих коментарів.
  • Читачеві не потрібно сканувати проміжний код на ранні breaks (хоча все ще можливо для безпринципного хакера симулювати continueз ранньою goto).

Правила:

  • Робіть вигляд, що готофоби не перемогли. Зрозуміло, що вищезазначене не можна використовувати в реальному коді, оскільки це суперечить усталеній ідіомі.
  • Припустимо, що всі ми чули про "Goto вважається шкідливим" і знаємо, що goto можна використовувати для написання коду спагетті.
  • Якщо ви не згодні з прикладом, критикуйте його лише з технічної точки зору («Тому що люди не люблять goto» - це не технічна причина).

Давайте подивимось, чи можна говорити про це як про дорослих.

Редагувати

Здається, це питання зараз закінчене. Це дало кілька високоякісних відповідей. Дякую всім, особливо тим, хто серйозно сприйняв мій маленький приклад. Більшість скептиків були стурбовані відсутністю блоку блоків. Як зазначив @quinmars у коментарі, ви завжди можете розмістити фігурні дужки навколо тіла циклу. Зауважу принагідно , що for(;;)й while(true)не дають вам брекети безкоштовно або для (і опускаючи їх може викликати прикрі помилки). У будь-якому разі, я більше не буду витрачати на це дрібницю вашу силу мозку - я можу жити з нешкідливим і ідіоматичним for(;;)і while(true)(так само добре, якщо хочу зберегти свою роботу).

Беручи до уваги інші відповіді, я бачу, що багато людей розглядають gotoяк те, що ти завжди повинен переписати по-іншому. Звичайно, ви можете уникнути a goto, ввівши цикл, додатковий прапор, стек вкладених ifs або що завгодно, але чому б не подумати, чи gotoце, мабуть, найкращий інструмент для роботи? Іншими словами, скільки потворності готові витерпіти люди, щоб уникнути використання вбудованої мовної функції за призначенням? Я вважаю, що навіть додавання прапора є занадто високою ціною, яку потрібно заплатити. Мені подобається, щоб мої змінні представляли речі в доменах проблеми чи рішення. "Тільки щоб уникнути goto" не вирізає.

Я прийму першу відповідь, яка дала шаблон C для розгалуження до блоку очищення. ІМО, це робить найбільш вагомий випадок gotoз усіх розміщених відповідей, звичайно, якщо ви вимірюєте це за викривленнями, які ненависник повинен пройти, щоб уникнути цього.


11
Я не розумію, чому готофоби не просто вказують "#define goto report_to_your_supervisor_for_re_education_through_labour" у верхній частині файлу включення проекту. Якщо це завжди неправильно, зробіть це неможливим. Інакше іноді це правильно ...
Стів Джессоп,

14
"не може використовуватися в реальному коді"? Я використовую його в "реальному" коді в будь-який час, це найкращий інструмент для роботи. "Сформована ідіома" - приємний евфемізм для "сліпого догматизму".
Dan Molding

7
Я погоджуюся, що "goto" може бути корисним (нижче є чудові приклади), але я не погоджуюся з вашим конкретним прикладом. Рядок із написом "goto infinite_loop" звучить так, як це означає "перейти до тієї частини коду, де ми почнемо цикл назавжди", як у "initialize (); set_things_up (); goto infinite_loop;" коли те, що ви насправді хочете передати, це "розпочати наступну ітерацію циклу, в якому ми вже перебуваємо", що зовсім інше. Якщо ви намагаєтеся виконати цикл, а ваша мова має конструкції, розроблені спеціально для циклу, використовуйте їх для наочності. while (true) {foo ()} є досить однозначним.
Джош,

6
Одним з основного недоліку вашого прикладу є те , що це не ясно , яким обтерла місце в коді може прийняти рішення перейти до цієї мітці. "Нормальний" нескінченний цикл ( for (;;)) не має дивовижних точок входу.
jalf

3
@ Дьєрдь: так, але це не точка входу.
jalf 05.03.11

Відповіді:


87

Ось один трюк, який я чув про те, як люди використовують. Я ніколи не бачив цього в дикій природі. І це стосується лише C, оскільки C ++ має RAII, щоб зробити це більш ідіоматично.

void foo()
{
    if (!doA())
        goto exit;
    if (!doB())
        goto cleanupA;
    if (!doC())
        goto cleanupB;

    /* everything has succeeded */
    return;

cleanupB:
    undoB();
cleanupA:
    undoA();
exit:
    return;
}

2
Що в цьому поганого? (крім того, що коментарі не розуміють нових рядків) void foo () {if (doA ()) {if (doB ()) {if (doC ()) {/ * все вдалося * / return; } undoB (); } undoA (); } повернення; }
Артелій

6
Додаткові блоки викликають непотрібні відступи, які важко читати. Це також не працює, якщо одна з умов знаходиться всередині циклу.
Адам Розенфілд,

26
Багато подібного коду ви можете побачити в більшості речей Unix на низькому рівні (наприклад, в ядрі Linux, наприклад). У C - це найкраща ідіома для відновлення помилок IMHO.
Девід Курно,

8
Як уже згадувалося cournape, то Linux ядро використовує цей стиль весь час .
CesarB

8
Не лише ядро ​​Linux подивіться на зразки драйверів Windows від Microsoft, і ви знайдете такий самий шаблон. Загалом, це C-спосіб обробки винятків і дуже корисний :). Зазвичай я віддаю перевагу лише 1 мітці, а в деяких випадках 2. 3 можна уникнути у 99% випадків.
Ілля

85

Класична потреба в GOTO на C наступна

for ...
  for ...
    if(breakout_condition) 
      goto final;

final:

Немає простого способу вирватися з вкладених петель без goto.


49
Найчастіше, коли виникає ця потреба, я замість цього можу використовувати повернення. Але вам потрібно писати невеликі функції, щоб це вийшло так.
Дарій Бекон,

7
Я безумовно погоджуюсь з Дарієм - переформатуйте його у функцію і натомість поверніть!
metao

9
@metao: Бьярн Струструп не погоджується. У його книзі з мов програмування на C ++ це саме той приклад, як "хороше використання" goto.
paercebal

19
@Адам Розенфілд: Вся проблема з goto, на яку звернув увагу Дейкстра, полягає в тому, що структуроване програмування пропонує більш зрозумілі конструкції. Ускладнення читання коду, щоб уникнути goto, доводить, що автор не зрозумів цього есе ...
Стів Джессоп,

5
@ Стів Джессоп: Не тільки люди неправильно зрозуміли есе, більшість культистів "не йти до" не розуміють історичного контексту, в якому він був написаний.
ТІЛЬКИ МОЙ правильний ДУМКА

32

Ось мій нерозумний приклад (від Стівенса APITUE) для системних дзвінків Unix, які можуть бути перервані сигналом.

restart:
    if (system_call() == -1) {
        if (errno == EINTR) goto restart;

        // handle real errors
    }

Альтернатива - вироджена петля. Ця версія читається як англійська "якщо системний дзвінок був перерваний сигналом, перезапустіть його".


3
цей спосіб використовується, наприклад, у планувальника Linux є таке goto, але я б сказав, що дуже мало випадків, коли зворотні goto є прийнятними, і взагалі їх слід уникати.
Ілля

8
@Amarghosh, continueце просто gotoмаска.
jball

2
@jball ... хмм .. заради аргументу, так; ви можете зробити добре читабельний код за допомогою goto та коду спагетті з continue .. Зрештою, це залежить від людини, яка пише код. Справа в тому, що легко загубитися з goto, ніж з continue. І новачки зазвичай використовують перший молоток, який отримують, для будь-яких проблем.
Amarghosh

2
@Amarghosh. Ваш "вдосконалений" код навіть не еквівалентний оригіналу - він завжди цикується у випадку успіху.
fizzer

3
@fizzer oopsie .. йому знадобляться два else breaks (по одному на кожен ifs), щоб зробити його еквівалентним .. що я можу сказати ... gotoчи ні, ваш код такий же хороший, як і ви :(
Amarghosh

14

Якщо пристрій Даффа не потребує goto, то вам не потрібно! ;)

void dsend(int count) {
    int n;
    if (!count) return;
    n = (count + 7) / 8;
    switch (count % 8) {
      case 0: do { puts("case 0");
      case 7:      puts("case 7");
      case 6:      puts("case 6");
      case 5:      puts("case 5");
      case 4:      puts("case 4");
      case 3:      puts("case 3");
      case 2:      puts("case 2");
      case 1:      puts("case 1");
                 } while (--n > 0);
    }
}

Код вище з Вікіпедії записи .


2
Хлопці, якщо ви збираєтеся проти, короткий коментар, чому це було б чудово. :)
Mitch Wheat

10
Це приклад логічної помилки, також відомої як "звернення до влади". Перевірте це у Вікіпедії.
Маркус Гріп,

11
гумор тут часто не цінується ...
Стівен А. Лоу,

8
чи робить пристрій Дафа пиво?
Steven A. Lowe

6
це може зробити 8 за раз ;-)
Джаспер Беккерс

14

Кнут написав статтю "Структуроване програмування з операторами GOTO", її можна отримати, наприклад, звідси . Там ви знайдете багато прикладів.


Цей папір виглядає глибоко. Я все ж прочитаю. Дякую
fizzer

2
Цей папір настільки застарілий, що навіть не смішно. Припущення Кнута там просто більше не тримаються.
Конрад Рудольф

3
Які припущення? Його приклади сьогодні такі ж реальні для процесуальної мови, як C (він подає їх у якомусь псевдокоді), як вони були на той час.
zvrba

8
я розчарований, що ти не писав :: Ви можете ПЕРЕЙТИ "сюди", щоб отримати його, каламбур для мене був би нестерпним, щоб не зробити!
RhysW

12

Дуже часто.

do_stuff(thingy) {
    lock(thingy);

    foo;
    if (foo failed) {
        status = -EFOO;
        goto OUT;
    }

    bar;
    if (bar failed) {
        status = -EBAR;
        goto OUT;
    }

    do_stuff_to(thingy);

OUT:
    unlock(thingy);
    return status;
}

Єдиний випадок, який я коли-небудь використовую, goto- це стрибки вперед, як правило, з блоків і ніколи в блоки. Це дозволяє уникнути зловживань do{}while(0)та інших конструкцій, які збільшують вкладеність, зберігаючи при цьому читабельний структурований код.


5
Я думаю, що це типовий спосіб обробки помилок. Не бачу, щоб його замінили красиво, краще читали будь-яким іншим чином З повагою
Фрідріх

Чому б і ні ... do_stuff(thingy) { RAIILockerObject(thingy); ..... /* -->*/ } /* <---*/ :)
Петър Петров

1
@ ПетърПетров Це шаблон у C ++, який не має аналога в C.
ефемієнт

Або навіть більш структурованою мовою, ніж C ++, try { lock();..code.. } finally { unlock(); }але так, C не має еквівалента.
Сліпий

12

Я взагалі нічого не маю проти gotos, але я можу придумати кілька причин, чому ви не хотіли б використовувати їх для циклу, як ви вже згадували:

  • Це не обмежує сферу дії, отже, будь-які тимчасові змінні, які ви використовуєте всередині, не будуть звільнені до пізніше.
  • Це не обмежує сферу застосування, отже, це може призвести до помилок.
  • Це не обмежує область, отже, ви не можете повторно використовувати ті самі імена змінних пізніше в майбутньому коді в тій самій області.
  • Це не обмежує область дії, отже у вас є шанс пропустити оголошення змінної.
  • Люди до цього не звикли, і це ускладнить читання вашого коду.
  • Вкладені цикли цього типу можуть вести до коду спагетті, звичайні цикли не ведуть до коду спагетті.

1
є inf_loop: {/ * body loop * /} goto inf_loop; краще? :)
quinmars

2
Пункти 1-4 сумнівні для цього прикладу навіть без фігурних дужок. 1 Це нескінченна петля. 2 Занадто розпливчатий для звернення. 3 Спроба приховати однойменну змінну в області, що охоплює, є поганою практикою. 4. Зворотне перехід не може пропустити декларацію.
fizzer

1-4, як і у всіх нескінченних петель, у вас зазвичай є якийсь стан обриву посередині. Тож вони застосовні. Re bakward goto не може пропустити декларацію ... не всі gotos є назад ...
Брайан Р. Бонді

7

Одне хороше місце для використання goto - це процедура, яка може перерватись у декількох точках, кожна з яких вимагає різного рівня очищення. Готофобі завжди можуть замінити гото структурованим кодом та низкою тестів, але я думаю, що це простіше, оскільки це виключає надмірне відступ:

if (! openDataFile ())
  перейти;

if (! getDataFromFile ())
  перейти до closeFileAndQuit;

if (! allocateSomeResources)
  перейти до freeResourcesAndQuit;

// Виконайте тут більше роботи ....

freeResourcesAndQuit:
   // вільні ресурси
closeFileAndQuit:
   // закрити файл
кинути:
   // кинути!

Я хотів би замінити це з набором функцій: freeResourcesCloseFileQuit, closeFileQuitі quit.
RCIX

4
Якщо ви створили ці 3 додаткові функції, вам потрібно буде передати вказівники / дескриптори на ресурси та файли, які потрібно звільнити / закрити. Ви, напевно, зробили б виклик freeResourcesCloseFileQuit () closeFileQuit (), який, у свою чергу, викликав би quit (). Тепер у вас є 4 тісно пов’язані функції для обслуговування, і 3 з них, ймовірно, будуть викликані не більше одного разу: з однієї функції вище. Якщо ви наполягаєте на униканні goto, IMO, вкладені if () блоки мають менше накладних витрат і їх легше читати та підтримувати. Що ви отримуєте завдяки 3 додатковим функціям?
Адам Лісс

7

@ fizzer.myopenid.com: ваш розміщений фрагмент коду еквівалентний наступному:

    while (system_call() == -1)
    {
        if (errno != EINTR)
        {
            // handle real errors

            break;
        }
    }

Я точно віддаю перевагу цій формі.


1
Добре, я знаю, що заява про перерву - це просто більш прийнятна форма goto ..
Mitch Wheat

10
На мої очі, це бентежить. Це цикл, який ви не очікуєте ввести у звичайний шлях виконання, тому логіка здається зворотною. Однак питання думки.
fizzer

1
Назад? Починається, продовжується, провалюється. Я думаю, що ця форма більш очевидно говорить про свої наміри.
Mitch Wheat

8
Тут я б погодився з fizzer, що goto дає чіткіші очікування щодо значення умови.
Маркус Гріп,

4
Яким чином цей фрагмент читати легше, ніж фіззер.
erikkallen

6

Незважаючи на те, що я з часом зненавидів цей шаблон, він вбудований у програмування COM.

#define IfFailGo(x) {hr = (x); if (FAILED(hr)) goto Error}
...
HRESULT SomeMethod(IFoo* pFoo) {
  HRESULT hr = S_OK;
  IfFailGo( pFoo->PerformAction() );
  IfFailGo( pFoo->SomeOtherAction() );
Error:
  return hr;
}


1

Я бачив, як правильно вживали goto, але ситуації звичайно потворні. Це лише тоді, коли використання самого gotoсебе набагато гірше, ніж оригіналу. @Johnathon Holland, проблема в тому, що ти версія, менш зрозуміла. здається, люди бояться локальних змінних:

void foo()
{
    bool doAsuccess = doA();
    bool doBsuccess = doAsuccess && doB();
    bool doCsuccess = doBsuccess && doC();

    if (!doCsuccess)
    {
        if (doBsuccess)
            undoB();
        if (doAsuccess)
            undoA();
    }
}

І я віддаю перевагу таким циклам, але деякі воліють while(true).

for (;;)
{
    //code goes here
}

2
Ніколи, ніколи, ніколи не порівнюйте булеве значення з істинним чи хибним. Це вже логічне значення; просто використовуйте "doBsuccess = doAsuccess && doB ();" і "if (! doCsuccess) {}" замість цього. Це простіше і захищає вас від розумних програмістів, які пишуть такі речі, як "#define true 0", бо, ну, бо вони можуть. Це також захищає вас від програмістів, які змішують int та bool, а потім припускають, що будь-яке ненульове значення є істинним.
Адам Лісс,

Дякую і очко == trueзнято. Це все одно ближче до мого справжнього стилю. :)
Чарльз Бітті

0

Моя думка з цього приводу полягає в тому, що ви втрачаєте масштаб блоків; будь-які локальні змінні, оголошені між гото, залишаються в силі, якщо цикл коли-небудь перервано. (Можливо, ви припускаєте, що цикл працює назавжди; я не думаю, що це те, що задавав оригінальний автор запитань.)

Проблема масштабування є більшою проблемою з C ++, оскільки деякі об'єкти можуть залежати від виклику їх dtor у відповідний час.

Для мене найкраща причина використовувати goto - це під час багатоетапного процесу ініціалізації, де життєво важливо, щоб усі ініціали були захищені, якщо хтось не вдається, a la:

if(!foo_init())
  goto bye;

if(!bar_init())
  goto foo_bye;

if(!xyzzy_init())
  goto bar_bye;

return TRUE;

bar_bye:
 bar_terminate();

foo_bye:
  foo_terminate();

bye:
  return FALSE;

0

Я сам не використовую goto, однак колись працював із людиною, яка б використовувала їх у конкретних випадках. Якщо я добре пам’ятаю, його обгрунтування було навколо питань продуктивності - він також мав конкретні правила щодо того, як . Завжди в одній функції, і мітка завжди була ПІД заявою goto.


4
Додаткові дані: зауважте, що ви не можете використовувати goto для виходу за межі функції, і що в C ++ заборонено обходити конструкцію об’єкта через goto
paercebal

0
#include <stdio.h>
#include <string.h>

int main()
{
    char name[64];
    char url[80]; /*The final url name with http://www..com*/
    char *pName;
    int x;

    pName = name;

    INPUT:
    printf("\nWrite the name of a web page (Without www, http, .com) ");
    gets(name);

    for(x=0;x<=(strlen(name));x++)
        if(*(pName+0) == '\0' || *(pName+x) == ' ')
        {
            printf("Name blank or with spaces!");
            getch();
            system("cls");
            goto INPUT;
        }

    strcpy(url,"http://www.");
    strcat(url,name);
    strcat(url,".com");

    printf("%s",url);
    return(0);
}

-1

@Greg:

Чому б не зробити ваш приклад таким:

void foo()
{
    if (doA())
    {    
        if (doB())
        {
          if (!doC())
          {
             UndoA();
             UndoB();
          }
        }
        else
        {
          UndoA();
        }
    }
    return;
}

13
Це додає непотрібні рівні відступу, які можуть стати дуже волохатими, якщо у вас є велика кількість можливих режимів несправності.
Адам Розенфілд,

6
Я б взяв відступи над будь-яким днем.
FlySwat

13
Крім того, це набагато більше рядків коду, має надлишковий виклик функції в одному з випадків і набагато складніше правильно додати doD.
Патрік,

6
Я погоджуюсь з Патріком - я бачив цю ідіому, використовувану з кількома умовами, які вклалися на глибину приблизно 8 рівнів. Дуже противний. І дуже схильні до помилок, особливо при спробі додати щось нове до суміші.
Майкл Бурр,

11
Цей код просто кричить: "У мене буде помилка через кілька наступних тижнів", хтось забуде щось скасувати. Вам потрібно все скасувати в одному місці. Це як "нарешті" у рідних мовах OO.
Ілля
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.