Небольшое введение

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

Зачем эта статья?

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

Опираться я буду на прекрасную статью компании PVS-Studio как на самую полную и близкую к моему стеку.

Оператор goto

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

Когда может быть полезно

Оператор goto в некоторых случаях, по моему мнению, может улучшить читаемость кода. Рассмотрим фрагмент кода для выполнения GET-запроса с помощью библиотеки winhttp

#include <winhttp.h>

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

Опять же, на мой взгляд, весь этот код из лестниц выглядит криво, и пример с goto выглядит симпатичнее:

 #include <winhttp.h>

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

  internet = WinHttpOpen(<...>);
  if (!internet){
  goto CLOSE_HANDLES_AND_RET;
  }
  <...>
  connect = WinHttpConnect(<...>);
  if(!connect){
  goto CLOSE_HANDLES_AND_RET;
  }
  <...>
  request = WinHttpOpenRequest<...>);	
  if(!request){
  goto CLOSE_HANDLES_AND_RET;
  }
  <...>
  result = true;

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

  return result;
}

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

Более сложный пример кода можно найти на MSDN

Почему вредно

Если коротко, то при злоупотреблении goto ваш код становится запутанным и сложным для восприятия, а отследить пути выполнения программы становится всё сложнее

Глобальные переменные

Глобальные переменные в C и C++ объявляются вне тела какой-либо функции и видны всей программе. Что касается их применения, лично мне сразу же на ум приходит UEFI и его структуры данных EFI_SYSTEM_TABLE, EFI_BOOT_SERVICES, EFI_RUNTIME_SERVICES. Данные структуры предоставляют приложению интерфейс для работы с аппаратной частью. Указатель на структуру EFI_SYSTEM_TABLE (она содержит указатели на две другие структуры) передаётся в качестве параметра в точку входа программы.

#include <Uefi.h>
    	
EFI_STYSTEM_TABLE *gST = NULL;
EFI_BOOT_SERVICES *gBS = NULL;
    
EFI_STATUS EFIAPI EntryPoint(HANDLE image_handle, EFI_SYSTEM_TABLE *system_table){
  gST = system_table;
  gBS = gST->BootServices;
   
  <Основной код>
  return EFI_SUCCESS;
}

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

Когда может быть полезно

Польза от применения глобальных переменных заключается в простоте доступа к ним. Продолжая пример с UEFI: управление памятью происходит с помощью функций структуры EFI_BOOT_SERVICES (например, AllocatePool и FreePool). Итак, вы написали свои обёртки для функций работы с памятью, написали функции для создания различных необходимых в процессе работы объектов; функции, которые инкапсулируют процессы подготовки и освобождения ресурсов. В результате получаем что-то подобное:

#include <Uefi.h>
	
VOID* SimpleAlloc(EFI_BOOT_SERVICES* boot_serv, size){
	return boot_serv->AllocatePool(size, <...>);
}

EFI_STATUS AllocateNecessaryStructs(EFI_BOOT_SERVICES* boot_serv, <...>){
	VOID* buffer = SimpleAlloc(boot_serv, 123);
	<...>
	return EFI_SUCCESS;
}

EFI_STATUS Prepare(EFI_BOOT_SERVICES* boot_serv, <...>){
	EFI_STATUS last_status = AllocateNecessaryStructs(boot_serv, <...> );		
	<...>
	return EFI_SUCCESS;
}

EFI_STATUS EFIAPI EntryPoint(HANDLE image_handle, EFI_SYSTEM_TABLE *system_table){
	EFI_STATUS last_status = Prepare(system_table->BootServices, <...> );
	<...>
   	return EFI_SUCCESS;
}

Как можно заметить, мы вынуждены тянуть указатель на EFI_BOOT_SERVICES через все функции, в которых (и во вложенных функциях которых) будет происходить работа с памятью. Это не совсем правильно, потому что захламляет стек и приводит к постоянному копированию значения указателя. А что, если нам понадобятся и две другие структуры?

При хранении указателя на EFI_BOOT_SERVICES вы лишены этих проблем и приведённый выше код становится проще

#include <Uefi.h>
	
EFI_STYSTEM_TABLE *gST = NULL;
EFI_BOOT_SERVICES *gBS = NULL;

VOID* SimpleAlloc(size){
	return gBS->AllocatePool(size, <...>);
}

EFI_STATUS AllocateNecessaryStructs(<...>){
	VOID* buffer = SimpleAlloc(123);
	<...>
	return EFI_SUCCESS;
}

EFI_STATUS Prepare(<...>){
	EFI_STATUS last_status = AllocateNecessaryStructs(<...> );		
	<...>
	return EFI_SUCCESS;
}

EFI_STATUS EFIAPI EntryPoint(HANDLE image_handle, EFI_SYSTEM_TABLE *system_table){
	gST = system_table;
	gBS = gST->BootServices;
	EFI_STATUS last_status = Prepare(<...> );
	<...>
   	return EFI_SUCCESS;
}

Почему вредно

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

VOID* CustomMalloc(UINT64 BufferSize){
  if(gBS = NULL){
    return NULL;
  }
  
  VOID* Buffer = gBS->AllocatePool(...);
  <...>
}

Представьте, что вы случайно опечатались в условии, и теперь при попытке выделения памяти происходит разыменование NULL, что приводит к UB. Также простота доступа приводит к сложности отслеживания причин изменения значения глобальной переменной. В пределах небольшого проекта несложно найти, когда, где и почему изменяется значение глобальной переменной (особенно, если вы - его единственный разработчик и понимаете всю архитектуру). Но что, если речь идёт о крупном проекте, разрабатываемом большой командой? В таком случае глобальные переменные могут изрядно потрепать вам нервы.

Массив на стеке

Про массив на стеке довольно подробно уже написано в статье PVS-Studio, здесь я хочу остановиться только на примере.

Когда может быть полезно

Когда вы точно знаете размер используемого буфера или его максимально возможный размер. Например, функция WinAPI GetModuleFileName() принимает указатель на массив символов для сохранения результата. Максимальный размер пути в Windows 260 символов. Соответственно, мы можем выделить память на стеке, так как точно знаем максимальный размер.

bool GetProcessName(std::wstring& proc_name) {
  wchar_t process_path[MAX_PATH];
  
  if (!GetModuleFileName(NULL, process_path, MAX_PATH)) {
    return false;
  }
  
  std::wstring process_name = process_path;
  proc_name = process_name.substr(process_name.find_last_of('\\') + 1);

  return true;
}

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

Почему вредно

Подробно о вреде использования массивов на стеке рассказано всё в той же статье PVS-Studio. Я только отмечу, что такой подход приводит к избыточности, поскольку мы почти всегда выделяем буфер с запасом; а переполнение такого массива может привести к изменению её поведения злоумышленником (например, к выполнению shell-кода)

Напишу всё сам

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

В какой-то момент передо мной встала задача из-под UEFI перечислить все подключенные PCI-устройства и узнать, какие из них являются PCI-to-PCI мостами. Казалось бы, можно использовать те протоколы, что предоставляет сам UEFI, вызывать функции интерфейса для получения адреса устройства и его типа и все будет хорошо. Вот только отдельного протокола для работы с мостами в UEFI нет (поправьте, если я ошибаюсь). Всё равно нужно читать из конфигурационного пространства PCI-устройства. К тому же, функция GetLocation() возвращает 3 UINTN (UINT64 в моём случае) для указания положения устройства (номер сегмента я здесь не учитываю), тогда как оно полностью умещается в UINT16 (8 бит на номер шины + 5 бит на номер устройства + 3 бита на номер функции). Добавим сюда процесс перечисления, который требует выделения памяти под хэндлы устройств и её последующего освобождения.

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

Когда может быть полезно

В описанной мной ситуации я вижу следующие плюсы от самописного решения:

  • Я разобрался в работе технологии, лучше понял тонкости работы

  • Реализован и используется только необходимый функционал

  • Добавлен специфичный для задачи функционал, которого не было в стандартном интерфейсе

  • Я не навредил процессу разработки основного решения и не сорвал никаких сроков

Почему вредно

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

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

Заглянуть за пределы массива

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

typedef struct _PACKET{
  uint64_t	sign;
  uint64_t	id;
  uint64_t	answer_len;
  uint8_t	raw_answer[0];
} PACKET;

void* GetCopyOfRawAnswer(uint8_t* full_pack){
  PACK* pack = reinterpret_cast<PACK*>(full_pack);
  if(pack->sign != PACK_SIGN){
    return nullptr;
  }

  uint8_t* raw_answer = new uint8_t[pack->answer_len];
  memcopy(raw_answer, pack->raw_answer, pack->answer_len);
  return raw_answer;
}

Как это работает? При таком объявлении массива в конце структуры компилятор позволяет нам работать с оставшимся данными по имени массива. Почему не uint8_t* raw_answer? Все потому, что тогда те 8 байт данных (или то число, какое используется на данной платформе), которые лежат после поля answer_len будут восприняты как указатель. О значении этого указателя можно только догадываться, как и о том, к чему приведёт его разыменование. Возможность использования массивов с размером 0 зависит от компилятора (точно поддерживаются MSVC и GCC в качестве расширения).

Когда может быть полезно?

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

Почему вредно?

Во-первых, это может привести к путанице при использовании оператора sizeof, который вернёт размер структуры без учёта хвоста. Во-вторых, не забывайте про выравнивание структур:

typedef struct _pack{
  uint8_t  type;
  uint64_t len;
  uint8_t  sig;
  uint64_t data[0];
} pack;

int main() {
 uint8_t packet[] = {
  0x1,
  0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
  0xAD,
  0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8,
  0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10
  };

  pack* p = (pack*)packet;
} 

Как думаете, что содержится в полях данной структуры? Зависит от ситуации, но на моём стенде результат следующий:

Packet info:
type: 1
len: 60504030201ad00
sig: 7
data[0]: cccccccccccc100f
data[1]: cccccccccccccccc

Из-за выравнивания полей структур данные утеряны. Для исправления ситуации необходимо упаковать структуру:

#pragma pack(push, 1)
typedef struct _pack{
  uint8_t  type;
  uint64_t len;
  uint8_t  sig;
  uint64_t data[0];
}pack;
#pragma pack(pop)

Это позволит нам получить желаемый результат:

Packet info:
type: 1
len: 2
sig: ad
data[0]: 807060504030201
data[1]: 100f0e0d0c0b0a09

Заключение

Как говорит мой знакомый профессор математики: "Если бы было лучшее решение, других бы не было". Не стоит сразу клеймить решение, если вы видели его в статьях с названием а-ля "10 антипаттернов <...>". Возможно, оно продиктовано вескими причинами, о которых вы не знаете.

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

Комментарии (84)


  1. MountainGoat
    11.06.2024 05:46
    +5

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


  1. vadimr
    11.06.2024 05:46
    +1

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


  1. Goron_Dekar
    11.06.2024 05:46
    +4

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

    #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;
    }

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


    1. silverpopov
      11.06.2024 05:46

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


      1. Goron_Dekar
        11.06.2024 05:46
        +1

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

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


        1. vadimr
          11.06.2024 05:46

          Это не лучше.


          1. Goron_Dekar
            11.06.2024 05:46
            +1

            Не согласен.


            1. qw1
              11.06.2024 05:46
              +4

              На мой взгляд хуже, потому что внимание размывается на 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;
              }
              

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


              1. GBR-613
                11.06.2024 05:46
                +1

                А проще?

                #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. И не надо ничего лишнего.


                1. qw1
                  11.06.2024 05:46

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


      1. SpiderEkb
        11.06.2024 05:46

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

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


        1. Goron_Dekar
          11.06.2024 05:46
          +1

          Есть.

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

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

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


          1. vadimr
            11.06.2024 05:46

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


            1. Goron_Dekar
              11.06.2024 05:46

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


              1. vadimr
                11.06.2024 05:46

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

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


                1. Goron_Dekar
                  11.06.2024 05:46

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

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


                  1. vadimr
                    11.06.2024 05:46
                    +1

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

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


                  1. SpiderEkb
                    11.06.2024 05:46

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

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

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


                    1. eao197
                      11.06.2024 05:46

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

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


          1. SpiderEkb
            11.06.2024 05:46

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

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

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

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

            Все-таки лучше, когда это реализовано на уровне компилятора, который сам генерирует блок _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


            1. Goron_Dekar
              11.06.2024 05:46

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

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


        1. eao197
          11.06.2024 05:46
          +3

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

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

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


          1. SpiderEkb
            11.06.2024 05:46

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

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


            1. eao197
              11.06.2024 05:46
              +4

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

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

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

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

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


      1. seamant Автор
        11.06.2024 05:46
        +3

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

        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`


        1. eao197
          11.06.2024 05:46

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

          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 вот просто проситься быть переписанным нормальным способом.


          1. seamant Автор
            11.06.2024 05:46
            +1

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


            1. eao197
              11.06.2024 05:46

              Отступы я поправил, но я не понимаю, чем вам не понравился вариант с 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 в циклах доставят сам незабываемых эмоций.


              1. seamant Автор
                11.06.2024 05:46

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


                1. eao197
                  11.06.2024 05:46
                  +2

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


    1. voldemar_d
      11.06.2024 05:46
      +3

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


      1. Goron_Dekar
        11.06.2024 05:46

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


        1. voldemar_d
          11.06.2024 05:46

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


          1. Goron_Dekar
            11.06.2024 05:46

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

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

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


            1. voldemar_d
              11.06.2024 05:46
              +4

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

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


              1. Goron_Dekar
                11.06.2024 05:46
                +1

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

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

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


                1. voldemar_d
                  11.06.2024 05:46

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

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


  1. kumkurum
    11.06.2024 05:46
    +1

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


    1. SpiderEkb
      11.06.2024 05:46

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

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

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


      1. Goron_Dekar
        11.06.2024 05:46

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

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


        1. SpiderEkb
          11.06.2024 05:46
          +2

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

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


          1. Goron_Dekar
            11.06.2024 05:46
            +1

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

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


            1. SpiderEkb
              11.06.2024 05:46
              +2

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

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

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


              1. Goron_Dekar
                11.06.2024 05:46
                +1

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


                1. SpiderEkb
                  11.06.2024 05:46

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

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

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


          1. voldemar_d
            11.06.2024 05:46

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


    1. zzzzzzerg
      11.06.2024 05:46
      +1

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

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


      1. SpiderEkb
        11.06.2024 05:46

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

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


        1. zzzzzzerg
          11.06.2024 05:46
          +1

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


  1. SpiderEkb
    11.06.2024 05:46

    Как и любой другой инструмент, 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 никогда не проскочим :-)

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

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

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


    1. Deosis
      11.06.2024 05:46
      +1

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


      1. SpiderEkb
        11.06.2024 05:46

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


        1. voldemar_d
          11.06.2024 05:46

          __try / __except?


          1. SpiderEkb
            11.06.2024 05:46

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

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


            1. voldemar_d
              11.06.2024 05:46

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


              1. SpiderEkb
                11.06.2024 05:46

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

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

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

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


                1. voldemar_d
                  11.06.2024 05:46

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

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


                  1. SpiderEkb
                    11.06.2024 05:46

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

                    Любая программа (бинарник) у нас может быть описана внутри другой программы как обычная процедура (с модификатором 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 - там больше всяких параметров, более муторно писать). Но работать будет точно также.


                    1. voldemar_d
                      11.06.2024 05:46

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

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


                      1. SpiderEkb
                        11.06.2024 05:46

                        В целом да. Но поскольку на нашей платформе есть удобный механизм генерации исключений через посылку сообщений с очередь программы (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

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

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


    1. voldemar_d
      11.06.2024 05:46

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


      1. vadimr
        11.06.2024 05:46

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


        1. voldemar_d
          11.06.2024 05:46

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


          1. vadimr
            11.06.2024 05:46

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


            1. voldemar_d
              11.06.2024 05:46

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

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

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


              1. vadimr
                11.06.2024 05:46
                +1

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

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


                1. voldemar_d
                  11.06.2024 05:46

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


    1. firehacker
      11.06.2024 05:46

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


  1. anonymous
    11.06.2024 05:46

    НЛО прилетело и опубликовало эту надпись здесь


  1. zatim
    11.06.2024 05:46

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


    1. vadimr
      11.06.2024 05:46

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


  1. alexac
    11.06.2024 05:46
    +7

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

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

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

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

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

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


    1. voldemar_d
      11.06.2024 05:46

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

      Будет же std::start_lifetime_as


  1. skovoroad
    11.06.2024 05:46

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

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

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

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

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


    1. seamant Автор
      11.06.2024 05:46

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


      1. voldemar_d
        11.06.2024 05:46
        +1

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


        1. seamant Автор
          11.06.2024 05:46

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


  1. GBR-613
    11.06.2024 05:46

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


  1. Foror
    11.06.2024 05:46

    В 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));
    }


    1. firehacker
      11.06.2024 05:46
      +2

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


      1. Foror
        11.06.2024 05:46

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

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


        1. firehacker
          11.06.2024 05:46
          +2

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


          1. qw1
            11.06.2024 05:46

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

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


  1. S_gray
    11.06.2024 05:46

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