Як уникнути ланцюгів "якщо"?


266

Припустимо, що у мене є цей псевдо-код:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

Функції executeStepXслід виконувати, якщо і лише в тому випадку, коли попередній успіх. У будь-якому випадку executeThisFunctionInAnyCaseфункцію слід викликати наприкінці. Я новачок у програмуванні, тож вибачте за основне питання: чи є спосіб (наприклад, у C / C ++) уникнути того довгого ifланцюга, який створює таку "піраміду коду" за рахунок розбірливості коду ?

Я знаю, що якби ми могли пропустити executeThisFunctionInAnyCaseвиклик функції, код можна спростити як:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Але обмеженням є executeThisFunctionInAnyCaseвиклик функції. Чи breakможна висловлювання якось використовувати?


254
@ FrédéricHamidi неправильно неправильно неправильно! Ніколи не кажіть, що керування потоком вашої програми за винятками добре! Винятки, безумовно, НЕ підходять для цієї мети з занадто багатьох причин.
Пьотр Цирхоффер

26
@Piotr, мене зіпсував Python (що насправді це заохочує). Я знаю, що винятки не повинні використовуватися для контролю потоку в C ++, але чи дійсно тут контроль потоку? Чи не можна було повернути якусь функцію falseяк подібну до виняткової ситуації?
Фредерік Хаміді

13
Це залежить від семантики програми. falseПовернення може бути цілком нормальним.
dornhege

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

13
Я б хотів, щоб усі «новачки-програмісти» задавали такі питання дизайну.
Єзен Томас

Відповіді:


486

Ви можете використовувати &&(логічний І):

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

це задовольнить обидві ваші вимоги:

  • executeStep<X>()слід оцінювати лише в тому випадку, якщо попереднє вдалося (це називається оцінкою короткого замикання )
  • executeThisFunctionInAnyCase() буде виконано в будь-якому випадку

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

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

24
Це має бути головна відповідь
Відображати ім’я

61
@RobAu: Це не хакер, використовуючи якийсь незрозумілий синтаксичний трюк, це дуже ідіоматично майже в кожній мові програмування, до того моменту, що це незаперечно стандартна практика.
BlueRaja - Danny Pflughoeft

38
Це рішення застосовується лише в тому випадку, якщо умови насправді викликають прості функції. У реальному коді ці умови можуть бути завдовжки 2-5 рядків (а самі вони є комбінаціями багатьох інших &&та ||), тому немає жодного способу, ifякби ви хотіли б з'єднати їх в одне висловлення без викрутки для читання. І перемістити ці умови до зовнішніх функцій не завжди просто, оскільки вони можуть залежати від безлічі раніше обчислених локальних змінних, що створило б жахливий безлад, якщо спробувати передати кожну окрему аргументацію.
хамстерген

358

Просто використовуйте додаткову функцію, щоб змусити свою другу версію працювати:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

Використання або глибоко вкладеного ifs (ваш перший варіант), або бажання вийти з "частини функції" зазвичай означає, що вам потрібна додаткова функція.


51
+1 нарешті хтось опублікував здорову відповідь. На мою думку, це найбільш правильний, безпечний і читабельний спосіб.
Лундін

31
+1 Це хороша ілюстрація "Принципу єдиної відповідальності". Функція fooспрацьовує через послідовність пов'язаних умов та дій. Функція barчітко відокремлена від рішень. Якби ми побачили деталі умов та дій, може виявитись, що fooвсе ще робиться занадто багато, але наразі це хороше рішення.
GraniteRobert

13
Мінус полягає в тому, що C не має вкладених функцій, тому якщо ці 3 кроки, необхідні для використання змінних від barвас, доведеться вручну передавати їх fooяк параметри. Якщо це так, і якщо він fooвикликається лише один раз, я б помилявся з використанням goto-версії, щоб уникнути визначення двох щільно зв'язаних функцій, які в кінцевому підсумку не дуже використовуються.
хугомг

7
Не впевнений у синтаксисі короткого замикання для C, але в C # foo () можна було б записати так:if (!executeStepA() || !executeStepB() || !executeStepC()) return
Тревіс

6
@ user1598390 Goto's використовуються постійно, особливо в системному програмуванні, коли потрібно розкрутити багато налаштування коду.
Скотті Бауер

166

Програмісти зі старої школи gotoв цьому випадку використовують. Це одне використання, gotoяке насправді заохочує керівництво стилем Linux, воно називається вихід централізованої функції:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

Деякі люди працюють навколо використання goto, загортаючи тіло в петлю і відриваючись від нього, але ефективно обидва підходи роблять те саме. gotoПідхід краще , якщо вам потрібні інші очищення , тільки якщо executeStepA()був процвітаючим:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

З підходом до циклу у цьому випадку ви отримаєте два рівні петлі.


108
+1. Дуже багато людей бачать gotoі негайно думають, що "Це страшний код", але він має дійсне використання.
Graeme Perrow

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

29
-1 Я бачу, gotoі мені навіть не потрібно думати, щоб побачити, що це страшний код. Мені колись доводилося підтримувати таке, це дуже неприємно. В кінці питання ОП пропонує розумну альтернативу мові С, і я включив це у свою відповідь.
Ура та хт. - Альф

56
Немає нічого поганого в цьому обмеженому, самостійному використанні гото. Але будьте обережні, гото - це ворота, і якщо ви не будете обережні, ви одного разу зрозумієте, що ніким більше не їсть спагетті своїми ногами, і ви це робите вже 15 років, починаючи після того, як ви подумали: «Я можу це просто виправити в іншому випадку кошмарний помилок з етикеткою ... "
Tim Post

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

131

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

Це Стріла

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

Згладьте стрілку охоронцем

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

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... ти б використовував ....

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

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

Стрілка:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

Охорона:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

Це об’єктивно і кількісно простіше читати, оскільки

  1. Символи {і} для даного логічного блоку ближче один до одного
  2. Обсяг ментального контексту, необхідний для розуміння певної лінії, менший
  3. Повна логіка, пов'язана з умовою if, швидше за все, буде на одній сторінці
  4. Потреба в кодері прокручувати сторінку / очну доріжку значно зменшується

Як додати загальний код в кінці

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

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

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

Варіант 1. Ви не можете цього зробити finally

На жаль, як розробник c ++, ви не можете цього зробити. Але це відповідь номер один для мов, які містять нарешті ключове слово, оскільки саме це і є.

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

Варіант 2. Уникайте проблеми: реструктуруйте свої функції

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

Ось приклад:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Варіант 3. Мовна хитрість: використовуйте підроблений цикл

Ще одна поширена хитрість, яку я бачу, - це використовувати while (true) та перерву, як показано в інших відповідях.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

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

Існують і інші варіанти цього варіанту. Наприклад, можна використовувати switchзамість while. Будь-яка мовна конструкція з breakключовим словом, ймовірно, спрацює.

Варіант 4. Використовуйте життєвий цикл об'єкта

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

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Примітка. Переконайтеся, що ви розумієте життєвий цикл об'єкта на обраній вами мові. Для цього вам потрібен якийсь детермінований збір сміття, тобто ви повинні знати, коли буде викликаний деструктор. У деяких мовах вам потрібно буде використовувати Disposeзамість деструктора.

Варіант 4.1. Використовуйте життєвий цикл об'єкта (візерунок обгортки)

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

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Знову ж таки, будьте впевнені, що ви розумієте свій життєвий цикл об'єкта.

Варіант 5. Мовна хитрість: Використовуйте оцінку короткого замикання

Ще одна методика - скористатися оцінкою короткого замикання .

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

Це рішення використовує переваги способу роботи оператора &&. Якщо ліва частина && оцінює значення false, права частина ніколи не оцінюється.

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


12
Нарешті? C ++ не має остаточного застереження. Об'єкт з деструктором використовується в ситуаціях, коли ви вважаєте, що вам потрібно остаточно застереження.
Двері Білла

1
Відредаговано так, щоб вмістити два коментарі вище.
Джон Ву

2
Із тривіальним кодом (наприклад, у моєму прикладі) вкладені візерунки, ймовірно, більш зрозумілі. З реальним кодом світу (який може охоплювати кілька сторінок), шаблон захищання об'єктивно легше читати, оскільки він вимагатиме менше прокрутки та менше відстеження очей, наприклад, середня відстань від {до} коротша.
Джон Ву

1
Я бачив вкладений візерунок, де код більше не був видно на екрані 1920 x 1080 ... Спробуйте дізнатися, який код обробки помилок буде виконуватися, якщо третя дія не вдалася ... Я використовував do {...} (0) замість цього вам не потрібна остаточна перерва (з іншого боку, тоді як (правда) {...} дозволяє "продовжувати" починати все спочатку.
gnasher729

2
Ваш варіант 4 - це фактично витік пам'яті в C ++ (ігнорування незначної синтаксичної помилки). У такому випадку не використовується "new", просто скажіть "MyContext myContext;".
Сумуду Фернандо

60

Просто роби

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

Це так просто.


Через три редагування, кожне з яких принципово змінило питання (чотири, якщо один вважає перегляд версії до версії №1), я включаю приклад коду, на який я відповідаю:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

14
Я відповів на це (більш стисло) у відповідь на першу версію питання, і він отримав 20 оновлень, перш ніж його видалив Білл Ящірка, після деякого коментаря та редагування Darkness Races в Орбіті.
Ура та хт. - Альф

1
@ Cheersandhth-Alf: Я не можу повірити, що його видалено модом. Це просто дуже погано. (+1)
Парамагнітний круасан

2
Мій +1 до вашої сміливості! (3 рази має значення: D).
хакі

Зовсім важливо, щоб програмісти-початківці дізналися про такі речі, як виконання замовлень із декількома булевими "ands", як це різниться різними мовами тощо. І ця відповідь є чудовим покажчиком на це. Але це просто "нестартер" у звичайному комерційному програмуванні. Це не просто, це не є ремонтом, не підлягає модифікації. Звичайно, якщо ви просто пишете для себе швидкий «ковбойський код», зробіть це. Але це просто "ніякого зв'язку з повсякденною розробкою програмного забезпечення, як це практикується сьогодні" До речі вибачте за смішну «редагувати-плутанину» Ви повинні були витримати тут, @cheers :)
Fattie

1
@JoeBlow: Я з Альфом з цього приводу - мені здається, що &&список набагато чіткіший. Я зазвичай розбиваю умови на окремі рядки і додаю кінцевий пробіл // explanationдо кожного .... Зрештою, це набагато менше коду для перегляду, і як тільки ти зрозумієш, як &&працює, немає постійних розумових зусиль. Моє враження, що більшість професійних програмістів на C ++ були б знайомі з цим, але, як ви кажете, в різних галузях / проектах фокус та досвід відрізняються.
Тоні Делрой

35

Насправді існує спосіб відкласти дії в C ++: використання деструктора об'єкта.

Припустимо, що у вас є доступ до C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

А потім використовуючи цю утиліту:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo

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

24
@Lundin: Ну, я не розумію, як можна задовольнитися крихким кодом, який порушиться, як тільки буде введено раннє продовження / перерва / повернення або викид виключення. З іншого боку, це рішення є стійким перед майбутнім обслуговуванням і покладається лише на те, що деструктори виконуються під час розмотування, що є однією з найважливіших особливостей С ++. Кваліфікувати це як екзотичне, хоча саме основоположним принципом є повноваження всіх стандартних контейнерів, щонайменше, забавно.
Матьє М.

7
@Lundin: Перевага коду Матьє полягає в тому, що він executeThisFunctionInAnyCase();буде виконаний, навіть якщо foo();викине виняток. Коли ви пишете безпечний для винятків код, добре використовувати всі такі функції очищення в деструкторі.
Брайан

6
@Brian Тоді не кидайте жодного винятку в foo(). А якщо так, то злови. Проблема вирішена. Виправляйте помилки, виправляючи їх, а не записуючи робочі місця.
Лундін

18
@Lundin: Deferклас - це невеликий багаторазовий фрагмент коду, який дозволяє робити будь-яку очистку в кінці блоку, за винятком безпечного способу. він більше відомий як охорона сфери . да будь-яке використання області дії огорожею , можуть бути виражені в інших , більш ручними способами, як і будь-який forцикл може бути виражений у вигляді блоку і whileпетлі, які , в свою чергу , може бути виражено з ifі goto, які можуть бути виражені на мові асемблера , якщо ви хочете, або для тих, хто є справжніми господарями, змінюючи шматочки в пам’яті за допомогою космічних променів, спрямованих ефектом метелика особливих коротких бурчань та співів. але, навіщо це робити.
Ура та хт. - Альф

34

Є приємна техніка, яка не потребує додаткової функції обгортки із поверненнями операторів (метод, призначений Itjax). Він використовує while(0)псевдо-цикл do . У while (0)гарантує , що він на самому ділі не цикл , а виконується тільки один раз. Однак синтаксис циклу дозволяє використовувати оператор break.

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}

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

4
Так, безумовно, це читабельніше ... однак, з точки зору ефективності, додатковим викликом функції (передача та повернення параметра) накладні витрати можна уникнути за допомогою конструкції do {} while (0).
Дебасіс

5
Ви все ще вільні зробити функцію inline. У будь-якому випадку це приємна техніка, яку потрібно знати, адже вона допомагає в більш ніж цій проблемі.
Руслан

2
@Lundin Ви повинні взяти до уваги локацію коду, розповсюджуючи код на занадто багато місць, це теж проблеми.
API-Beast

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

19

Ви також можете це зробити:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

Таким чином, ви маєте мінімальний лінійний ріст, +1 лінія за виклик, і це легко підтримувати.


EDIT : (Спасибі @Unda) Не великий фанат, оскільки ви втрачаєте видимість IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

1
Йшлося про випадкові дзвінки функції всередині push_back (), але ви все одно це виправили :)
Квентін,

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

1
Хоча це, можливо, може виглядати чистіше. Це може бути важче зрозуміти людям, і, безумовно, складніше зрозуміти для компілятора!
Рой Т.

1
Ставлення до своїх функцій більше як до даних часто є хорошою ідеєю - як тільки ви це зробите, ви також помітите наступні рефактори. Ще краще - це те, де кожен твір, який ви обробляєте, - це посилання на об'єкт, а не лише посилання на функцію - це дасть вам ще більше можливостей покращити ваш код внизу.
Білл К

3
Трохи перероблений для тривіального випадку, але ця методика, безумовно, має приємну особливість, яку інші не роблять: Ви можете змінити порядок виконання та [кількість] функцій, викликаних під час виконання, і це приємно :)
Xocoatzin

18

Це би спрацювало? Я думаю, що це еквівалентно вашому коду.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();

3
Зазвичай я називаю змінну, okколи використовую ту саму змінну, як ця.
Макке

1
Мені було б дуже цікаво дізнатись, чому знищуються. Що тут не так?
ABCplus

1
порівняйте відповідь ур із підходом на коротке замикання за цикломатичною складністю.
AlphaGoku

14

Припустимо, що потрібний код є таким, яким я його бачу зараз:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

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

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

Це дозволяє уникнути будь-яких потреб у gotos, винятках, фіктивних whileпетлях чи інших складних конструкціях і просто починається з простої роботи.


Під час використання циклів, як правило, прийнятно використовувати returnта breakвистрибувати з циклу, не потребуючи введення додаткових змінних «прапор». У цьому випадку використання goto було б аналогічним чином - пам’ятайте, що ви торгуєте додатковою складністю goto для додаткової змінної складності.
hugomg

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

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

12

Чи не можна якоюсь умовою використовувати розрив?

Можливо, не найкраще рішення, але ви можете поставити свої заяви в do .. while (0)цикл і використовувати breakоператори замість return.


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

3
@ClickRick, що використовує do .. while (0)для визначення макросів, також зловживає циклами, але це вважається нормальним.
оуа

1
Можливо, але є більш чіткі способи її досягнення.
ClickRick

4
@ClickRick Єдиною метою моєї відповіді є відповідь. Чи не вдалося б якось використати твердження про розрив, а відповідь - так, перші слова в моїй відповіді припускають, що це може бути не рішенням для використання.
оуа

2
Ця відповідь повинна бути просто коментарем
msmucker0527

12

Ви можете поставити всі ifумови, відформатовані так, як ви хочете, у власну функцію, функція повернення виконується executeThisFunctionInAnyCase().

З базового прикладу в ОП тестування та виконання стану можна розділити як таке;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

А потім закликав як такого;

InitialSteps();
executeThisFunctionInAnyCase();

Якщо доступні C ++ 11 лямбда (у OP не було тегів C ++ 11, але вони все ще можуть бути варіантом), то ми можемо відмовитись від окремої функції та перетворити її на лямбда.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();

10

Якщо ви не любите gotoі не любите do { } while (0);петлі і любите використовувати C ++, ви можете також використовувати тимчасову лямбда, щоб мати той же ефект.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();

1
if вам не подобається, що && ви не любите робити {} в той час, як (0) && вам подобається C ++ ... Вибачте, не втримався, але ця остання умова не вдається, оскільки питання позначено як c, так і c ++
ClickRick

@ClickRick завжди важко порадувати всіх. На мою думку, немає такої речі, як C / C ++, яку ти зазвичай кодуєш в одній із них, а використання іншої нахмурюється.
Олексій

9

Ланцюги IF / ELSE у вашому коді - це не питання мови, а дизайн вашої програми. Якщо ви можете перефактурувати або переписати свою програму, я б запропонував вам заглянути в «Шаблони дизайну» ( http://sourcemaking.com/design_patterns ), щоб знайти краще рішення.

Зазвичай, коли ви бачите у своєму коді багато ІФ та інших, це можливість реалізувати шаблон дизайну стратегії ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) або, можливо, комбінацію інших моделей.

Я впевнений, що є альтернативи, щоб написати довгий список if / else, але я сумніваюся, що вони щось змінять, окрім того, що ланцюжок буде виглядати красиво на вас (Однак, краса в очах того, хто дивиться, все ще стосується коду теж :-)). Вас повинні турбувати такі речі (через 6 місяців, коли у мене новий стан, і я нічого не пам’ятаю про цей код, чи зможу його легко додати? Або що, якщо ланцюжок зміниться, як швидко і без помилок чи буду це реалізовувати)


9

Ви просто зробите це ..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 разів по 100, це єдиний спосіб зробити це.

Ніколи, ніколи, ніколи не намагайтеся зробити щось «складне» в комп'ютерному коді.


До речі, я впевнений, що наступне - це власне рішення, яке ви мали на увазі ...

Заява продовження є критичною в алгоритмічному програмуванні. (Начебто оператор goto є критичним для алгоритмічного програмування.)

Для багатьох мов програмування ви можете це зробити:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(Перш за все зауважте: голі блоки подібного прикладу є важливою і важливою частиною написання красивого коду, особливо якщо ви маєте справу з "алгоритмічним" програмуванням.)

Знову ж таки, саме це у вас було в голові, правда? І це прекрасний спосіб його написати, тому у вас є добрі інстинкти.

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

введіть тут опис зображення

Ось як ви обійшли це ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

Тож не забувайте, що ..

робити {} while (помилково);

просто означає "зробити цей блок один раз".

тобто абсолютно не існує різниці між написанням do{}while(false);та просто написанням {}.

Це зараз ідеально працює, як ви хотіли ... ось результат ...

введіть тут опис зображення

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

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

#define RUNONCE while(false)

... тож ви можете це зробити ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

Є два моменти:

a, хоч і нерозумно, що aim-c перевіряє тип блоку, у якому є твердження "продовження", "боротися з цим" викликає занепокоєння. Тож це важке рішення.

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

Сподіваюся, це допомагає.



Ви поклали щедрості, щоб отримати більше представників? :)
TMS

Замість того, щоб ставити всі ці коментарі до пункту if, ви також можете використовувати більш описові назви функцій та помістити коментарі у функції.
Thomas Ahle

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

8

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

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

Звичайно, я припускаю, що у вашому оригінальному прикладі крок виконання поверне помилковий лише у випадку помилки, що виникає всередині кроку?


3
використання виключення для контролю потоку часто вважають поганою практикою та смердючим кодом
user902383

8

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

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;

Чому ok &= conditionX;і не просто ok = conditionX;?
ABCplus

@ user3253359 У багатьох випадках так, ви можете це зробити. Це концептуальна демонстрація; у робочому коді ми б спробували максимально спростити його
blgt

+1 Один з небагатьох чистих та бездоганних відповідей, який працює в c , як зазначено в питанні.
ClickRick

6

У C ++ (питання позначено як C і C ++), якщо ви не можете змінити функції, щоб використовувати винятки, ви все одно можете використовувати механізм виключення, якщо ви пишете невелику функцію помічника, наприклад

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Тоді ваш код міг би читати так:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

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

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Тоді ви можете написати свій код як

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Перевірка значень рефакторингу на винятки - це не обов'язково хороший шлях, але є значні викривлення, що розгортаються.
Джон Ву

4
-1 Використання винятків для нормального потоку на C ++, як це, є поганою практикою програмування. У C ++ винятки мають бути зарезервовані для виняткових обставин.
Джек Едлі

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

1
Ні. По-перше, були створені винятки, що дозволяють поширювати помилки , а не обробляти помилки ; по-друге, "Функції ExecuteStepX повинні бути виконані, якщо і тільки в тому випадку, якщо попередній успіх." не означає, що булева помилка, повернена попередньою функцією, вказує на явно винятковий / помилковий випадок. Таким чином, ваше твердження не є послідовним . Поводження з помилками та дезіннізація потоку можуть бути реалізовані багатьма різними способами, винятки - це інструмент, що дозволяє розповсюджувати помилки та виконувати помилки на місці та виконувати це.

6

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

StepA() && StepB() && StepC() && StepD();
DoAlways();

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


Насправді я відредагував своє запитання, щоб краще пояснити тему, але його відхилили, щоб не визнати недійсною більшість відповідей. : \
ABCplus

Я новий користувач у програмі SO та новачок-програміст. Тоді 2 питання: чи існує ризик, що інше запитання, як те, що ви сказали, буде позначене як дубльоване через ЦЕ питання? Ще один момент: як міг користувач / програміст для початківців вибрати найкращу відповідь між усіма там (я думаю, майже добре ...)?
ABCplus

6

Для C ++ 11 і вище, приємним підходом може бути реалізація системи виходу з діапазону, аналогічної механізму D (виходу) D.

Один з можливих способів його реалізації - використання C ++ 11 лямбда та деяких допоміжних макросів:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

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

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Макроси справді просто прикраса. MakeScopeExit()можна використовувати безпосередньо.


Не потрібно макросів, щоб зробити цю роботу. І, [=]як правило, неправильно для лямбда, що охоплюється.
Якк - Адам Невраумон

Так, макроси просто для прикраси і їх можна викинути. Але чи не сказати б ви, що цінність - це найбезпечніший "загальний" підхід?
glampert

1
ні: якщо ваша лямбда не буде зберігатися за межами поточного обсягу, де створена лямбда, використовуйте [&]: це безпечно і мінімально дивно. Захоплення за значенням лише тоді, коли лямбда (або копії) могли вижити довше, ніж сфера дії в точці декларації ...
Якк - Адам Невраумон

Так, це має сенс. Я його зміню. Дякую!
гламперт

6

Чому ніхто не дає найпростішого рішення? : D

Якщо всі ваші функції мають однаковий підпис, ви можете зробити це так (для мови C):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Для чистого рішення C ++ слід створити клас інтерфейсу, який містить метод виконання та обертає ваші кроки в об'єкти.
Тоді рішення вище буде виглядати так:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();

5

Припустимо, що вам не потрібні індивідуальні змінні умови, інвертуючи тести та використовуючи else-falthrough, оскільки шлях "нормально" дозволить вам отримати більш вертикальний набір операторів if / else:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

Опускання невдалої змінної робить код трохи неясним IMO.

Оголошення змінних всередині добре, не хвилюйтеся про = vs ==.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Це незрозуміло, але компактно:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();

5

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

У Java це виглядатиме приблизно так:

interface StepState{
public StepState performStep();
}

Реалізація працює так:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

І так далі. Тоді ви можете замінити великий, якщо умова:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();

5

Як згадував Rommik, ви можете застосувати для цього шаблон дизайну, але я б скористався шаблоном Decorator, а не стратегією, оскільки ви хочете здійснювати ланцюжки дзвінків. Якщо код простий, я б пішов з однією з добре структурованих відповідей, щоб запобігти введенню. Однак якщо він складний або вимагає динамічного ланцюжка, то візерунок «Декоратор» - хороший вибір. Ось діаграма класів yUML :

діаграма класів yUML

Ось зразок програми LinqPad C #:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

Найкраща книга, яку можна прочитати про шаблони дизайну, ІМХО, - це шаблони дизайну Head First .


Яка користь від цього у чомусь на зразок відповіді Джеффрі?
Дейсон

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

4

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

Поширеною схемою було використання do { } while (false);

Я використовував макрос для, while(false)щоб зробити це do { } once;Загальним шаблоном було:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

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


4

Щоб вдосконалити відповідь Матьє на C ++ 11 і уникнути витрат на виконання, пов'язаних з використанням std::function, я б запропонував використовувати наступне

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Цей простий клас шаблонів приймає будь-який функтор, який можна викликати без будь-яких параметрів, і робить це без будь-яких динамічних виділень пам'яті, і тому краще відповідає цілі абстракції C ++ без зайвих накладних витрат. Додатковий шаблон функції є для спрощення використання параметри шаблону виведення (який недоступний для параметрів шаблону класу)

Приклад використання:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Так само, як відповідь Матьє, це рішення є повністю безпечним винятком, і executeThisFunctionInAnyCaseйого вимагатимуть у всіх випадках. Якщо executeThisFunctionInAnyCaseкинути себе, деструктори неявно позначаються, noexceptі тому заклик до std::terminateвидається замість того, щоб викликати виключення під час розмотування стека.


+1 Я шукав цю відповідь, тому мені не доведеться її публікувати. Ви повинні вдосконалитись вперед functorв deferred'd конструкторі, не потрібно змушувати a move.
Якк - Адам Невраумон

@Yakk змінив конструктор на конструктор-переадресатор
Джо

3

Схоже, ви хочете робити всі дзвінки з одного блоку. Як запропонували інші, вам слід використовувати або whileцикл, і залишити за допомогою, breakабо нову функцію, яку ви можете залишити за допомогою return(може бути чистішою).

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

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

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();

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

Я не розумію, що ти маєш на увазі, ні чому я не міг скористатися однокроковим?
AxFab

3

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

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

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

А потім у своєму коді просто потрібно зателефонувати:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Я думаю, що найкраще рішення, якщо кожен виклик ConditionX щось ініціалізує, виділяє пам'ять тощо. Найкраще, щоб бути впевненим, що все буде очищено.


3

цікавий спосіб - це робота з винятками.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

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

Порада: обговоріть ці випадки з досвідченим розробником, якому ви довіряєте ;-)


Я думаю, що ця ідея не може замінити будь-яку мережу. У будь-якому випадку, у багатьох випадках це дуже хороший підхід!
WoIIe

3

Інший підхід - do - whileцикл, хоча він згадувався раніше, не було його прикладу, який би показував, як він виглядає:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(Добре, що вже є відповідь whileциклом, але do - whileцикл не надто перевіряє справжність (на початку), а натомість у кінці xD (це, однак, можна пропустити).


Гей Заффі - у цій відповіді є масивне пояснення підходу do {} while (false). stackoverflow.com/a/24588605/294884 Дві інші відповіді також згадували про це.
Fattie

Це захоплююче питання, чи більш елегантно використовувати ПРОДУКЦІЮ або BREAK у цій ситуації!
Fattie

Привіт @JoeBlow Я бачив усі відповіді ... просто хотів показати, а не говорити про це :)
Zaffy

Першу свою відповідь тут я сказав: "Цього ніхто не згадав ...", і тут же хтось люб'язно зазначив, що це 2-а найкраща відповідь :)
Fattie

@JoeBlow Eh, ти маєш рацію. Я спробую це виправити. Я відчуваю, що ... xD Дякую, все одно, наступного разу я відвідаю трохи більше :)
Zaffy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.