Які є кращі способи уникнути часу (0); зламати в C ++?


233

Коли потік коду такий:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

Я зазвичай бачив цю роботу, щоб уникнути вищезгаданого брудного потоку коду:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Які кілька кращих способів уникнути цього обходу / злому, щоб він став кодом вищого рівня (галузевий)?

Будь-які пропозиції, які не вийшли з коробки, вітаються!


38
RAII та кидайте винятки.
ta.speot.is

135
Мені це здається гарним часом для використання goto- але я впевнений, що хтось відзначить мене за те, що він запропонував це, тому я не пишу відповіді на це. Мати довге do ... while(0);здається невірною справою.
Матс Петерсон

42
@dasblinkenlight: Так, справді. Якщо ви збираєтеся використовувати goto, будьте чесні з цим і робіть це на відкритому повітрі, не ховайте цього, використовуючи breakіdo ... while
Mats Petersson

44
@MatsPetersson: Зробіть це відповіддю, я дам вам +1 за ваші gotoреабілітаційні зусилля. :)
wilx

27
@ ta.speot.is: "RAII та кидати винятки". Роблячи це, ви будете імітувати контроль потоку за винятком. Тобто це якось, як використання дорогого кровоточивого обладнання в якості молотка або ваги для паперу. Ви можете це зробити, але це, безумовно, схоже на дуже поганий смак для мене.
SigTerm

Відповіді:


309

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

Наприклад:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}

22
@MatsPetersson: "Ізолювати функцію" означає рефакторинг на нову функцію, яка виконує лише тести.
MSalters

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

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

24
@Damon, якщо що, returnчистіше, тому що будь-який читач одразу усвідомлює, що він працює правильно і що робить. З gotoвами потрібно озирнутися, щоб побачити, що це таке, і бути впевненим, що жодних помилок не було. Маскування - це користь.
Р. Мартіньо Фернандес

11
@SigTerm: Іноді код слід переміщувати в окрему функцію лише для того, щоб зменшити розмір кожної функції до простого в розумі розміру. Якщо ви не хочете платити за виклик функції, позначте його forceinline.
Бен Войгт

257

Бувають випадки, коли використання gotoнасправді ПРАВИЛЬНА відповідь - принаймні тим, хто не виховується в релігійній вірі, що "goto ніколи не може бути відповіді, незалежно від того, про що йдеться», - і це один із таких випадків.

Цей код використовує хак з do { ... } while(0);для єдиної мети одягання gotoа break. Якщо ви збираєтеся використовуватиgoto , то будьте відкриті щодо цього. Немає сенсу змушувати читати код ТРЕБІШЕ.

Особлива ситуація полягає лише в тому, що у вас багато коду з досить складними умовами:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

Насправді це Чіткіше, що код правильний, не маючи такої складної логіки.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

Редагувати: Для уточнення я ні в якому разі не пропоную використовувати gotoяк загальне рішення. Але бувають випадки, колиgoto краще рішення, ніж інші рішення.

Уявімо, наприклад, що ми збираємо деякі дані, і різні умови, на які перевіряються, - це якесь "це кінець даних, що збираються" - що залежить від якихось маркерів "продовжити / закінчити", які залежать від місця ви знаходитесь у потоці даних.

Тепер, коли ми закінчимо, нам потрібно зберегти дані у файл.

І так, часто є інші рішення, які можуть забезпечити розумне рішення, але не завжди.


76
Не погоджуюсь. gotoможе мати місце, але goto cleanupцього немає. Очищення проводиться за допомогою RAII.
MSalters

14
@MSalters: Це передбачає, що очищення включає щось, що можна вирішити за допомогою RAII. Можливо, я повинен був сказати "помилка випуску" або щось подібне замість цього.
Матс Петерсон

25
Отже, продовжуйте оскаржувати цю проблему, імовірно, від релігійних переконань goto, що ніколи не є правильною відповіддю. Буду вдячний, якби був коментар ...
Mats Petersson

19
люди ненавидять, gotoбо треба думати, щоб його використовувати / щоб зрозуміти програму, яка її використовує ... Мікропроцесори, з іншого боку, побудовані jumpsі conditional jumps... тому проблема полягає в деяких людей, а не в логіці чи чомусь іншому.
woliveirajr

17
+1 для відповідного використання goto. RAII не є правильним рішенням, якщо очікуються помилки (не виняткові), оскільки це буде зловживанням винятками.
Джо

82

Ви можете використовувати простий шаблон продовження зі boolзмінною:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

Цей ланцюжок виконання припиняється, як тільки checkNповертається a false. Більше check...()не здійснюватиметься дзвінків через коротке замикання &&оператора. Більше того, оптимізатори компіляторів досить розумні, щоб визнати, що налаштування goOnна falseце одностороння вулиця, і вставити відсутні goto endдля вас. Як результат, виконання коду, наведеного вище, було б ідентичним результатам а do/ while(0), лише без болісного удару по його читабельності.


30
Призначення в ifумовах виглядає дуже підозріло.
Михайло

90
@Mikhail Тільки нетренованому оку.
dasblinkenlight

20
Я раніше користувався цією технікою, і це завжди набридло мені, що я вважав, що компілятору доведеться генерувати код для перевірки кожного, ifнезалежно від того, що goOnбуло, якщо ранній не вдався (на відміну від стрибків / виривань) ... але я просто провів тест, і VS2012 принаймні був досить розумним, щоб коротко замикати все після першої помилки. Я буду користуватися цим частіше. Примітка: якщо ви використовуєте, goOn &= checkN()то checkN()завжди буде працювати, навіть якщо це goOnбуло falseна початку if(тобто не робіть цього).
позначка

11
@Nawaz: Якщо у вас непрофесійний розум внесення довільних змін по всій кодовій базі, у вас набагато більша проблема, ніж просто завдання в ifs.
Іделік

6
@sisharp Елегантність так в очах глядача! Я не розумію, як у світі неправомірне використання петельної конструкції можна якось сприймати як "елегантне", але, можливо, це лише я.
dasblinkenlight

38
  1. Спробуйте витягнути код в окрему функцію (або, можливо, більше). Потім поверніться з функції, якщо перевірка не виконана.

  2. Якщо це занадто щільно поєднано з навколишнім кодом, щоб це зробити, і ви не можете знайти спосіб зменшити з'єднання, подивіться на код після цього блоку. Імовірно, він очищує деякі ресурси, використовувані функцією. Спробуйте керувати цими ресурсами за допомогою об’єкта RAII ; потім замініть кожну хитрість breakна return(або throw, якщо це підходить), і нехай деструктор очистить вас.

  3. Якщо потік програми (обов'язково) настільки чіткий, що вам справді потрібен goto, тоді використовуйте це, а не даючи йому дивну маскування.

  4. Якщо у вас є правила кодування, які сліпо забороняють goto, і ви дійсно не можете спростити потік програми, то, ймовірно, вам доведеться замаскувати його з допомогою doхак- коду .


4
Я смиренно стверджую, що RAII, хоча й корисний, не є срібною кулею. Коли ви збираєтеся написати клас перетворення-goto-в-RAII, який не має іншого використання, я, безумовно, думаю, що вам краще послужити, просто використовуючи вже згадувані люди ідіоми "goto-end-of-the-world".
idoby

1
@busy_wait: Дійсно, RAII не може вирішити все; тому моя відповідь не зупиняється на другому пункті, але продовжує підказувати, gotoчи справді це кращий варіант.
Майк Сеймур

3
Я погоджуюся, але я думаю, що це погана ідея писати перетворювальні класи goto-RAII, і я думаю, що це слід чітко вказати.
idoby

@idoby, що робити з написанням одного шаблону RAII шаблонів та інстанціюванням його за допомогою будь-якого
способу

@Caleth мені звучить так, ніби ти винаходиш диски / дтори?
idoby

37

TLDR : RAII , код транзакцій (встановлюйте результати або повертайте матеріали, коли вони вже обчислені) та винятки.

Довга відповідь:

У C найкращою практикою для цього виду коду є додавання в код мітки EXIT / CLEANUP / іншої , де відбувається очищення локальних ресурсів та повертається код помилки (якщо такий є). Це найкраща практика, оскільки він природно розбиває код на ініціалізацію, обчислення, фіксацію та повернення:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

У C, у більшості баз коду, if(error_ok != ...і gotoкод зазвичай прихований за деякими макросами зручності ( RET(computation_result),ENSURE_SUCCESS(computation_result, return_code) і т.д.).

C ++ пропонує додаткові інструменти понад C :

  • Функціонал блоку очищення може бути реалізований як RAII, що означає, що вам більше не потрібен весь cleanupблок і дозволяє клієнтському коду додавати оператори раннього повернення.

  • Ви кидаєте, коли не можете продовжувати, перетворюючи всі if(error_ok != ...прямі виклики в прямі.

Еквівалентний код C ++:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

Це найкраща практика, оскільки:

  • Він явний (тобто, якщо обробка помилок не явна, основний потік алгоритму є)

  • Написати код клієнта нескладно

  • Він мінімальний

  • Це просто

  • У ньому немає повторюваних конструкцій коду

  • Тут не використовуються макроси

  • Він не використовує дивні do { ... } while(0)конструкції

  • Він може бути повторно використаний з мінімальними зусиллями (тобто, якщо я хочу скопіювати виклик на computation2();іншу функцію, мені не потрібно переконуватись, що я додаю do { ... } while(0)новий код, ні #defineмакро goto обгортки та мітку очищення, ні будь-що інше).


+1. Це те, що я намагаюся використовувати, використовуючи RAII або щось подібне. shared_ptrза допомогою спеціального делетера можна зробити багато чого. Ще простіше з лямбдами в C ++ 11.
Макке

Щоправда, але якщо ви в кінцевому підсумку використовуєте shared_ptr для речей, які не є покажчиками, врахуйте принаймні набір даних : namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };У цьому випадку (при make_handleвстановленні правильного типу делетора на конструкції), назва типу більше не говорить про те, що це покажчик .
utnapistim

21

Я додаю відповідь заради повноти. Ряд інших відповідей вказував на те, що великий блок умов можна розділити на окрему функцію. Але, як також неодноразово вказувалося, такий підхід відокремлює умовний код від початкового контексту. Це одна з причин того, що лямбда були додані до мови в C ++ 11. Використання лямбдаз було запропоновано іншими, але чіткого зразка не було надано. Я поставив один у цій відповіді. Мене вражає те, що він do { } while(0)багато в чому схожий на підхід - і, можливо, це означає, що він все ще gotoмаскується….

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations

7
Для мене цей злом виглядає гірше, ніж у ... під час злому.
Майкл

18

Звичайно , НЕ відповідь, але відповідь (для повноти картини )

Замість :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Ви можете написати:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

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

Конструкція також досить проста, що можна сподіватися, що компілятор оптимізує її.

Як запропонував @jamesdlin, ви навіть можете приховати це за таким макросом

#define BLOC switch(0) case 0:

І використовувати це як

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

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


2
О, розумний. Ви навіть можете сховати його за таким макросом, як define BLOCK switch (0) case 0:і використовувати його BLOCK { ... break; }.
jamesdlin

@jamesdlin: ніколи раніше мені не приходило в голову, що це може бути корисним, щоб поставити справу перед початковою дужкою комутатора. Але це дійсно дозволено C, і в цьому випадку зручно написати хороший макрос.
kriss

15

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

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}

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

2
Я не пам’ятаю, щоб ОП нічого говорило про придбання ресурсів у середині його кодового блоку, але я приймаю це як можливу вимогу. У цьому випадку що не так у оголошенні ресурсу на стеку в будь-якій точці func()і дозволити його деструктору обробляти звільнення ресурсів? Якщо щось, що не func()потребує доступу, до того ж ресурсу, його слід оголосити на купі до виклику func()відповідним менеджером ресурсів.
Ден Бешард

12

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


11

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

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()

2
Але тоді кожна умова повинна включати попередню, яка не тільки негарна, але й потенційно погана для виконання. Краще перескочити ці перевірки та прибирання негайно, як тільки ми дізнаємось, що ми «не в порядку».
TNE

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

10

Схожий на відповідь dasblinkenlight, але уникає призначення всередині того, ifщо може "виправити" рецензент коду:

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

Цю схему я використовую, коли результати кроку потрібно перевірити до наступного кроку, який відрізняється від ситуації, коли всі перевірки можна було зробити попередньо з великим if( check1() && check2()...типом.


Зауважте, що check1 () дійсно може бути PerformStep1 (), який повертає код результату кроку. Це дозволить уникнути складності вашої функції потоку процесу.
Деніз Скідмор

10

Використовуйте винятки. Ваш код буде виглядати набагато чіткіше (а винятки створені саме для обробки помилок у потоці виконання програми). Щоб очистити ресурси (дескриптори файлів, підключення до бази даних тощо), прочитайте статтю Чому C ++ не забезпечує конструкцію "нарешті"? .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}

10
Дійсно? По-перше, я, чесно кажучи, не бачу жодного покращення читабельності. НЕ ВИКОРИСТОВУЙТЕ ВИКЛЮЧЕННЯ для управління ПОТОКОМ ПРОГРАМИ Це НЕ їх належне призначення. Крім того, винятки несуть значні штрафи за ефективність роботи. Належний випадок використання для винятку - це коли є певна умова, ЩО НЕ МОЖЕТЕ РОБИТИ НІЧОГО, наприклад, спроба відкрити файл, який вже виключно заблокований іншим процесом, або не вдалося підключити мережу, або виклик до бази даних не вдається. або абонент передає недійсний параметр процедурі. Така річ. Але НЕ використовуйте винятки для управління потоком програми.
Крейг

3
Я маю на увазі, щоб не бути соплями з цього приводу, але перейдіть до тієї статті Stroustrup, на яку ви посилаєтесь, і шукайте "Для чого я не повинен використовувати винятки?" розділ. Серед іншого, він говорить: "Зокрема, кидок - це не просто альтернативний спосіб повернення значення з функції (подібного до повернення). Це буде повільним і заплутає більшість програмістів на C ++, які використовуються для вибору винятків, що використовуються лише для помилок Аналогічно, кидання - це не гарний спосіб виходу з циклу ".
Крейг

3
@Craig Все, на що ви вказали, - це правильно, але ви припускаєте, що зразок програми може продовжуватися після того, як check()умова провалиться, і це певно ВАШЕ припущення, у вибірці немає контексту. Якщо припустити, що програма не може продовжуватись, використання винятків - це шлях.
Картухо

2
Ну, правда, якщо це насправді контекст. Але, дивна річ щодо припущень ... ;-)
Крейг

10

Для мене do{...}while(0)це добре. Якщо ви не хочете бачитиdo{...}while(0) , ви можете визначити для них альтернативні ключові слова.

Приклад:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;

   //processing code here

END_TEST

Я думаю, що компілятор видалить непотрібну while(0)умову вdo{...}while(0) у двійковій версії та перетворить перерви в безумовний стрибок. Ви можете переконатися, що це версія мови монтажу.

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

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;

   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.

Зверніть увагу, етикетка ставиться після закриття }. Це уникає однієї можливої ​​проблеми в gotoтому, що випадкове розміщення коду між ними, оскільки ви не побачили етикетку. Зараз це як do{...}while(0)без коду умови.

Щоб зробити цей код більш чистим і зрозумілим, ви можете зробити це:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

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

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);

      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

Код спагетті не є виною goto, це вина програміста. Ви все ще можете виробляти код спагетті без використання goto.


10
Я б вирішив gotoрозширити синтаксис мови за допомогою препроцесора мільйон разів.
Крістіан

2
"Щоб зробити цей код більш чистим і зрозумілим, ви можете [використовувати LOADS_OF_WEIRD_MACROS]" : не обчислює.
підкреслюй_

8

Це відома і добре вирішена проблема з точки зору функціонального програмування - можливо монада.

У відповідь на коментар, який я отримав нижче, я відредагував своє вступ тут: Ви можете знайти повну інформацію про реалізацію монардів C ++ у різних місцях монардів що дозволить вам досягти того, що пропонує Ротсор. Щоб витратити монади, потрібен певний час, тому замість цього я запропоную тут швидкий монадоподібний механізм, який нагадує монаду, про який вам не потрібно знати нічого, крім boost :: необов’язковий.

Налаштуйте свої кроки обчислення таким чином:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

Кожен обчислювальний крок, очевидно, може зробити щось на кшталт повернення, boost::noneякщо необов’язковий він був заданий порожнім. Так, наприклад:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

Потім з'єднайте їх між собою:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

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

Якщо вам не байдуже незмінність, і зручніше повертати один і той же об’єкт кожного разу, коли ви можете придумати певні зміни, використовуючи shared_ptr або подібне.


3
Цей код має небажану властивість змушувати кожну з окремих функцій обробляти відмову попередньої функції, таким чином, не використовуючи належним чином ідіому Монада (де монадійні ефекти, в даному випадку збої, повинні поводитися неявно). Для цього вам потрібно мати optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);замість цього і використовувати монадійну операцію композиції ("прив'язувати"), а не функціональну програму.
Ротсор

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

7

Як щодо переміщення операторів if у додаткову функцію, що дає числовий чи перерахувальний результат?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}

Це приємно, коли можливо, але набагато менш загальне, ніж тут задається питання. Кожна умова може залежати від коду, виконаного після останнього тесту дерашінгу.
kriss

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

@kriss: Що ж, функцію ConditionCode () можна було налаштувати для того, щоб зайнятися цим. Основним моментом цієї функції є те, що ви можете використовувати return <result> для чистого виходу, як тільки буде обчислена остаточна умова; І саме це забезпечує структурну ясність тут.
karx11erx

@Riga: Це цілком академічні заперечення. Я вважаю, що C ++ стає складнішим, виразнішим і нечитабельнішим із кожною новою версією. Я не можу побачити проблему з невеликою помічною функцією, що оцінює складні умови добре структурованим способом, щоб зробити цільову функцію прямо нижче.
karx11erx

1
@ karx11erx мої заперечення практичні і ґрунтуються на моєму досвіді. Ця закономірність погано пов'язана з C ++ 11 або будь-якою мовою. Якщо у вас є труднощі з мовними конструкціями, які дозволяють написати гарну архітектуру, яка не є мовною проблемою.
Рига

5

Можливо, щось подібне

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

або використовувати винятки

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

Використовуючи винятки, ви також можете передавати дані.


10
Будь ласка, не робіть розумних речей, таких як ваше визначення коли-небудь, вони зазвичай роблять код важче читати для інших розробників. Я бачив, як хтось визначав Case як break; case у файлі заголовка, і використовував його в комутаторі у файлі cpp, змушуючи інших годинами дивуватися, чому перемикач переривається між операторами Case. Grrr ...
Майкл

5
І коли ви називаєте макроси, ви повинні зробити їх схожими на макроси (тобто у всіх великих літерах). Інакше хтось, хто трапляється назвати змінну / функцію / тип / тощо. названий everбуде дуже нещасний ...
jamesdlin

5

Я не особливо вдаюся до використання breakабоreturn в такому випадку. Зважаючи на те, що зазвичай ми стикаємося з такою ситуацією, як правило, це порівняно довгий метод.

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

Наприклад,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

Переглядаючи блоки, що вкладаються, легко з’ясувати, що myLogic()відбувається лише тоді, колиconditionA and conditionB and conditionC це правда.

Це стає набагато менш помітним, коли є раннє повернення:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

Ми більше не можемо орієнтуватися вгору myLogic() , дивлячись на блок, що з’являється, щоб зрозуміти стан.

Є різні способи вирішення, якими я користувався. Ось один із них:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(Звичайно, можна замінити всі ті ж змінні isA isB isC .)

Такий підхід принаймні надасть читачеві коду, який myLogic()виконується коли isB && conditionC. Читачеві надається підказка, що йому потрібно додатково шукати, що спричинить, щоб ІБ було правдою.


3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

Як щодо цього?


якщо if застаріло, просто return condition;, інакше я вважаю, що це цілком ретельно.
SpaceTrucker

2

Ще одна корисна модель, якщо вам потрібні різні етапи очищення залежно від місця відмови:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }

2

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

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

Хороша річ, що така конструкція дуже добре дотримується принципу відкритого / закритого типу , оскільки ви можете легко додати нові умови під час ініціалізації об'єкта, що містить відповідний метод. Ви можете навіть додати другий метод в інтерфейс із методом оцінки стану, що повертає опис умови. Це можна використовувати для систем самодокументування.

Мінус, однак, полягає в тому, що дещо більше задіяних накладних витрат через використання більшої кількості об'єктів та ітерації над списком.


Чи можете ви додати приклад іншою мовою? Я думаю, що це питання стосується багатьох мов, хоча його задавали спеціально щодо C ++.
Деніз Скідмор

1

Це так, як я це роблю.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}

1

Спочатку короткий приклад, щоб показати, чому gotoце не вдале рішення для C ++:

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

Спробуйте скомпілювати це в об’єктний файл і побачити, що відбувається. Потім спробуйте еквівалент do+ break+ while(0).

Це було вбік. Випливає головний пункт.

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

Одним із варіантів отримання цих семантик є RAII ; див. відповідь @ utnapistim. C ++ гарантує, що автоматичні деструктори працюватимуть у зворотному порядку до конструкторів, що, природно, забезпечує «розмотування».

Але для цього потрібно багато класів RAII. Іноді більш простим варіантом є просто використовувати стек:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

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

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


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

0

Якщо у коду є довгий блок тверджень if..else if..else, ви можете спробувати переписати весь блок за допомогою Functorsабо function pointers. Це може бути не завжди правильним рішенням, але досить часто є.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html


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

0

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

Початковий код:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

Зараз:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

Отже, що тут зроблено, це те, що оздоблювальний матеріал був ізольований у функції, і речі раптом стали такими простими та чистими!

Я вважав, що це рішення варто згадати, тому надав його тут.


0

Об'єднайте його в одне ifтвердження:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

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


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

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

@AJMansfield так, саме про це я говорив у своєму другому реченні ... але я не впевнений, що це буде поліпшенням якості коду.
Джеремі Фріснер

@JeremyFriesner Ніщо не заважає вам писати умови, оскільки (/*do other stuff*/, /*next condition*/)ви навіть можете їх добре відформатувати. Просто не сподівайтеся, що людям це сподобається. Але, чесно кажучи, це свідчить лише про те, що Java помилково goto
припинила

@JeremyFriesner Я припускав, що це булі. Якщо функції потрібно було запускати всередині кожної умови, то є кращий спосіб впоратися з цим.
Тизоїд

0

Якщо ви використовуєте однаковий обробник помилок для всіх помилок, і кожен крок повертає bool, що вказує на успіх:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(Подібно до відповіді тизоїда, але умовами є дії, а && запобігає виникненню додаткових дій після першого відмови.)


0

Чому не відповів метод позначення, він використовується з віків.

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.