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