Pull to refresh

Comments 108

Зло не goto. Зло - что с ним будет после десяти рефакторингов, минимум один из которых делал человек, который не знает, что такое goto.

Помнится, у IBM в DB2 API есть такие массивы нулевой длины, так что это универсальное решение.

В Windows API такой трюк тоже неоднократно используется.

меня учили делать так:

#include <winhttp.h>
struct t_res
{
  HINTERNET internet = NULL;
  HINTERNET connect = NULL;
  HINTERNET request = NULL;
  bool OK;
}
t_res try()
{
  t_res res;
  res.internet = WinHttpOpen(<...>);
  if (!res.internet){
    res.OK = false;
    return res;
  }
  <...>
  res.connect = WinHttpConnect(<...>);
  if(!res.connect){
    res.OK = false;
    return res;
  }
  <...>
  res.request = WinHttpOpenRequest<...>);	
  if(!res.request){
    res.OK = false;
    return res;
  }
  <...>
  res.OK = true;
  return res;
}
bool SendRequest(<...>){

  t_res result = try();
  if(result.internet){
  WinHttpCloseHandle(result.internet);
  }
  if(result.internet){
  WinHttpCloseHandle(result.connect);
  }
  if(result.internet){
  WinHttpCloseHandle(result.request);
  }

  return result.OK;
}

И код лучше, и разделение ответственности.

Множество точек возврата - тоже антипаттерн.

я рефакторил goto, и сделал лучше.

Множество точек возврата приемлемо, когда нет исключений.

Это не лучше.

На мой взгляд хуже, потому что внимание размывается на 3 сущности (структура и 2 функции), нужно их модифицировать синхронно. Когда такой паттерн приходится применять часто, возникнет зоопарк одноразовых структур t_res и одноразовых функций try, что загрязняет пространство имён (в IDE попробуйте перейти к функции или типу).

Решение автора мне тоже не очень. Оно, хотя и "защищено" от ошибок проверками в конце (кстати, найдите там 2 опечатки, вызванные копипастой), порождает лишний код, что не C-style.

Мой вариант

bool SendRequest(<...>){
  bool result = false;
  HINTERNET internet;
  HINTERNET connect;
  HINTERNET request;

  internet = WinHttpOpen(<...>);
  if (!internet) goto RET;
  <...>
  connect = WinHttpConnect(<...>);
  if(!connect) goto CLOSE_INTERNET;
  <...>
  request = WinHttpOpenRequest<...>);	
  if(!request) goto CLOSE_CONNECT;
  <...>
  result = true;

  WinHttpCloseHandle(request);
CLOSE_CONNECT:
  WinHttpCloseHandle(connect);
CLOSE_INTERNET:
  WinHttpCloseHandle(internet);
RET:
  return result;
}

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

А проще?

#include <winhttp.h>

bool SendRequest(<...>){
HINTERNET internet=NULL, connect=NULL, request=NULL;
bool result = false;
internet = WinHttpOpen(<...>);
if (internet) {
connect = WinHttpConnect(<...>); }
if(connect) {
request = WinHttpOpenRequest<...>);}
if(request){
<...> <---Какие-то промежуточные действия
WinHttpCloseHandle(request);
result = true;
}
<...>

if(connect) {
WinHttpCloseHandle(connect);}
if(internet){
WinHttpCloseHandle(internet);}
return result;
}

request будет NULL, пока internet и connect будут NULL. И не надо ничего лишнего.

Тоже хорошо, но блок "Какие-то промежуточные действия" может быть довольно объёмным, а мы его включаем внутрь if, добавляя уровень вложенности.
Вроде как, это было проблемой, от которой хотели уйти. А так-то можно нарисовать матрёшку if-ов, без всяких goto.

Тогда этот блок лучше всего сделать отдельной функцией, как уже кем-то предлагалось.

Но если от матрёшки ифов оставить только один, как в моём варианте, тогда, я думаю, это будет наименьшее зло. Вызов функции занимает и больше времени, и стек. А метки для goto можно потом куда-то не туда сдвинуть.

А самым правильным подходом, на мой взгляд, является помещение всех проверок внутри вызываемых библиотечных функций. Если по ошибке в качестве параметра ей передали NULL, пусть сделает return false или return NULL, и дело с концом.

самым правильным подходом, на мой взгляд, является помещение всех проверок внутри вызываемых библиотечных функций. Если по ошибке в качестве параметра ей передали NULL, пусть сделает return false или return NULL

Бессмысленный расход процессорного времени, чтобы зайти во все функции "матрёшки", когда уже первая вернула ошибку.

Равно, как и

if(connect) {
     WinHttpCloseHandle(connect);

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

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

Это да. Прежде всего потому, что перед выходом могут потребоваться некоторые обязательные действия по освобождению ресурсов (например) или откату каких-то действия в случае неудачи.

Но, увы, в С/С++ нет удобного механизма реализации единой точки выхода. Об этом написал ниже.

Есть.

Ты заворачиваешь функцию, которая требует единой точки выхода, в другую функцию и обрабатываешь единый выход там.

если нужно освобождать ресурсы, то занимаем их тоже во внешней функции.

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

Тут такое дело. Излишняя глубина вызовов – тоже антипаттерн.

ну так предложи вариант, что?

Для языка Си – нормальный вариант с goto. Один вход и один выход, как доктор прописал.

А варианте с return кто-нибудь рано или поздно полезет дописывать финализацию в конце текста, а потом ещё через неопределённое время это заключит при нештатном возврате.

C goto ты не сможешь отвязать логику от ресурсов.

Если для тебя важно иметь общий механизм входа/выхода это значит, что у тебя на самом деле 2 сущности: логика и общий вход/выход. И они должны быть разделяемыми. В идеале - ничего не должно помешать тебе разнести их в разные единицы компиляции. Goto это не даст.

Общий вход/выход – это и есть часть логики. Просто логики передачи управления в программе, как стрелочек в блок-схеме. Ресурсы тут вообще не причём.

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

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

Пытаться все это реализовать на деструкторах часто приводит к излишне сложной структуре классов, причем, классов без переиспользования, "одноразовых".

Понятно, что всегда можно изобрести какой-то workaround, но, поверьте, если бы на уровне языка была бы возможность объявления точки выхода из функции все было бы намного проще и гибче.

Пытаться все это реализовать на деструкторах часто приводит к излишне сложной структуре классов, причем, классов без переиспользования, "одноразовых".

Скажите, а вы о выходе C++11 с лямбдами что-нибудь слышали? Ну хотя бы в общих чертах?

Не очень простой механизм.

Во-первых, это лишний уровень стека

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

За неимением горничной будем иметь дворника (с).

Все-таки лучше, когда это реализовано на уровне компилятора, который сам генерирует блок _QRNI_ON_EXIT_<proc_name>.

Причем, опционально, on-exit еще может быть снабжен "индикатором ошибки" on-exit wasError; который "взводится" в том случае, когда в блок on-exit влетели в случае необработанного системного исключения

dcl-proc myproc;
   dcl-s isAbnormalReturn ind;
   ...
   p = %alloc(100);
   price = total_cost / num_orders; 
   filename = crtTempFile();
   return;  

on-exit isAbnormalReturn;   
   dealloc(n) p;

   if filename <> blanks;
      dltTempFile (filename);
   endif;

   if isAbnormalReturn;
      reportProblem ();
   endif;
end-proc;

Если в строке 5 будет деление на 0, сразу влетаем в on-exit со взведенным isAbnormalReturn

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

в вашем варианте разнести открытие/закрытие ресурсов и работу с ними в разные единици компиляции не возможно. Это плохо.

Но, увы, в С/С++ нет удобного механизма реализации единой точки выхода.

Языка C/C++ не существует.

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

Во-первых, в С нет деструкторов. Во-вторых, далеко не все имеет смысл оборачивать в классы и объекты (это тоже накладные расходы). В-третьих, чтобы все это работало, требуется куча плясок с бубном типа автоматического вызова деструктора при выходе объекта из области видимости, порядка вызова деструкторов для набора объектов и т.п.

Там на самом деле много проблем и много ситуаций, которые могут вызывать UB. Просто потому, что все это происходит неявно и разработчиком слабо контролируется. В отличии от явно прописываемой единой точки выхода где все видно в явном виде.

Во-первых, в С нет деструкторов

Во-первых, я ничего не говорил про C.

Языка C/C++ не существует. Если вы рассуждаете сразу о C/C++, то, вероятно, толком не знаете ни того, ни другого.

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

Матчасть подтяните. Есть ощущение, что отстали лет на 20.

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

while(cur_struct){
  if(cur_struct->sign){
    DoSmth();
    if(cur_struct-address){
      DoAnother();
      if(cur_struct->flag)
            AnotherFunc();
    }
  }
  cur_struct = cur_struct->next;
}

Если отбросить подобные мне непонятные правила, то фрагмент превращается в более читаемый:

for(cur_struct = root; cur_struct; cur_struct = cur_struct->next){
  if(!cur_struct->sign)
    continue;
  DoSmth();
  if(!cur_struct->address)
    continue;
   <…>
}

Кстати, такой же пример я приводил под роликом, где человек призывал отказаться от циклов `for` из-за того, что это просто обёртка над `while`

Уверены что у вас здесь с отступами все нормально:

while(cur_struct){
  if(cur_struct->sign)
    DoSmth();
      if(cur_struct-address)
        DoAnother();
          if(cur_struct->flag)
            AnotherFunc();
  cur_struct = cur_struct->next;
}

?

for(cur_struct = root; cur_struct; cur_struct = cur_struct->next){
  if(!cur_struct->sign)
    continue;
  DoSmth();
  if(!cur_struct->address)
    continue;
   <…>
}

Имею "счастье" регулярно заглядывать в код, написанный в таком стиле. Только там еще в одном цикле кроме continue еще и break-и могут быть. А могут быть и вот такие вот "перлы":

bool does_contain_apropriate_item(
   const item_container & items,
   const search_criteria & search_params)
{
   for(const auto & i : items) {
      if(!does_meet_coditions(i, search_params)) {
         continue;
      }

      return true;
   }

   return false;
}

Ничего кроме незлых тихих слов такая любовь к continue не вызывает. А код с continue вот просто проситься быть переписанным нормальным способом.

Отступы я поправил, но я не понимаю, чем вам не понравился вариант с `for`. Есть список указателей на структуры, есть порядок строгий порядок условий, которые нужно проверить, и действий, которые нужно выполнить. Не подходит по сигнатуре? Переходим к следующей. Флаг говорит о занятости? Переходим к следующей. И так далее

Отступы я поправил, но я не понимаю, чем вам не понравился вариант с for

Так ведь дело не в for. Дело в continue.
Ваш for и без continue можно переписать:

for(cur_struct = root; cur_struct; cur_struct = cur_struct->next){
  if(cur_struct->sign) {
    DoSmth();
    if(cur_struct->address) {
      DoAnother();
      if(cur_struct->flag) {
        AnotherFunc();
      }
    }
  }
}

И здесь "лесенка вправо" прямо говорит о том, что последующие действия (т.е. DoSmth, DoAnother, AnotherFunc) выполняются только при срабатывании предшествующих условий. При этом в сами условия без необходимости можно не вглядываться.

Тогда как continue разбивает нормальную логику и глядя на тело цикла нужно напрягаться чтобы понять, когда мы дойдем до AnotherFunc и как это все будет связано с предшествующими действиями.

В общем, если вам придется приводить в чувства чужой (т.е. написанный не вами) код, да еще и в предметной области, с которой вы мало знакомы, то continue в циклах доставят сам незабываемых эмоций.

Я понял вас, но мне кажется, что тут уже пошла вкусовщина и дело привычки. Я не очень люблю большую вложенность и мне как-то проще воспринимать вариант с continue. Но согласен с вами, злоупотребление может доставить кучу проблем другому человеку. Хотя в конечном счёте опираться в выборе стоит на устоявшиеся в компании нормы. Опять же, если они адекватные

Я тут с вами поделился своей болью своим опытом. Когда пишешь код сам, то кажется, что continue -- это норм, ничего сложного. Когда въезжаешь в чужой, то оказывается, что все рекомендации про единую точку выхода, про отсутствие в теле цикла continue/break, про нежелательность goto -- это все как устав, написанный кровью :(
В том числе и твоей собственной ;)

Почему бы в структуре сразу не прописать res.OK = false? И только в случае, когда всё хорошо, в конце туда присвоить true. Ещё код сократит немного.

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

Можно еще написать какой-нибудь класс-обертку, который в деструкторе сам освободит ресурсы, которые успели открыть до возникновения ошибки. Или пользоваться какими-нибудь умными указателями с deleter, в котором ресурсы освобождаются.

А тут уже спорно.

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

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

Если пишешь на современном C++, как-то уже давно считается хорошим тоном знать, что такое RAII. И уж тем более, что такое деструктор, и когда он вызывается.

Деструктор вызовется сам, как ни крути. А вот про явный вызов можно и забыть.

Закладывать сложную логику в деструктор/конструктор во многих coding conventions заприщено. И дело не только в том, что команда не ожидает, что все её участники отлично чувствуют, а не только знают про RAII. Дело в сложностях, которые возникают при трассировке, профилировании и статическом анализе такого кода. Отсутствие явного вызова части логики, а не только технической части, это, как ни крути, плохо. Код из прямого, легко читаемого превращается в кашу.

Те же притензии к калбэкам, исключениям, и сигналам.

Видел, как люди обходят это делая метод класса, который вызывают из деструктора. Им и их лиду так казалось проще.

Соглашусь, всё хорошо в меру.

Один знакомый, пока работал в Facebook, рассказывал, что в их группе было запрещено в коде кидать исключения, можно было только возвращать код ошибки. Видимо, в большой команде очень сложно писать и поддерживать код, в котором кто-то глубоко изнутри может кинуть исключение, и все вызывающие должны это иметь ввиду. Особенно если есть текучка кадров.

Тему с goto уже давно переживали и съели. Есть прекраснаяальтернатива позволяющая фильтровать ветвления и с возможностью выполнить обязательный код

bool SendRequest(<...>){
  bool result = false;
  HINTERNET internet = NULL;
  HINTERNET connect = NULL;
  HINTERNET request = NULL;
  
  do {
    internet = WinHttpOpen(<...>);
    if (!internet){
      break;
    }
    <...>
    connect = WinHttpConnect(<...>);
    if(!connect){
      break;
    }
    <...>
    request = WinHttpOpenRequest<...>);	
    if(!request){
      break;
    }
    <...>
    result = true;
  } while(false)

  if(internet){
    WinHttpCloseHandle(internet);
  }
  if(internet){
    WinHttpCloseHandle(connect);
  }
  if(internet){
    WinHttpCloseHandle(request);
  }

  return result;
}

Я обычно пишу:

bool function(Arg ... arg){
  bool result{};
  do {
    <...>
    if(cond) break;
    <...>
    if(cond) break;
    <...>
    if(cond) break;
    <...>
    result = true; // например
  } while(false);
  if(!result) {
    <...>
  }
  return result; 
}

ПыСы: товарищь сверху, оказывается, уже предложил. Коменты не дочитал)))

подскажите, пожалуйста, в чём массив нулевой длинны лучше чем
std::vector<uint8_t> raw_data; ?

Представьте, что вы получаете эти данные откуда-то по каналам связи. Например, через UDP порт. Или через pipe. Или через очередь. Или передаете в канал связи.

Как вы туда запихнете это самый std::vector? А если там на другой стороне что-то, что вообще на другом языке написано и не знает что такое std:vector?

Массив нулевой длины - это просто байтовый буфер. Который вы неким образом интерпретируете наложением на заданную структуру. Без привязки к конкретному языку и реализации. Внутри своей программы вы можете использовать что удобно. Но как только вам требуется обеспечить связь с внешним миром, вам необходимо думать о том, чтобы ваши данные были понятны всем. Без привязки к конкретным языкам и библиотекам.

Для решения этой проблемы в современном мире принято использовать протоколы.

Json, например. Но, да, пока ты работаешь с каналом связи напрямую, ты получаешь не данные, а байты. А для работы с байтами принято использовать сырые указатели или массивы нулевой длины.

Любой протокол тащит за собой накладные расходы. Иногда это допустимо и приемлемо, иногда нет. Попробуйте реализовать json на каком-нибудь ATMega или STM32 - в самом лучшем случае вам просто придется покупать более дорогие чипы с большим количеством ресурсов там, где запросто можно обойтись более дешевыми.

А если количество "изделий" исчисляется тысячами, а бюджет на компоненты и з/п разработчику один - получите выбор. Или делать эффективно по ресурсам и получить прибавку к з/п из сэкономленных на компонентах денег, или сделать как проще разработчику за меньшую з/п.

Пробовал. На меге 48. Проклял всё!

Согласен с вами на все 100.

Я достаточно долго работал с обработкой информации от контроллеров на однокристаллаках. И мега и стм - это еще цветочки. Начинали мы еще в 90-х, вообще с 8080 - тут в принципе ничего сложного не поместится.

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

Я больше скажу - сейчас работаю с сервером на 120 8-поточных ядер Power9 и 12Тб оперативки - и то json используется только в самых крайних случаях. Когда без него ну совсем никаких (обычно речь идет об обмене данными с внешними системами через очереди). Использование же json внутри сервера - ничем неоправданный расход ресурсов "в никуда".

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

Да. Очень простой. АБС банка из топ-10 с 50+ млн клиентов. Куда уж проще.

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

Что такое модель акторов представляете? Так вот все жто ближе всего к ней. И никакой джейсон там никуда не уперся.

Был случай, когда даже в PC мне оказалось проще написать свой парсер json, который мог приходить из разных источников, чем пользоваться готовыми библиотеками. Одна из них кидала исключения в неочевидные моменты, другая просто возвращала ошибку в духе "не смогла", безо всякого объяснения, чем ей json не нравится, у третьей были ещё какие-то приколы. А когда код исполняется на удалённой машине у клиента, который всё, что может объяснить - "оно не работает" или "оно зависло", самое то с чужими библиотеками разбираться.

Наверное вот тут довольно не плохо описано - The benefits and limitations of flexible array members | Red Hat Developer.

Как уже написали выше это может быть полезно при низкоуровневой реализации различных протоколов или форматов хранения данных. FAM позволяет компактнее разместить данные и повысить их локальность в памяти, уменьшить фрагментацию памяти.

Как и любой инструмент, все это имеет свои плюсы, минусы, граничные условия и требует осознанного использования.

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

Простите, но первое предложение это здравый смысл, а второе демагогия.

Как и любой другой инструмент, goto может как упростить код, так и безнадежно его запутать.

На С, потом С++ писал достаточно долго и много. В С++ в указанном примере можно использовать исключения вместо goto. В С альтернативы, увы нет...

Сейчас волею судеб пишу на другом языке, где goto просто нет. Совсем. Зато есть такая конструкция как "подпрограммы" (subroutines). Как в бейсике. В зоне видимости процедуры и без образования нового уровня стека. Казалось бы древность древняя, но вот позволяет решать такие проблемы

// do something

if not error;
  // do something

  if not error;
    // do something
  else;
    // rollback
    return false;
  endif;
else;
  // rollback
  return false;
endif;

return true;

вместо этого можно написать

// do something

if error;
  exsr srOnError;
endif;

// do something

if error;
  exsr srOnError;
endif;

return true;

begsr srOnError;
  // rollback
  return false;
endsr;

что более читаемо. Сабрутины вообще оказались достаточно удобны как средства структурирования кода.

А сравнительно недавно появилось то, чего всегда отчаянно не хватало в С (да ив С++ тоже) - блок on-exit - единая точка выхода куда всегда попадаешь после return. И тут еще проще

// do something

if error;
  return false;
endif;

// do something

if error;
  return false;
endif;

return true;

on-exit;
  if error;
    // rollback
  endif;

  // cleanup

мимо on-exit никогда не проскочим :-)

Вот такое очень хотел бы иметь в С.

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

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

on-exit на С++ можно эмулировать классом с логикой в деструкторе, которая отработает даже при исключении.

При языковом исключении - да. При системном - нет. Проверено.

И как это поможет если исключение выкинула сама система?

Ну не умеет оно само системные мсключения ловить. Их надо перехватывать системными средствами и оборачивать в языковые. Но это двойная работа и такое себе с точки щрения производительности. Куда проше самому кидать системные исключения и их перезватывать. Причем, если не перехватил тут, оно автоматом перколируется выше по стеку и перехватывается там. Потенциально - в вызывающей программе, которая вообще на другом языке написана может быть.

Какое исключение может выкинуть сама система? Access violation?

У нас очень много какое. Например, попытка работы с заблокированным кем-то объектом.

Или есть такая штука как очереди (писал про них). Так вот там когда читаешь из очереди с таймаутом, очередь пуста, выход по таймауту идет с выбросом системного исключения.

Переполнение может вызвать системное исключение "The target for a numeric operation is too small to hold the result". Выход за границу объекта...

Любая программа (независимо от того, на каком языке она написано) может кинуть системное исключение - послать прерывающее сообщение (со статусом *escape) в очередь сообщений. Которое может быть перехвачено и обработано как на том же уровне стека, так и на более высоком (до тех пор, пока оно не перехвачено и не обработано, он поднимается по стеку вверх).

Имхо, у Вас довольно специфическая область. Под Windows __except(EXCEPTION_EXECUTE_HANDLER) много чего ловит. Выход за пределы массива, например. Или исключения floating point процессора (сейчас, возможно, уже не так актуально).

Потенциально опасные места окружается такими конструкциями, и проблем обычно нет. Например, приходилось когда-то вызовы кодеков Video for Windows окружать, некоторые кидали исключения на ровном месте.

Мне просто приходится думать шире чем рамки одной программы на одном языке.

Любая программа (бинарник) у нас может быть описана внутри другой программы как обычная процедура (с модификатором extpgm) и вызываться, соответственно, как обычная процедура.

И в коде (не глядя в объявление) вы никогда не скажете что это - внутренняя процедура в этом же бинарнике, внешняя процедура из "сервисной программы" (динамическая библиотека) или вообще отдельная программа.

И написано оно может быть не обязательно на С++. Поэтому больше пользуемся системными исключениями.

В RPG (основной язык для реализации работы с БД и бизнес-логики - писал про него тут) работа с системными исключениями мало чем отличается от С++-ного try/catch/throw)

dcl-proc myProc2;
  dcl-pi *n;
    prm1 char(5);
    prm2 packed(15: 0);
  end-pi;

  //делаем что-то...
  if ... // что-то пошло не так - кидаем исключение
    snd-msg *escape %msg('ABC1234': 'MYMSGF'); // throw
  endif;

  return;
end-proc;

dcl-proc myProc1;
  dcl-pi *n;
  end-pi;

  dcl-s prm1 char(5);
  dcl-s prm2 packed(15: 0);

  monitor; // try
    myProc2(prm1: prm2);
  on-excp 'ABC1234'; // catch
    //Обрабатываем выброшенное в myProc2 исключение
  endmon;

  return;
end-proc;

И не важно ка реализована myProc2 - внутри бинарника, в сервисной программе или отдельной программой.

Если myProc2 пишется на С или С++ - вместо snd-msg придется использовать вызов соотв. системного API (QMHSNDPM - там больше всяких параметров, более муторно писать). Но работать будет точно также.

Ваши проблемы понятны. Но, прямо скажем, они вряд ли являются очень распространёнными в среде тех, кто пишет на C/C++.

Так-то модуль, написанный на C++, можно вызывать из программ на Delphi, Расте, Питоне и ещё много чëм. Имхо, возникновение исключений лучше изолировать внутри модуля, чтобы внешним клиентам не приходилось рассчитывать на то, что им придётся ловить исключения, которые внутри вызываемого модуля возникают.

В целом да. Но поскольку на нашей платформе есть удобный механизм генерации исключений через посылку сообщений с очередь программы (PGMQ), то это вполне нормальный способ сообщить о возникшей ошибке. Кроме того, такие сообщения еще автоматически фиксируются в логе задания (joblog) и потом можно проводить какие-то расследования

RNQ0103    Sender copy             99   08.06.24  02:57:04.664410  QRNXIE       QSYS        *STMT    QRNXIE      QSYS        *STMT  
                                     From module . . . . . . . . :   QRNXMSG                                                        
                                     From procedure  . . . . . . :   InqMsg                                                         
                                     Statement . . . . . . . . . :   8                                                              
                                     To module . . . . . . . . . :   QRNXMSG                                                        
                                     To procedure  . . . . . . . :   InqMsg                                                         
                                     Statement . . . . . . . . . :   8                                                              
                                     Message . . . . :   The target for a numeric operation is too small to hold                    
                                       the result (C G D F).                                                                        
                                     Cause . . . . . :   RPG procedure ECLCUSSUBJ in program ALIBB01/ECLCUSSUBJ at                  
                                       statement 000191 performed an arithmetic operation which resulted in a value                 
                                       that is too large to fit in the target.  If this is a numeric expression,                    
                                       the overflow could be the result of the calculation of some intermediate                     
                                       result. Recovery  . . . :   Contact the person responsible for program                       
                                       maintenance to determine the cause of the problem. Possible choices for                      
                                       replying to message . . . . . . . . . . . . . . . :   D -- Obtain RPG                        
                                       formatted dump. S -- Obtain system dump. F -- Obtain full formatted dump. C                  
                                       -- Cancel. G -- Continue processing at *GETIN.  

Сразу видно где именно проблема:

RPG procedure ECLCUSSUBJ in program ALIBB01/ECLCUSSUBJ at
statement 000191 performed an arithmetic operation which resulted in a value that is too large to fit in the target

Естественно, что то, что может быть обработано внутри программного модуля, обрабатывается там. Но иногда надо пробросить что-то наверх. Можно перехватить внутри и вернуть это в виде "структурированной ошибки" (фактически те же данные, что и в исключение бросаются, но в виде структуры), иногда проще бросить исключение.

А поскольку это универсальный системный механизм, то работать будет везде.

Нельзя просто бесконечный цикл использовать, внутри которого делать break при возникновении ошибок? Также иметь несколько булевских переменных, означающих "вот этот ресурс был получен, после выхода из цикла его нужно освободить". Сорри, если ерунду предлагаю, я не силен в чистом C.

Можно. Только непонятно, зачем. Просто потому что слово goto относится к табуированной лексике?

Для меня - да. Не вижу ни одной объективной причины, почему лучше пользоваться goto, чем не пользоваться. Ещё один из способов потенциально выстрелить себе в ногу.

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

Не совсем так. Внутри цикла можно объявлять переменные, которые при выходе из цикла в любом месте сами правильно освободят ресурсы в деструкторе. Если goto всего один, с этим как-то можно жить. А если их два, три, часть из них ведут в конец процедуры, часть возвращают обратно - вот этого лучше избегать.

А то можно ещё через goto выходить из цикла, и даже входить в него. Да даже в оператор switch - синтаксис языка это позволяет. Можно много всякой дичи натворить, и не всегда это бывает специально. Можно при правке кода ошибиться с copy/paste, например. Можно написать прекрасно работающий код с кучей goto, а потом кто-то через несколько лет при модификации этого кода скажет очень много слов, добрых и не очень.

Не я все эти ужасы придумал, и кажется, что вот лично тебя это точно не коснётся. Хорошо, если так, но если писать код так, чтобы goto не понадобился, это само по себе снижает вероятность всяких нехороших сюрпризов.

Конечно, с помощью goto можно наделать много всяких кривых вещей (как и с помощью любого другого оператора, впрочем). Но я здесь говорю о семантике конкретной программы, и с семантической точки зрения нет вообще никакой разницы между break и соответствующим его точке назначения goto.

А так-то я не агитирую входить в цикл через goto.

В конкретной программе проблем может не быть. Хорошо, когда её автор понимает, в каких разумных пределах можно использовать goto.

В Си исключения сравнительно легко эмулируются с помощью функций setjmp/longjmp. Легко пишутся макросы my_try, my_catch, которые позволяют получить структуры кода как в плюсах.

UFO just landed and posted this here

Тоже не понимаю, чего они так взъелись на это goto. Goto - это нативная команда почти любого процессора, простой безусловный переход. Можно выпендриваться в коде как угодно, но компилятор, вероятно, все равно в итоге сведет все к goto. Если заниматься такой фигней, можно еще и от +/- отказаться, свести все к inc/dec в цикле)

Дейкстра однажды в своей статье написал, что спагетти из перекрёстных goto, не образующие вложенной логической структуры, сложно формально интерпретировать. А для народа это преобразовали в лозунг: “четыре ноги – хорошо, две ноги – плохо!”

Я бы сказал, все описанное относится к C, но не к C++.

В C++ есть RAII, который позволяет корректно высвобождать ресурсы без использования goto и вложенных условий.

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

Массив на стеке — отличное решение, когда у нас есть защита от выхода за пределы массива или размер данных точно известен. Во-первых, это очень дешево, во-вторых, даже в C это гарантирует автоматическое высвобождение ресурса. Просто надо следить за размерами массива и использовать исключительно функции, которые принимают на вход размер буффера.

Доступ за пределами объявленной длины массива, это UB в C++. Да, это скорее всего будет работать как ожидается, но технически, это доступ к объекту, для которого не начат лайфтайм. Плюс потенциальные проблемы с выравниванием. Такое как правило всплывает только в коде, который работает с данными которые получены откуда-то извне, или читаются из файла (или пишутся обратно). В этом случае, можно либо использовать отдельный буффер, и копировать из него данные в нормальные структуры данных (попутно проводя валидацию), либо считывать данные частями. Да, это может быть не очень оптимально (добавляется дополнительное копирование, либо увеличивается количество вызовов чтения/записи), однако это гораздо безопаснее и с точки зрения работы с памятью, и с точки зрения валидации входящих данных.

Я очень удивляюсь тому, что где-то сейчас кто-то пишет что-то на C, вместо C++ (если это не старый проект, который уже давным давно написан на C). Отключаем исключения и RTTI и получаем практически 0 оверхеда поверх того, что можно написать на C, при этом имеем очень много удобных и гибких инструментов для упрощения написания кода, и гораздо более надежные инструменты для управления ресурсами.

можно либо использовать отдельный буффер, и копировать из него данные в нормальные структуры данных

Будет же std::start_lifetime_as

Какое-то адское велосипедирование

  1. Чтобы не заниматься goto и не вносить новых рисков, давно придуманы scope guard и во множестве есть готовые реализации. В том числе и в бусте.

  2. Глобальные переменные убивают тестируемость кода, а также потенциальную возможность параллельного выполнения, необходимость в которой всплывёт именно в тот момент, когда ваша глобальная переменная расползётся по всему коду

  3. ну и так далее

Можно, конечно, и зайца научить курить, но зачем? Люди сорок лет решали проблемы, чтобы взять и в 2024 году от рождества Христова взять и снова приняться за goto.

Я не говорю, что подобными вещами стоит заниматься постоянно. Но иногда приходится. Например, некоторые библиотеки из EDK II ждут, что в вашем модуле будет объявлена глобальная переменная gST (поправьте, если неправ).

Но они хотя бы не требуют применения goto? ;)

Лучше не знать, как там всё под капотом работает, если честно. Чего только стоит то, что структуры, которые я в статье описал, общие для всех модулей. Любой модуль может спокойно подменить указатели на функции на собственные

То, что Вы описываете про аппаратуру в UEFI, это типичный паттерн Singleton. А singleton это, можно сказать, почти глобальная переменная. И лучшего способа его сделать в ANSI C, пожалуй, нет.

В 21 веке делают примерно так:

#include <winhttp.h>

bool SendRequest(<...>){
  return chain.create(WinHttpOpen::new)
       .with(<...>)
       .create(WinHttpConnect::new)
       .with(<...>)
       .create(WinHttpOpenRequest::new)
       .with(<...>)
       .process(request -> {
          <...> <---Какие-то промежуточные действия
       })
       .close(handle -> WinHttpCloseHandle(handle));
}

В аду есть отдельный котёл для тех, кто такую форму записи придумал.

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

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

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

Это DSL. К самодельным DSL многие приходят, когда написаны тысячи страниц однотипного кода, и новый код хочется писать только так, чтобы отразить главное, а всю обвязку скрыть.

Понятно, что новый человек, не знакомый с этим фреймворком, ничего сразу не поймёт и будет плеваться. Но если приложение состоит на 90% из таких похожих функций, лучше, чтобы функции были как можно короче и содержали только суть (логику конкретного места, ради чего эта функция существует).

Ну, да, код должен быть легко читаемым. Если вы этого не понимаете, то вам есть ещё куда расти. Только и всего.

Легкость понимания, имхо, все-таки важнее легкости чтения. А код, который легко читается, не обязательно будет легким в понимании (хотя это два очень сильно связанных с друг с другом параметра). Проблема со всякими then, when, or_else, with, process и пр. в том, что слишком многое может быть (может быть, это тоже далеко не всегда так) упрятано под капот. И если хочется понять, а что же вся эта красота скрывает (есть ли там какие-то аллокации, переключения контекста и пр.), то может потребоваться не только прочитать кусок прикладного кода, но и еще больший кусок вспомогательного.

Тут нужно выбирать между лёгкостью чтения случайным человеком с улицы, и тогда выгоднее использовать только стандартные средства, и человеком, который варится в этом много лет, знает все особенности этих or_else как свои пять пальцев, и он увидит только суть, и будет избавлен от лишних строчек, которые надо прочитать.

Примерно как ranges/streams api - поначалу ничего не понятно, но после некоторого порога становится удобным.

По-моему, мы сейчас оба говорим банальности.

Своим комментарием я хотел сказать, что понимаю и поинт @firehacker, и поинт @Foror. А дьявол, как всегда, будет в деталях (продолжаю банальности).

>Легкость понимания, имхо, все-таки важнее легкости чтения

Поэтому я и написал в 21 веке. Я не программирую на С++, но на Java и других подобных ЯП сегодня такой код данность. То, что компилятор не сможет его оптимизировать или то, что у вас нет опыта работы с таким кодом ещё ничего не значит. Компиляторы нужно доделывать, вам нужно повышать квалификацию, потому что программирование идёт дальше. И лаконичность кода сегодня важна. Конечно если только вы не выжимаете наносекунды на критичном участке рантайма.

>а что же вся эта красота скрывает (есть ли там какие-то аллокации, переключения контекста и пр.)

Это касается любого исходного кода. Тот же пример на С++ высокоуровневый и я могу сказать про С++ код тоже самое. Дальше идёт компилятор, потом ассемблер, потом транзисторы и т.д. И на каждом уровне абстракций что-то скрывается. В данном коде это не особо важно и уровень абстракции можно поднять выше ничего особо не потеряв в исполнении и понимании подобного кода.

Я не программирую на С++

Но пришли рассказать как нужно программировать на C++? Ну OK, давайте, я с интересом послушаю. А может и не я один.

но на Java и других подобных ЯП сегодня такой код данность.

Так та же Java была убога в плане освобождения ресурсов с самого рождения. Тамошний finally есть ни что иное, как С-шный goto err, только присыпанный синтаксическим сахаром. Ну и спустя полтора десятка лет траха и собирания граблей вам дали try-with-resources скоммунизженный их C#-овского using. Так себе история успеха. Не удивительно, что приходится изобретать лисапеды.

Кстати, а можно ссылку на код, в котором вот эти вот with-ы используются в полный рост?

И лаконичность кода сегодня важна.

Этот тезис нуждается в доказательстве.

Конечно если только вы не выжимаете наносекунды на критичном участке рантайма.

В наше время не смысла применять C++ если скорость и/или предсказуемость не важны. Когда не важны люди берут ту же Java, а когда не важны совсем -- и Python с JavaScript-ом.

Но когда скорость и ресурсоемкость важны, то лаконичность, за которой прячется неявные new/delete, std::async-и с std::future и пр., однозначным достоинством уже не является. Как и лаконичность, основанная на пятиэтажных шаблонах, с сообщениями об ошибках на 800 строк и internal compiler error когда в одной единице трансляции заиспользовали пару навороченных шаблонных библиотек.

>Но пришли рассказать как нужно программировать на C++? Ну OK, давайте, я с интересом послушаю. А может и не я один.

Не вижу смысла. Уровень большинства программистов застрял в 90-х. Инструменты застряли там же. Зачем, мне грубо говоря метать бисер перед свиньями? Всё порешает рынок. Когда конторы использующие устаревшие инструменты начнут закрываться не выдержав конкуренции компаниям использующие современные инструменты. Тогда и не нужно будет ничего доказывать. И спойлер. Таких инструментов ещё нет. Поэтому можете продолжать изображать из себя специалиста по крестам считая свой подход верным и конкурентным. И вы будет правы, у вас сейчас нет конкурентов.

>Так та же Java была убога в плане освобождения ресурсов с самого рождения

Здесь бы я вернул ваши аргументы, зачем вы лезете туда, где у вас нет опыта работы?

>Кстати, а можно ссылку на код, в котором вот эти вот with-ы используются в полный рост?

В моём IoC фреймворке используется, но он ещё не в опенсорц. Ссылку искать лень.

Уровень большинства программистов застрял в 90-х.

О как... Мне иногда кажется, что современным говнокодерам до программистов из 90-х никогда не дорасти, но я из своей крошечной выборки никаких выводов не делаю. А вы, надо полагать, лично знакомы с кодом большинства программистов, отсюда и такие глобальные выводы.

Зачем, мне грубо говоря метать бисер перед свиньями?

Т.е. вы один здесь такой в белом пОльто стоите красивый?

Здесь бы я вернул ваши аргументы, зачем вы лезете туда, где у вас нет опыта работы?

Как раз опыт работы на Java у меня был.

В моём IoC фреймворке используется, но он ещё не в опенсорц. Ссылку искать лень.

Да, с аргументацией у вас как-то совсем слабенько. Предлагаете просто верить вам на слово?

>А вы, надо полагать, лично знакомы с кодом большинства программистов, отсюда и такие глобальные выводы.

Не нужно быть знакомым с кодом большинства программистов, достаточно посмотреть на инструменты разработки которые большинство использует. С такими инструментами даже хороший программист будет создавать плохой код. Я уже не говорю о разработке кода в корпорациях, для них даже инструмент в виде Go придумали отражающий суть современного программирования.

>Т.е. вы один здесь такой в белом пОльто стоите красивый?

Я опросы не проводил. Но 99% стандартное правило.

>Как раз опыт работы на Java у меня был.

У меня опыт С++ тогда тоже был.

>Предлагаете просто верить вам на слово?

Я ничего не предлагаю ) Высказал свои мысли, а доказывать кому-то что-то ) Доказываю делами - опубликованными проектами.

достаточно посмотреть на инструменты разработки которые большинство использует.

Т.е. вы предлагаете об уровне работ Микеланджело или Рафаэля судить по их кистям и палитрам? O_o

Я опросы не проводил. Но 99% стандартное правило.

Опросы не проводили, но выводы делаете. Отлично.

Про 99% -- это какое такое правило?

У меня опыт С++ тогда тоже был.

Но пришли учить C++ников программировать сейчас.

Высказал свои мысли

Ваш первый комментарий не похож на мысли, он поход на утверждение. Которое было бы желательно чем-то подтвердить. Однако, пока что все идет к тому, что ваши слова это обычный интернетовский бла-бла-бла.

Доказываю делами - опубликованными проектами.

Которые не в опенсорсе и на которые ссылки искать лень. Понятно.

>Т.е. вы предлагаете об уровне работ Микеланджело или Рафаэля судить по их кистям и палитрам? O_o

Я предлагаю сравнить инструменты доступные Микеланджело и современному профессиональному художнику. То, что умеет делать современный профессиональный художник в очень короткие сроки - Микеланджело бы от зависти помер. Я уже не говорю, что современный профессионал на голову выше по работам художников прошлого. Я здесь конечно про цифру и 3D в частности.

>Про 99% -- это какое такое правило?

Скорее народная мудрость. У вас часом криокамера не протекла, если подобные простые вещи вам приходиться объяснять?

>Но пришли учить C++ников программировать сейчас.

Так и вы пришли учить Java-ов сейчас )

>ваши слова это обычный интернетовский бла-бла-бла

Как и ваши ) В эту игру можно играть вдвоём.

Я предлагаю сравнить инструменты доступные Микеланджело и современному профессиональному художнику.

Это у тех, кто пишет маслом по холсту? Ну сравните. Интересно какой прогресс в кистях и палитрах вы обнаружите. Хотя химический состав красок, наверняка, далеко ушел вперед.

Скорее народная мудрость.

Какая именно?

Так и вы пришли учить Java-ов сейчас )

И тут бы ссылку на мои слова о том, где я бы говорил хоть что-то о том, как следует программировать на Java.

Наверное программисты в 20 веке приводили точно такие же аргументы при переходе с ассемблера на высокоуровневые языки типа С++

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

Подобные аргументы были когда в массы стало проникать ООП. И, что характерно, подобные аргументы до сих пор применяются. Только вот делают это люди, которые специализируются на специфических областях, вроде вычислительной математики или разработки под совсем уж дохлый embedded (где на все про все пара-тройка килобайт в наличии). Причем, что характерно, зачастую они правы, т.к. в их областях выигрыша от того же ООП нет.

Выигрыш от ООП есть, но нет качественных инструментов. Мутант типа крестов не считается. Ржавый полуинвалидный ООП тоже.

Хм... Чисто моё мнение. Пример с if-ами ужасен. Если не ошибаюсь, правило гласит: если вам в коде приходится писать несколько if-ов подряд (или оператор case) - код нуждается в редизайне через создание классов-наследников с оверрайдингом метода, поскольку имеется попытка впихнуть в один метод разные действия. Замена if на goto - это замена шила на мыло. Глобальные переменные - не хватает сущностей. Они же не сами по себе в коде болтаются, у них есть какой-то смысл. Логически рассуждая, их надо упаковать хотя бы в синглтон (если, конечно, это позволено в конкретном проекте), иначе с ними потом не разгребешься... Я обычно не влезаю в профессиональные обсуждения, но здесь как-то всё очень сыро, а комментарии уводят в дебри рассуждений...

Если не ошибаюсь, правило гласит: если вам в коде приходится писать несколько if-ов подряд (или оператор case) - код нуждается в редизайне через создание классов-наследников с оверрайдингом метода, поскольку имеется попытка впихнуть в один метод разные действия.

Это вопрос парадигмы и стиля программирования. В функциональном программировании обычно всё построено именно на аналогах case.

Как раз вот на днях объяснял на занятии, что общепринятой практикой в Scheme является написание рекурсивной функции, тело которой состоит из формы cond, даже когда cond можно заменить на if.

А что говорит функциональная парадигма по поводу использования goto? Можно, конечно, придумать разные случаи использования и if'ов и goto и даже не упоминая парадигму, просто сказать, что так мы пишем на Фортране 🙂. В заголовке, вот только, C++ упомянут - почему бы и не вспомнить про ООП? Но я ожидал подобной реакции - потому и написал, что, как правило, в профессиональные обсуждения не влезаю, ибо в таких обсуждениях всё превращается в холивар и рассуждения про парадигмы.

По поводу рекурсии - в Израиле говорят (правда, с долей шутки 🙂), что если на интервью предлагаешь для решения тестовой задачки рекурсию - значит интервью не прошёл 🙂.

В функциональной парадигме по поводу использования goto есть call/сс.

По поводу рекурсии - дикие люди. Даже компиляторы C++ и Фортрана умеют эффективно обрабатывать концевую рекурсию.

Что касается C++, то он в некоторой степени не чужд современной моде на миграцию от элементов ООП к элементам ФП.

Sign up to leave a comment.

Articles