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

Есть С++ код где активно применяются POSIX вызовы, которые возвращают коды ошибок. В случае если такой вызов вернул ошибку то я считываю errno и кидаю соответсвующее исключение throw std::system_error( errno, std::system_category(), "Сообщение об ошибе" );. На каждый такой вызов нам приходиться сохранять значение, писать if, писать throw, после чего код даже с самой простой логикой превращается в месиво. Чтобы уйти от этого, я написал небольшую обёртку, которая проверяет возвращаемое значение и в случае чего бросает:

   inline int PosixResultChecker( int ret_value, const std::string_view error_message ) {
      if ( ret_value < 0 ) [[unlikely]] {
         throw std::system_error( errno, std::system_category(), error_message.data() );
      } else {
         return ret_value;
      }
   };
   
   int main() {
      try {
         auto descriptor = PosixResultChecker( open( "/home/riv/tmp.txt", O_RDWR | O_NOCTTY | O_NDELAY ), "Could not open file '/home/riv/tmp.txt'" );
         PosixResultChecker( write( descriptor, "Hello world", strlen( "Hello world" ) ), "Could not write to '~/tmp.txt'");
      } catch ( std::system_error err ) {
         std::cout << err.what() << std::endl;
      }
      return 0;
   }

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


Ответы (2 шт):

Автор решения: user7860670

Прежде чем делать быстро, надо сделать правильно:

  • Непосредственно перед вызовом функции, устанавливающей errno, следует самостоятельно сбросить значение errno на 0. Так как они сами могут не сбрасывать и вызывать затем конфуз. А у вас этого нет.
  • Между записью / чтением errno и вызовом функции, устанавливающей errno, не должно быть вызовов никаких других функций. Так как они потенциально могут сами поменять значение errno. А у вас создается string_view, вызывается string_view::data, создается system_category.
  • У посиксных функций бывают разные типы возвращаемых значений. Тот же write возвращает ssize_t, а, например, mmap возвращает вообще void *. А у вас в примере предполагается, что бывает только int.
  • Если используются исключения, то все ресурсы должны иметь RAII обертки. А у вас при выкидывании исключения после write утекает файловый дескриптор.
  • Если используется std::system_error, то следует удостовериться, что третий параметр является указателем на C-строку, а std::string_view совсем не обязан хранить строку, заканчивающуюся нулем.

Делайте RAII обертки, которые бы инкапсулировали все посиксные функции и проверки их результатов.

→ Ссылка
Автор решения: KoVadim

Из того, что я видел

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

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

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

Второй способ - функциям даются сходные имена (например, плюс суффикс X). Разные "неудобные параметры" типа const char* заворачиваются в std::string/std::array.

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

Код восстановил по памяти, вот кусочек (тут улучшать и улучшать)

struct buffer_t {
    std::vector<char> data;
};

class Socket {
    int s;
    public:
        int write(const buffer_t& b) {
            int r = ::write(s, b.data.data(), b.data.size());
            if (r < 0) {
                throw "some error";
            }
            return r;
        }
        int read(buffer_t& b) {
            if (b.data.empty()) {
                throw "buffer empty";
            }
            int r = ::read(s, b.data.data(), b.data.size());
            return r;
        }
    ~Socket() {
        close(s);
    }
};
→ Ссылка