Каковы некоторые лучшие способы избежать do-while (0); взломать в C ++?

233

Когда поток кода выглядит так:

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

Я обычно видел эту работу, чтобы избежать беспорядочного потока кода:

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

Какие есть лучшие способы избежать этого обходного пути, чтобы он стал кодом более высокого уровня (отраслевого уровня)?

Любые предложения, которые из коробки приветствуются!

Sankalp
источник
38
RAII и бросать исключения.
ta.speot.is
135
Для меня это звучит как хорошее время для использования goto- но я уверен, что кто-то отметит меня за то, что я предложил это, поэтому я не пишу ответ на этот счет. Иметь длинное do ... while(0);кажется неправильным.
Матс Петерссон
42
@dasblinkenlight: Да, действительно. Если вы собираетесь использовать goto, будьте честны с этим и делайте это открыто, не скрывайте это с помощью breakиdo ... while
Матс Петерссон
44
@MatsPetersson: Сделайте ответ, я дам вам +1 за ваши gotoусилия по реабилитации. :)
wilx
27
@ ta.speot.is: "RAII и выбрасывать исключения". Делая это, вы будете эмулировать управление потоком с исключениями. То есть это похоже на использование дорогого, новейшего оборудования в качестве молотка или пресс-папье. Вы можете сделать это, но это определенно выглядит как очень плохой вкус для меня.
SigTerm

Ответы:

310

Приемлемой практикой считается выделение этих решений в функции и использование 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- это никогда не правильный ответ. Я был бы признателен, если бы был комментарий ...
Матс Петерссон
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), только без болезненного удара по его читабельности.

dasblinkenlight
источник
30
Задания внутри ifусловий выглядят очень подозрительно.
Михаил
90
@Mikhail Только неподготовленным глазом.
dasblinkenlight
20
Я использовал эту технику и раньше, и мне всегда было неприятно, что я полагал, что компилятору придется генерировать код для проверки каждого из них, ifнезависимо от того, что goOnбыло, даже если ранний отказал (в отличие от прыжка / вырыва) ... но я только что провел тест, и VS2012, по крайней мере, был достаточно умен, чтобы в любом случае замкнуть все после первой ошибки. Я буду использовать это чаще. примечание: если вы используете, goOn &= checkN()то checkN()всегда будет работать, даже если goOnбыл falseв начале if(то есть, не делайте этого).
отметка
11
@Nawaz: Если у вас есть неопытный ум, делающий произвольные изменения по всей кодовой базе, у вас есть гораздо большая проблема, чем просто назначения внутри ifs.
Idelic
6
@sisharp Elegance так в глазах смотрящего! Я не понимаю, как в мире неправильное использование циклической конструкции может восприниматься как «элегантное», но, возможно, это только я.
dasblinkenlight
38
  1. Попробуйте извлечь код в отдельную функцию (или, возможно, более чем одну). Затем вернитесь из функции, если проверка не пройдена.

  2. Если это слишком тесно связано с окружающим кодом, чтобы сделать это, и вы не можете найти способ уменьшить связь, посмотрите код после этого блока. Предположительно, он очищает некоторые ресурсы, используемые функцией. Попробуйте управлять этими ресурсами, используя объект RAII ; затем замените каждую хитрость breakна return(или throw, если это более уместно) и дайте деструктору объекта убрать за вас.

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

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

Майк Сеймур
источник
4
Я смиренно утверждаю, что RAII, хотя и полезен, не является серебряной пулей. Когда вы обнаружите, что собираетесь написать класс convert-goto-to-RAII, который больше не используется, я определенно думаю, что вам будет лучше, если вы воспользуетесь идиомой «goto-end-of-the-world», о которой уже упоминали.
idoby
1
@busy_wait: Действительно, RAII не может решить все; вот почему мой ответ не останавливается на втором пункте, но продолжает предлагать, gotoесли это действительно лучший вариант.
Майк Сеймур
3
Я согласен, но я думаю, что писать классы преобразования goto-RAII - плохая идея, и я думаю, что это должно быть указано явно.
idoby
@idoby как насчет написания одного класса шаблонов RAII и создания его с какой-либо одноразовой очисткой, которая вам нужна в
лямбде
@ Калет звучит для меня так, будто ты изобретаешь ctors / dtors?
idoby
37

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

Длинный ответ:

В C лучшая практика для этого вида кода заключается в добавлении в код метки EXIT / CLEANUP / other , где происходит очистка локальных ресурсов и возвращается код ошибки (если есть). Это лучшая практика, потому что он естественным образом разделяет код на инициализацию, вычисление, фиксацию и возврат:

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, ни метку очистки, ни что-нибудь еще).

utnapistim
источник
+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;
    ...
    ...
}

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

Конструкция также достаточно проста, и вы можете надеяться, что компилятор ее оптимизирует.

Как подсказывает @jamesdlin, вы можете даже скрыть это за макросом вроде

#define BLOC switch(0) case 0:

И используйте это как

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

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

Kriss
источник
2
Ох, умный. Вы можете даже спрятать его за макросом define BLOCK switch (0) case 0:и использовать его как BLOCK { ... break; }.
Джеймсдлин
@jamesdlin: мне никогда не приходило в голову, что это может быть полезно, если поставить случай перед открывающей скобкой переключателя. Но это действительно разрешено C, и в этом случае удобно написать хороший макрос.
Крисс
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, чтобы вы могли вернуться вместо перехода к конечному разделу функции.

stefaanv
источник
11

Если вам не нужно вводить локальные переменные во время выполнения, вы часто можете сгладить это:

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

// edit 
doThingsAtEndAndReportErrorStatus()
the_mandrill
источник
2
Но тогда каждое условие должно включать предыдущее, которое не только уродливо, но и потенциально плохо сказывается на производительности. Лучше сразу перепрыгнуть через эти проверки и очистить, как только мы узнаем, что мы «не в порядке».
TNE
Это правда, однако этот подход имеет свои преимущества, если есть вещи, которые необходимо сделать в конце, поэтому вы не хотите возвращаться раньше (см. Редактирование). Если вы комбинируете с подходом Дениз использовать bools в условиях, то прирост производительности будет незначительным, если только он не находится в очень узком цикле.
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 ++ не предоставляет конструкцию «finally»? ,

#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;
}
Cartucho
источник
10
В самом деле? Во-первых, я, честно говоря, не вижу там никакого улучшения читабельности. НЕ ИСПОЛЬЗУЙТЕ ИСКЛЮЧЕНИЯ ДЛЯ КОНТРОЛЯ ПОТОКА ПРОГРАММЫ. Это НЕ их правильное назначение. Кроме того, исключения приносят значительные потери производительности. Правильным вариантом использования исключения является случай, когда присутствует какое-то условие, которое вы ничего не можете сделать, например, попытка открыть файл, который уже заблокирован исключительно другим процессом, или сбой сетевого подключения, или сбой вызова базы данных или вызывающая сторона передает неверный параметр в процедуру. Что-то в этом роде. Но НЕ используйте исключения для управления потоком программ.
Крейг
3
Я имею в виду, чтобы не быть соплями об этом, а перейти к той статье Страуструпа, на которую вы ссылались, и найти «Для чего не следует использовать исключения?» раздел. Среди прочего, он говорит: «В частности, throw - это не просто альтернативный способ возврата значения из функции (аналогично return). Это будет медленным и запутает большинство программистов на C ++, привыкших видеть исключения, используемые только для ошибок. обработка. Точно так же, throw не является хорошим способом выхода из цикла. "
Крейг
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)

При этом вы можете делать вложенные блоки и указывать, куда вы хотите выйти / выпрыгнуть.

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.

acegs
источник
10
Я бы предпочел gotoрасширить синтаксис языка, используя препроцессор, миллион раз.
Кристиан
2
«Чтобы сделать этот код чище и понятнее, вы можете [использовать LOADS_OF_WEIRD_MACROS]» : не вычисляется.
underscore_d
обратите внимание на первое предложение, что нормаль do{...}while(0) предпочтительнее. Этот ответ является только предложением и игрой на макро / goto / label для разбивки на определенные блоки.
acegs
8

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

В ответ на комментарий, который я получил ниже, я отредактировал свое введение здесь: вы можете найти полную информацию о реализации монад C ++ в различных местах, что позволит вам достичь того, что предлагает Rotsor. Чтобы получить монады, требуется некоторое время, поэтому вместо этого я собираюсь предложить здесь быстрый, похожий на монаду механизм «бедняков», для которого вам нужно знать не что иное, как boost :: option.

Настройте свои шаги вычисления следующим образом:

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
Этот код имеет нежелательное свойство заставлять каждую из отдельных функций обрабатывать сбой предыдущей функции, таким образом неправильно используя идиому Monad (где монадические эффекты, в этом случае сбой, должны обрабатываться неявно). Для этого вам нужно optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);вместо этого использовать монадическую композиционную операцию ('bind'), а не приложение-функцию.
Ротсор
Спасибо. Вы правы - это способ сделать это правильно. Я не хотел писать слишком много в своем ответе, чтобы объяснить это (отсюда и термин «бедняки», который должен был означать, что я не собираюсь делать это целиком).
Бенедикт
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;
   }
}
karx11erx
источник
Это хорошо, когда это возможно, но гораздо менее общий, чем вопрос, заданный здесь. Каждое условие может зависеть от кода, выполненного после последнего теста удаления кода.
Крис
Проблема здесь в том, что вы разделяете причину и следствие. Т.е. вы отделяете код, который ссылается на тот же номер условия, и это может быть источником дальнейших ошибок.
Рига
@kriss: Ну, функция ConditionCode () может быть настроена, чтобы позаботиться об этом. Ключевым моментом этой функции является то, что вы можете использовать return <result> для чистого выхода, как только будет вычислено конечное условие; И это то, что обеспечивает структурную ясность здесь.
karx11erx
@Riga: Imo это полностью академические возражения. Я считаю, что C ++ становится все более сложным, загадочным и нечитаемым с каждой новой версией. Я не вижу проблемы с маленькой вспомогательной функцией, которая оценивает сложные условия в хорошо структурированном виде, чтобы сделать целевую функцию прямо внизу более читабельной.
karx11erx
1
@ karx11erx мои возражения практичны и основаны на моем опыте. Этот шаблон плохо связан с C ++ 11 или любым другим языком. Если у вас есть трудности с языковыми конструкциями, которые позволяют писать хорошую архитектуру, это не является языковой проблемой.
Рига,
5

Что-то вроде этого возможно

#define EVER ;;

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

или используйте исключения

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

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

liftarn
источник
10
Пожалуйста, не делайте умных вещей, таких как ваше определение, они обычно затрудняют чтение кода для других разработчиков. Я видел, как кто-то определял Case как break; case в заголовочном файле и использовал его в переключателе в файле cpp, заставляя других часами задумываться, почему переключается между операторами Case. Grrr ...
Майкл
5
И когда вы называете макросы, вы должны сделать их похожими на макросы (т. Е. Заглавными буквами). В противном случае кто-то, кто случайно назовет переменную / function / type / etc. названный everбудет очень несчастным ...
Джеймсдлин
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. Читателю дают подсказку, что ему нужно продолжить поиск, что приведет к тому, что B будет правдой.

Адриан Шум
источник
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++;
}

Как насчет этого?

sniperbat
источник
если устарела, просто 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 ++ , поэтому я не буду писать здесь никакого кода, но пока никто не упомянул объектно-ориентированное решение. Итак, вот мое предположение об этом:

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

Хорошо, что такой дизайн очень хорошо придерживается принципа открытия / закрытия , потому что вы можете легко добавлять новые условия при инициализации объекта, содержащего рассматриваемый метод. Вы даже можете добавить второй метод в интерфейс с методом оценки условия, возвращая описание условия. Это может быть использовано для систем самодокументирования.

Недостатком, однако, является то, что из-за использования большего количества объектов и итерации по списку происходит немного больше накладных расходов.

SpaceTrucker
источник
Не могли бы вы добавить пример на другом языке? Я думаю, что этот вопрос относится ко многим языкам, хотя он был задан специально о C ++.
Дениз Скидмор
1

Это способ, которым я делаю это.

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

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

  if (!check()) return;
  ...
  ...
}
Vadoff
источник
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;
}

...и так далее. Это легко проверить, так как он помещает код отмены рядом с кодом do. Легкий одитинг это хорошо. Это также делает поток управления очень четким. Это полезный шаблон для C тоже.

Может потребоваться, чтобы calcфункции принимали множество аргументов, но обычно это не проблема, если у ваших классов / структур хорошая сплоченность. (То есть вещи, которые принадлежат друг другу, живут в одном объекте, поэтому эти функции могут брать указатели или ссылки на небольшое количество объектов и при этом выполнять много полезной работы.)

Nemo
источник
Очень легко проверить путь очистки, но, возможно, не так просто отследить золотой путь. Но в целом, я думаю, что-то подобное способствует последовательной очистке.
Дениз Скидмор
0

Если в вашем коде есть длинный блок операторов if..else if..else, вы можете попробовать переписать весь блок с помощью Functorsили function pointers. Возможно, это не всегда правильное решение, но довольно часто.

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

HAL
источник
В принципе это возможно (и неплохо), но с явными объектами функций или -pointers это просто слишком сильно нарушает поток кода. OTOH, здесь эквивалентное, использование лямбда-выражений или обычных именованных функций является хорошей практикой, эффективным и хорошо читается.
оставлено около
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;
}
...
...

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

Я думал, что это решение стоит упомянуть, поэтому предоставил его здесь.

Sankalp
источник
0

Объедините это в одно ifутверждение:

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

Это шаблон, используемый в таких языках, как Java, где ключевое слово goto было удалено.

Tyzoid
источник
2
Это работает, только если вам не нужно ничего делать между тестами условий. (ну, я полагаю, вы могли бы скрыть выполнение вещи внутри некоторых вызовов функций, выполняемых условными тестами, но это может немного запутать, если вы сделаете это слишком много)
Джереми Фризнер,
@JeremyFriesner На самом деле, вы могли бы делать промежуточные вещи как отдельные логические функции, которые всегда оцениваются как true. Оценка короткого замыкания гарантирует, что вы никогда не запустите промежуточный материал, для которого не прошли все предварительные тесты.
AJMansfield
@ AJMansfield: да, это то, о чем я говорил во втором предложении ... но я не уверен, что это улучшит качество кода.
Джереми Фризнер,
@JeremyFriesner Ничто не мешает вам написать условия, так как (/*do other stuff*/, /*next condition*/)вы можете даже красиво отформатировать их. Просто не ожидайте, что людям это понравится. Но, честно говоря, это говорит о том, что для Java было ошибкой goto
свернуть
@JeremyFriesner Я предполагал, что это были логические значения. Если функции должны выполняться внутри каждого условия, то есть лучший способ справиться с этим.
Tyzoid
0

Если для всех ошибок используется один и тот же обработчик ошибок, и каждый шаг возвращает логическое значение, указывающее успех:

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

(Аналогично ответу tyzoid, но условия - это действия, а && предотвращает дополнительные действия после первого сбоя.)

Дениз Скидмор
источник
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
}
Fennekin
источник