Дійсне використання goto для управління помилками в C?


92

Це питання насправді є результатом цікавої дискусії на programs.reddit.com деякий час тому. В основному це зводиться до наступного коду:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Використання gotoтут, здається, є найкращим шляхом, що дає найчистіший та найефективніший код з усіх можливостей, або принаймні мені здається. Цитування Стіва Макконнелла в Code Complete :

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

Ще однією підтримкою цього підходу є книга з драйверів пристроїв Linux у цьому розділі .

Що ти думаєш? Чи є цей випадок допустимим для використання gotoв C? Чи хотіли б ви віддавати перевагу іншим методам, які створюють більш заплутаний та / або менш ефективний код, але уникайте goto?



@Eli: Чому б вам не видалити теги і не помістити функцію (cleanup_3 ();) в дужки if?

@Akito: що ти маєш на увазі? Не могли б ви опублікувати свою пропозицію як відповідь із повним зразком коду?
Елі Бендерський,

@EliBendersky: Будь ласка, перегляньте мою відповідь.

Однією з речей, яку я найбільше ненавидів від Visual Basic (включаючи VBS та VB6), була on error goto errorсистема обробки помилок :)
Manu343726

Відповіді:


61

FWIF, я вважаю ідіому обробки помилок, яку ви вказали у прикладі запитання, більш читабельною та легшою для розуміння, ніж будь-яка з альтернатив, наведених у відповідях на даний момент. Хоча gotoзагалом це погана ідея, вона може бути корисною для обробки помилок, коли це робиться просто і однаково. У цій ситуації, хоча це і є goto, воно використовується чітко визначеним і більш-менш структурованим способом.


1
Чи не може він просто видалити ці теги і вкласти функції безпосередньо в if? Чи не буде це більш читабельним?

8
@StartupCrazy, я знаю, що це років тому, але задля достовірності публікацій на цьому сайті я зазначу, що ні він не може. Якщо він отримує помилку в goto error3 у своєму коді, він запускає очищення 1 2 і 3, у вашому переліченому рішенні він запускає лише очистку 3. Він може вкладати речі, але це буде просто стрілка-шаблон, чого програмісти повинні уникати. .
gbtimmon

17

Як правило, уникнення goto - це гарна ідея, але зловживання, які були поширеними, коли Дейкстра вперше написав "GOTO вважається шкідливим", навіть не приходять у голову більшості людей сьогодні як варіант.

Те, що ви окреслили, - це узагальнене рішення проблеми з обробкою помилок - зі мною це добре, якщо воно ретельно використовується.

Ваш конкретний приклад можна спростити наступним чином (крок 1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Продовження процесу:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

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


23
Так, вкладене рішення є однією з життєздатних альтернатив. На жаль, він стає менш життєздатним, оскільки рівень гніздування поглиблюється.
Елі Бендерський,

4
@eliben: Погоджено - але більш глибоке вкладання може бути (мабуть, є) ознакою того, що вам потрібно ввести більше функцій, або зробити кроки підготовки більше, або іншим чином переробити ваш код. Я міг би стверджувати, що кожна з функцій підготовки повинна виконати свою настройку, зателефонувати наступному в ланцюжку та провести власне очищення. Він локалізує цю роботу - ви навіть можете заощадити на трьох функціях очищення. Це також частково залежить від того, чи використовується якась із функцій налаштування чи очищення (використовується) у будь-якій іншій послідовності викликів.
Джонатан Леффлер,

6
На жаль, це не працює з циклами - якщо помилка виникає всередині циклу, то goto набагато чистіше, ніж альтернатива встановлення та перевірки прапорів та операторів "break" (які просто хитро замасковані gotos).
Адам Розенфілд,

1
@Smith, Більше схоже на їзду без бронежилета.
Strager

6
Я знаю, що я тут некромантую, але вважаю цю пораду досить бідною - слід уникати набору стрілок .
KingRadical

16

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

Основна ідея полягає в тому, що для кожної дії очищення, яку, можливо, потрібно буде виконати, існує змінна, за значенням якої ми можемо визначити, чи потребує очищення чи ні.

gotoСпочатку покажу версію, оскільки вона ближча до коду в оригінальному питанні.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

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

Зараз деякі можуть стверджувати, що цей метод додає багато зайвих змінних - і справді в цьому випадку це правда, - але на практиці часто існуюча змінна вже відстежує або може бути зроблена для відстеження необхідного стану. Наприклад, якщо prepare_stuff()насправді є викликом до malloc()або до open(), тоді може використовуватися змінна, що містить повернутий покажчик або дескриптор файлу - наприклад:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

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

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Знову ж таки, є потенційна критика з цього приводу:

  • Хіба всі ці "якщо" не шкодять продуктивності? Ні - тому що у випадку успіху, ви все одно повинні виконати всі перевірки (інакше ви не перевіряєте всі випадки помилок); а у випадку відмови більшість компіляторів оптимізують послідовність невдалих if (oksofar)перевірок до одного переходу до коду очищення (GCC, безумовно) - і в будь-якому випадку випадок помилки, як правило, менш критичний для продуктивності.
  • Хіба це не додавання ще однієї змінної? У цьому випадку так, але часто return_valueзмінну можна використовувати для відігравання ролі, яка oksofarтут відіграє. Якщо ви структуруєте свої функції, щоб послідовно повертати помилки, ви можете навіть уникнути другої ifв кожному випадку:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }

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

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


2
Схоже, ви запізнилися на вечірку, але мені, звичайно, подобається відповідь!

Лінус ймовірно відхиляти код blogs.oracle.com/oswald/entry/is_goto_the_root_of
Fizz

1
@ user3588161: Якщо б він це зробив, це його прерогатива - але я не впевнений, на основі статті, на яку ви посилаєтесь, що це так: зауважте, що в стилі, який я описую, (1) умовні умови не вкладаються довільно глибоко, і (2) відсутні додаткові твердження "якщо" у порівнянні з тим, що вам все одно потрібно (за умови, що ви збираєтеся перевірити всі коди повернення).
psmears

Стільки цього замість жахливого goto і ще гіршого рішення зі стрілками-антипатернами!
Парамагнітний круасан

8

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

FWIW, якщо ви шукаєте підручники для розробників.apple.com, вони використовують підхід goto до обробки помилок.

Ми не використовуємо гото. Більше значення надається повернутим значенням. Обробка винятків здійснюється через setjmp/longjmp- що мало, що можете.


8
Хоча я, звичайно, використовував setjmp / longjmp у деяких випадках, коли це було закликано, я вважав би це навіть "гіршим", ніж goto (який я також використовую, дещо менш стримано, коли це вимагається). Єдиний раз, коли я використовую setjmp / longjmp, це коли (1) ціль збирається вимкнути все таким чином, що не буде залежати від її поточного стану, або (2) ціль збирається реініціалізувати все, що контролюється в захищений блок setjmp / longjmp способом, який не залежить від його поточного стану.
supercat

4

У вислові goto немає нічого морально неправильного, як і щось (морально) * покажчики.

Вся справа в тому, як ви використовуєте інструмент. У (тривіальному) випадку, який ви представили, виклад справи може досягти тієї самої логіки, хоча і з більшими накладними витратами. Справжнє запитання: "яка у мене вимога до швидкості?"

goto просто швидкий, особливо якщо ви обережно переконайтесь, що він компілюється до короткого стрибка. Ідеально підходить для додатків, де швидкість є преміум-класом. Для інших додатків, мабуть, має сенс взяти верхній удар за допомогою випадку if / else + для ремонтопридатності.

Пам'ятайте: goto не вбиває додатки, розробники вбивають програми.

ОНОВЛЕННЯ: Ось приклад випадку

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 

1
Не могли б ви представити альтернативу "випадку"? Крім того, я б стверджував, що це значно відрізняється від void *, який необхідний для будь-якої серйозної абстракції структури даних у C. Я не думаю, що хтось серйозно виступає проти void *, і ви не знайдете жодної великої бази коду без це.
Елі Бендерскі,

Re: void *, це саме моя думка, ні в чому морально погано. Приклад перемикача / випадку нижче. int foo (int bar) {int return_value = 0; int відмова_значення = 0; if (! do_something (bar)) {value_value = 1; } інакше if (! init_stuff (bar)) {value_value = 2; } в іншому випадку якщо (підготовка_матеріалів (бар)) {{return_value = do_the_thing (бар); очищення_3 (); } перемикач (значення_відмови) {випадок 2: очищення_2 (); перерва ; випадок 1: cleanup_1 (); перерва ; за замовчуванням: перерва; }}
webmarc

5
@webmarc, вибачте, але це жахливо! Ви просто повністю імітували goto з мітками - вигадували власні неописові значення для міток та реалізовували goto за допомогою перемикача / регістру. Failure_value = 1 чистіший за "goto cleanup_something"?
Елі Бендерський,

4
Я відчуваю, ніби ти влаштував мене тут ... твоє запитання стосується думки та того, що я б віддав перевагу. Але коли я запропонував свою відповідь, це жахливо. :-( Що стосується вашої скарги на назви міток, вони настільки ж описові, як і решта вашого прикладу: cleanup_1, foo, bar. Чому ви атакуєте імена міток, коли це навіть не загально для питання?
webmarc

1
Я не мав наміру "налаштовувати" і викликати будь-які негативні почуття, вибачте за це! Просто здається, що ваш новий підхід орієнтований виключно на "перехід", не додаючи більше ясності. Це наче ви повторно реалізували те, що робить goto, просто використовуючи більше коду, який є менш зрозумілим. Це ІМХО робити не дуже добре - позбутися переходу просто заради цього.
Елі Бендерський,

2

GOTO - це корисно. Це може зробити ваш процесор, і тому ви повинні мати до нього доступ.

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


3
Вам не потрібен доступ до кожного, що може зробити ваш процесор. Велику частину часу goto є більш заплутаним, ніж альтернатива.
Девід Торнлі,

@DavidThornley: Так, ви робите повинні мати доступ до кожної речі процесор може зробити, в іншому випадку, ви витрачаєте процесор. Goto - найкраща інструкція з програмування.
Рон Маймон,

1

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

  робити {
    .. налаштуйте thing1, який потребуватиме очищення лише у випадку дострокового виходу
    якщо (помилка) перерва;
    робити
    {
      .. налаштуйте річ2, яка потребуватиме очищення у разі дострокового виходу
      якщо (помилка) перерва;
      // ***** ДИВІТЬСЯ ТЕКСТ ЩОДО ЦЬОГО РЯДКУ
    } while (0);
    .. прибирання річ2;
  } в той час як (0);
  .. прибирання річ1;

але якщо очищення мало відбутися лише тоді, коли функція вийшла з ладу, gotoсправу можна було б вирішити, поставивши returnбезпосередньо перед першою цільовою міткою. Для наведеного вище коду потрібно буде додати a returnу рядку, позначеному *****.

У сценарії "очищення навіть у звичайному випадку" я б розглядав використання gotoяк більш чітке, ніж do/ while(0)конструкції, серед іншого тому, що самі цільові мітки практично вигукують "ПОГЛЯД МЕНЕ" набагато більше, ніж конструкції breakта do/ while(0). У випадку "очищення лише в разі помилки" returnвисловлювання в кінцевому підсумку повинно знаходитися приблизно в найгіршому місці з точки зору читабельності (оператори повернення, як правило, повинні бути або на початку функції, або інакше, як "виглядає" кінець); наявність returnбезпосередньо перед цільовою міткою набагато легше відповідає цій кваліфікації, ніж наявність безпосередньо перед кінцем "циклу".

До речі, один сценарій, коли я іноді використовую gotoдля обробки помилок, знаходиться в switchоператорі, коли код для кількох випадків має однаковий код помилки. Незважаючи на те, що мій компілятор часто виявляється досить розумним, щоб визнати, що кілька випадків закінчуються однаковим кодом, я думаю, що зрозуміліше сказати:

 REPARSE_PACKET:
  перемикач (пакет [0])
  {
    справа PKT_THIS_OPERATION:
      якщо (проблемний стан)
        перейти до PACKET_ERROR;
      ... обробляти THIS_OPERATION
      перерва;
    справа PKT_THAT_OPERATION:
      якщо (проблемний стан)
        перейти до PACKET_ERROR;
      ... обробляти THAT_OPERATION
      перерва;
    ...
    справа PKT_PROCESS_CONDITIONALLY
      if (довжина_пакета <9)
        перейти до PACKET_ERROR;
      якщо (умова_пакета, що включає пакет [4])
      {
        довжина пакета - = 5;
        memmove (пакет, пакет + 5, довжина пакета);
        перейти до REPARSE_PACKET;
      }
      ще
      {
        пакет [0] = PKT_CONDITION_SKIPPED;
        пакет [4] = довжина пакета;
        довжина пакета = 5;
        packet_status = READY_TO_SEND;
      }
      перерва;
    ...
    за замовчуванням:
    {
     PACKET_ERROR:
      packet_error_count ++;
      довжина пакета = 4;
      пакет [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      перерва;
    }
  }   

Незважаючи на те, що можна було б замінити gotoоператори на {handle_error(); break;}, і хоча б можна було використовувати цикл do/ while(0)разом із циком continueдля обробки загорнутого пакета умовного виконання, я не думаю, що це зрозуміліше, ніж використання goto. Далі, хоча може бути можливо скопіювати код PACKET_ERRORзвідусіль, де goto PACKET_ERRORвикористовується, і хоча компілятор може виписати повторений код один раз і замінити більшість випадків переходом до цієї спільної копії, використання за допомогою gotoполегшує помітити місця які встановлюють пакет трохи інакше (наприклад, якщо інструкція "виконувати умовно" вирішить не виконувати).


1

Я особисто є послідовником "Сили десяти - 10 правил написання критичного кодексу безпеки" .

Я включу невеликий фрагмент із цього тексту, який ілюструє те, що, на мою думку, є гарною ідеєю щодо goto.


Правило: Обмежте весь код дуже простими конструкціями потоку управління - не використовуйте оператори goto, конструкції setjmp або longjmp, а також пряму або непряму рекурсію.

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


Заборона використання goto здається поганою, але:

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


22
Проблема в тому, що звичайним способом повного вигнання gotoє використання деякого набору «розумних» логічних типів у глибоко вкладених ifs або циклах. Це справді не допомагає. Можливо, ваші інструменти будуть це краще відчувати, але ви ні, і ви важливіші.
Donal Fellows

1

Я згоден з тим, що очищення goto у зворотному порядку, наведеному у питанні, є найчистішим способом очищення речей у більшості функцій. Але я також хотів зазначити, що іноді ви хочете, щоб ваша функція все одно очистилася. У цих випадках я використовую такий варіант, якщо if (0) {label:} ідіома, щоб перейти до потрібної точки процесу очищення:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }

0

Мені здається, що cleanup_3слід зробити його прибирання, а потім зателефонувати cleanup_2. Подібним чином cleanup_2слід зробити це очищення, а потім викликати cleanup_1. Здається, що будь-коли ви це робите cleanup_[n], cleanup_[n-1]це потрібно, тому метод повинен відповідати за це (так що, наприклад, cleanup_3ніколи не можна викликати без виклику cleanup_2та, можливо, спричинення витоку.)

З огляду на такий підхід, замість gotos, ви просто викликаєте процедуру очищення, а потім повертаєтесь.

Однак gotoпідхід не є неправильним чи поганим , однак варто лише зазначити, що це не обов’язково найчистіший підхід (ІМХО).

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


4
Це не зменшить - очищення стосуються ресурсів, які розподіляються в такому порядку лише у цій процедурі. В інших місцях вони не пов’язані між собою, тому дзвонити одному з іншого не має сенсу.
Елі Бендерський,

0

Я думаю, що питання тут є помилковим стосовно даного коду.

Розглянемо:

  1. do_something (), init_stuff () та Prepa_stuff (), здається, знають, чи не вдалося, оскільки вони повертають або false, або nil у такому випадку.
  2. Відповідальність за створення штату, як видається, є відповідальністю цих функцій, оскільки жодна держава не створюється безпосередньо у foo ().

Отже: do_something (), init_stuff () та Prepa_stuff () повинні виконувати власні очищення . Наявність окремої функції cleanup_1 (), яка очищається після do_something (), порушує філософію інкапсуляції. Це поганий дизайн.

Якщо вони зробили власну очистку, тоді foo () стає досить простим.

З іншої сторони. Якщо foo () насправді створив власний стан, який потрібно було зруйнувати, то goto було б доречним.


0

Ось що я віддав перевагу:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}

0

Однак стара дискусія .... а як щодо використання "шаблону анти-стрілки" та інкапсуляції пізніше кожного вкладеного рівня у статичну вбудовану функцію? Код виглядає чисто, він є оптимальним (коли ввімкнено оптимізацію) і не використовується goto. Словом, розділяй і завойовуй. Нижче наведено приклад:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

Що стосується простору, ми створюємо змінну в стеку в три рази, що не годиться, але це зникає при компіляції з -O2, видаляючи змінну зі стеку та використовуючи регістр у цьому простому прикладі. Що я отримав із наведеного вище блоку, так gcc -S -O2 test.cце нижче:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

-1

Я вважаю за краще використовувати техніку, описану в наступному прикладі ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

джерело: http://blog.staila.com/?p=114


2
непомітний код та анти-шаблон стрілки (обидва показані у вашому прикладі) - це речі, які без потреби ускладнюють код. Немає жодного виправдання за межами "goto is evil", щоб використовувати їх.
KingRadical

-1

Ми використовуємо Daynix CStepsбібліотеку як ще одне рішення " проблеми goto " у функціях init.
Дивіться тут і тут .


3
зловживання макросами (як і в CSteps) набагато гірше, ніж розумне використанняgoto
KingRadical
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.