LINUX.ORG.RU

Обработка сишных ошибок в C++

 ,


1

6

Как принято работать с кодами ошибок C API в C++?
У меня есть такой код, но по нему несколько вопросов

//R — тип результата функции которая может вернуть ошибку
template<typename R>
class is_minus_one{
public:
  bool operator()(const R& val){
    return val == -1;
  }
};

//FailPolicy — функтор для проверки является ли результат выполнения функции ошибкой
template<template<class> typename FailPolicy>
class ExceptionWrapper{
public:
  template<typename R, typename... ArgsT>
  static auto wrap(R (*func)(ArgsT...)){
    return [func](ArgsT... args){
      FailPolicy<R> isFail;
      R res = func(args...);
      if(!isFail(res))
	return res;
      else {
	throw std::system_error(errno, std::system_category());
      }
    };
  }
};
using std_ex = ExceptionWrapper<is_minus_one>;
#define EX_WRAP(func) const static auto ex_##func = std_ex::wrap(func)

//Использование
EX_WRAP(epoll_create1);
EX_WRAP(close);
EX_WRAP(epoll_wait);
EX_WRAP(epoll_ctl);

  • Как избежать копирования аргументов?
  • Как сделать, чтобы такие обертки работали для функций с переменным количеством аргументов (fcntl, ioctl)?
  • Каковы накладные расходы использования исключений вместо кодов ошибок?
  • Правильно ли я понимаю, что при ошибке в POSIX всегда возвращается именно -1?
  • Есть ли смысл переписать код, заменив шаблоны на макросы?
  • Файл с обертками нужно будет подключать во все места где они используются. Может ли это стать причиной медленной компиляции, и как с этим бороться?

Наверняка есть более адекватные способы для обработки кодов ошибок в C++, но нагуглить их сходу не удается.


более адекватные способы для обработки кодов ошибок в C++

обрабатывать так же, как и в С

а у тебя какое-то жуткое плюсодрочево

Harald ★★★★★ ()

чес говоря - это БРЕД какой то
c++ шаблоны головного мозга ??
зачем там на каждый апи враппер ошибок ?

anonymous ()

Создай enum (можно enum class, если С++11, для более строго типизации), перечисляющий допустимые значения кода ошибки. Сравнивай код ошибки с элементами енума, передавай код ошибки только с типом этого енума. Остальное как в С.

За исключения - бить по пальцам.

annulen ★★★★★ ()
Ответ на: комментарий от anonymous

зачем там на каждый апи враппер ошибок ?

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

userd ()
Ответ на: комментарий от BceM_IIpuBeT

Как такую штуку разделять на .hpp и .cpp?

Насколько мне известно код с шаблонами нельзя разделять =\ только если инклуд .cpp сделать

userd ()
Ответ на: комментарий от annulen

За исключения - бить по пальцам.

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

userd ()
Ответ на: комментарий от userd

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

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

anonymous ()
Ответ на: комментарий от userd

Да и что плохого в исключениях? Появляются проблемы с производительностю?

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

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

Тогда мне придется придумывать как из функции вернуть либо ошибку либо значение

Возвращай пару значений. В добавок, это более наглядно и не препятствует рефаторингу в будущем

annulen ★★★★★ ()
Ответ на: комментарий от Harald

метод goto error; делает обработку ошибок достаточно компактной, разработчики ядра рекомендуют :)

Ок, сравни:

int read_bytes = ex_read(client_fd, buffer.data(),buffer.size());
VS
int result;
int read_bytes;
result = read(client_fd, buffer.data(),buffer.size());
if(result == -1){
  goto error;
}
read_bytes = result;
return 0;
error:
perror("What can I do now?");
return 1;

userd ()
Ответ на: комментарий от userd

Вместо пары можно использовать что-то вроде std::any/boost::variant (т.е. типизированную обертку над union, позволяющую функции возвращать либо один тип, либо другой)

Вырожденный случай - std/boost::optional - возвращается либо значение нужного типа, либо nullopt

annulen ★★★★★ ()
Ответ на: комментарий от userd

Насколько мне известно код с шаблонами нельзя разделять

«Одинарный» template — можно. А вот когда template в template классе, то компилятор объявляет джихад.

BceM_IIpuBeT ★★★★ ()
Ответ на: комментарий от annulen

Не, any/variant сделаны для объектов, которые в рантайме могут менять свой тип. Optional для типов, которые могут не иметь значения. Запихивать в них логику обработки ошибок глупо. Если функция должна возвращать насколько значений, то нужно использовать tuple (на худой конец pair, если старый компилятор).

С релизом c++17 появится Structured Bindings и станет совсем хорошо.

Deleted ()
Ответ на: комментарий от userd
int read_bytes = ex_read(client_fd, buffer.data(),buffer.size());

Раз уж C++, почему бы не писать объектно-ориентированный код в объектно-ориентированном виде?

client.read(&buffer);

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

i-rinat ★★★★★ ()
Последнее исправление: i-rinat (всего исправлений: 1)
Ответ на: комментарий от Deleted

Не, any/variant сделаны для объектов, которые в рантайме могут менять свой тип.

Так у нас именно такой случай и есть: либо ошибка, либо нормальный ответ, что из них мы получим можно узнать только в рантайме

annulen ★★★★★ ()
Ответ на: комментарий от annulen

Ну тут больше вопрос философии: возвращать одно значение, которое может быть одним из 2х типов или возвращать 2 значения (само значение и ошибку).

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

В c++17 можно будет сделать так

auto [result, err] = readBuf();
if (err) {
    // ...
}
// use result

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

Deleted ()

Как избежать копирования аргументов?

Интегеры незазорно и скопировать. А вообще — inline.

И ещё одно: обычно с template argument pack используют std::forward. Почитай: http://en.cppreference.com/w/cpp/utility/forward

Есть ли смысл переписать код, заменив шаблоны на макросы?

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

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

Да, может несколько продлить компиляцию, но #pragma once (предпочтительнее) или include guards в помощь

KennyMinigun ★★★★★ ()

Есть ощущение, что у вас явное переусложнение.

Для начала следует ответить на вопрос: какую цель вы преследуете?

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

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

Так что идти нужно от конкретной задачи и конкретных условий.

По поводу некоторых ваших вопросов.

Для того, чтобы не копировать аргументы нужно использовать perfect forwarding. Например, простейшая обертка, которая бросает исключение при ошибке, могла бы выглядеть так (disclaimer: код не компилировался):

template<typename F, typename... Args>
auto wrap(F f, Args &&... args) {
  auto r = f(std::forward<Args>(args)...);
  if(-1 == r)
    throw std::system_error(errno, std::system_category());
  return r;
}
Что позволит вам писать в таком стиле:
auto bytes_read = wrap(read, fd, buf, sizeof(buf));

Накладные расходы нужно замерять. Т.к. исключения, в принципе, заметно дороже кодов возврата, но стоимость будет определяться совокупностью факторов: частота возникновения исключений, глубина проброса исключения, количество проверок, которое делается при отказе от исключений. Может оказаться, что если исключения бросаются очень редко, а ловятся только где-то на самом верху, то стоимость исключений будет не сильно выше, чем постоянное использование if(-1 == ...)

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

eao197 ★★★★★ ()
Ответ на: комментарий от userd

Зачем каждый раз? Один раз сделать обёртку над c-api и всё.

RazrFalcon ★★★★★ ()
Ответ на: комментарий от Deleted

Советую посмотреть на rust. Вот там компактно. А в го обычная сишка.

RazrFalcon ★★★★★ ()
Ответ на: комментарий от RazrFalcon

что на какой ?
а какие бывают дизайны ?
ошибка
1) обрабатывается на месте 2) отправляется в отложеный вызов 3) генерирует ексепшн 4) случаи когда ошибка с чем то проверяется и результат bool возвращается из программы

anonymous ()
Ответ на: комментарий от RazrFalcon

Ну и у многих c++11 макс.

Для этого есть boost::variant и boost::optional, а так же сторонние реализации этих же стандартных шаблонов, если не хочется связываться с бустом.

annulen ★★★★★ ()
Ответ на: комментарий от i-rinat

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

Если из read (подразумевая read(2)) вернулся -1 можно ещё проверить errno, если там EINTR, то исключение не бросать, а вернуть 0. Для EAGAIN и EWOULDBLOCK то же самое. Правда теперь неясно как отличить ситуацию с посылкой нулевой длины (например, TCP server termination) от прерванного syscall. Поэтому я нашёл что в подобных обёрточках лучше возвращать структуру типа IOResult, с полями Ok, Completed и LastError, а уж в крайнем случае (то есть если errno не EINTR/EAGAIN/EWOULDBLOCK) кидать исключение.

d_a ★★★★★ ()
Ответ на: комментарий от RazrFalcon

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

Ну это в основном не проблема. В большинстве случаев есть некое пустое значение. Для ошибки - значение «не ошибка». В худшем случае заюзать optional, но такие случаи мне представляются с трудом.

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

Deleted ()
Ответ на: комментарий от annulen

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

Можете привести пример? За исключением оптимизаций STL-я для перемещаемых/уничтожаемых без исключений пользовательских типов.

vzzo ★★★ ()

Возвращайте что-то похожее на std::future с точки зрения работы с ошибками: обёртку над std::variant<T, Error> с функцией get(), которая будет возвращать значение T, если оно есть, а если его нет, кидать исключение, содержащее Error.

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

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

vzzo ★★★ ()
Ответ на: комментарий от anonymous

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

RazrFalcon ★★★★★ ()
Ответ на: комментарий от Deleted

В большинстве случаев есть некое пустое значение.

То есть вы изображаете result/optional/maybe/either через tuple?

ущерб по производительности

Ну обрабатывать ошибки всё равно надо.

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

Не распарсил.

RazrFalcon ★★★★★ ()
Ответ на: комментарий от d_a

Или забить на сиплюсплюсфикацию ради сиплюсплюсфикации и инкапсулировать полезные действия, а не всё подряд.

i-rinat ★★★★★ ()
Ответ на: комментарий от RazrFalcon

Нет, это не замена исключений.

Например, когда я настраиваю sandbox, мне надо сделать пачку open/read/write/close, chroot, chdir, unshare, seccomp и exec. Мне не важно, какой из них не удался, но я хочу поймать любую ошибку в одном месте и выдать вызывающему процессу нормальный ответ по RPC, содержащий полезную отладочную информацию вроде зафейлившегося системного вызова и его аргументов.

Исключения позволяют даже с использованием таких обёрток и парочки дефайнов решить задачу в три строки. «Замена» потребует писать if+return после каждого сисколла.

vzzo ★★★ ()
Ответ на: комментарий от vzzo

Во-первых, каждый «сискол» не бросает исключение, поэтому оборачивать их всё равно придётся.

Ну и это является заменой для тех, кто исключения не использует в принципе.

RazrFalcon ★★★★★ ()
Последнее исправление: RazrFalcon (всего исправлений: 1)
Ответ на: комментарий от i-rinat

С read действительно пример неудачный получился, потому что его как-раз и нужно повторять до тех пор, пока он не вернет ошибку и EAGAIN. Кстати, а почему ты передаешь указатель на буфер, не логичнее ли в клиенте сделать чтобы read в клиенте принимал параметр по ссылке? Ведь если нужно отбрасывать пришедшие данные можно добавить метод client.discard() без параметров. buffer имеет тип std::array<char, buffer_size>, бтв.
А вот методы которые не должны возвращать ошибку (например, close, socket, bind, или listen) следует обернуть и кидать исключения.

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

Тут как раз таки все логично, я планировал сделать epoll + reactor и ловить сигналы через signalfd(ну и таймеры через timerfd, и т.д.)

userd ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.