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++, но нагуглить их сходу не удается.


Ответ на: комментарий от KennyMinigun

std::forward — то, что надо, но насколько я понял он может использоваться когда параметры шаблона выводятся во время вызова функции. А у меня wrap(c_func) возвращает лямбду тип параметров которой выводится из сигнатуры c_func. Кстати, из-за этого моя обертка не работает когда в сигнатуре c_func есть переменное кол-во параметров (...).
Но вообще можно попробовать переписать wrap(c_func) чтобы оно возвращало функтор с шаблонным operator(cArgsT&&...) и применить std::forward. Это конечно будет полезно только при условии что лямбда не будет инлайнится.

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

О, т.е. при #pragma once файл будет парсится только один раз? Хорошо, но еще остается проблема с тем что этот шаблон будет инстанцироватся одинаковыми сишными функциями в каждом .cpp файле. Хотя может компиляторы научились кешировать такое, кто знает.

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

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

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

Ваша обертка у меня скомпилировалась и работает, только по ней есть один вопрос. Я часто вижу в пример проверку результата на r < 0, но в man и вашем примере указано r != -1. Какой вариант более правильный?

Судя по моим замерам(https://ideone.com/CfR4Mk), обработка исключений на месте более чем в 30 раз медленнее проверки кода возврата, но не влияет на производительность если ошибка не возникла. Следовательно, исключения стоит использовать только когда ошибки возникают намного реже чем вызов функции, и нет возможности обработать ошибку на месте.

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

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

Идея понятна, но что будет из себя представлять Error? Т.е. допустим у меня есть 3 функции: одна возвращает variant<A, AErr>, другая variant<B, BErr>. Третья использует обе функции и не может обработать их ошибки. Тогда она должна возвращать variant<X, AErr, BErr>? Или Err должен быть полиморфным классом ошибки, а вариант будет хранить указатель на него? Но тогда не ясно как выделять память под ошибки на стеке, а не в куче?

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

В точку)

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

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

Это дело личных предпочтений. Мне удобнее видеть, что функция принимает параметр, который может изменить. Передавать по ссылке это, может быть, и C++-way, только вот потом смотришь на такой код, и непонятно, что функция делает.

Она могла поменять аргумент? Стоит искать её определение? Функция принимает два аргумента, результат не используется. Видимо, один из аргументов — выходной. Какой именно?

Вот как-то так.

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

Идея понятна, но что будет из себя представлять Error? Т.е. допустим у меня есть 3 функции: одна возвращает variant<A, AErr>, другая variant<B, BErr>. Третья использует обе функции и не может обработать их ошибки. Тогда она должна возвращать variant<X, AErr, BErr>? Или Err должен быть полиморфным классом ошибки, а вариант будет хранить указатель на него? Но тогда не ясно как выделять память под ошибки на стеке, а не в куче?

Как правило, можно ввести единственное пространство ошибок и во всей программе обходиться только им, сложные ошибки пакуя в строки/доп. атрибуты. Пример от гугла — это grpc::Status / grpc::StatusCode.

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

я тренируюсь в написании асинхронного HTTP сервера

Зря :) Эта поляна уже плотно утоптана, даже мы там пытаемся топтаться ;)

Я часто вижу в пример проверку результата на r < 0, но в man и вашем примере указано r != -1. Какой вариант более правильный?

С точки зрения перестраховки спокойнее r<0. Но правильно сверяться с документацией.

Судя по моим замерам(https://ideone.com/CfR4Mk), обработка исключений на месте более чем в 30 раз медленнее проверки кода возврата, но не влияет на производительность если ошибка не возникла.

Такие микробенчмарки вводят в заблуждение. Более приближенным к жизни был бы бенчмарк, в котором вызывались бы функции до 5-6-7 уровней вложенности, в каждой из которых по 3-4-5 if-ов для проверки кодов возврата. Да еще желательно, чтобы между if-ами были какие-то действия (тогда процессору сложнее ветвления предсказывать).

Вот тогда разрывы между исключениями и кодами возврата будут меньше. Но все равно будут.

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

Да и что плохого в исключениях?

Исключения — по сути goto без меток. Однако это может быть нормой для fail-fast софта. Как по мне, монадическая обработка ошибок лучше.

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

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

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

P.S. Способы обработки ошибок в C++ https://www.youtube.com/watch?v=Fno6suiXLPs

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

я тренируюсь в написании асинхронного HTTP сервера

Зря :)

Зря тренируется или это про HTTP? Впрочем цыплят по осени считают (особенно если цель - получить опыт, что, по-моему, успешно выполняется).

Эта поляна уже плотно утоптана, даже мы там пытаемся топтаться ;)

Должно быть «Eao со товарищи» заняли 95% рынка, судя по комментарию :-). Впрочем туда все лезут, даже я. Со своими убогими stackfull корутинами и асинхронщиной во все поля (лавры golang покоя не дают). Однако, каждый туда пришедший довольно быстро понимает, что стек корутины на каждое соединение - это слишком много и нужно обрабатывать запросы комбинировано (контейнер, обрабатываемый реактором, на все соединения которые читаются или пишутся с конечным автоматом и корутина на каждый прочитанный запрос для генерации ответа).

Или коротко «не дождетесь». Щас каждый Jun пишет свой HTTP сервер (иногда с корутинами и асинхронным DB ORM).

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

Такие микробенчмарки вводят в заблуждение. Более приближенным к жизни был бы бенчмарк, в котором вызывались бы функции до 5-6-7 уровней вложенности, в каждой из которых по 3-4-5 if-ов для проверки кодов возврата. Да еще желательно, чтобы между if-ами были какие-то действия (тогда процессору сложнее ветвления предсказывать).

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

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

Зря тренируется или это про HTTP?

Зря про HTTP.

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

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

http://ideone.com/fyJrRI

Вот результаты:

Benchmarking "rdtsc()"
tsc: 21 22
Benchmarking "Exceptions (often error)"
tsc: 79415 11341
Benchmarking "Error codes (often error)"
tsc: 208 190
Benchmarking "Exceptions (rare error)"
tsc: 214 139
Benchmarking "Error codes (rare error)"
tsc: 149 122
Blackhole is:0

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

Исключения — по сути goto без меток.

goto на интерпретируемый код...

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

Теперь отказаться от всего, что за пределами std? Ну и упомянутые вам variant/optional/any хоть и есть в стандарте, но ещё не везде доступны.

RazrFalcon ★★★★★ ()

Хахаха :-) Вместо банальной проверки if (err == -1) в обычном сишном стиле вот это:

class is_minus_one
template<template<class> typename FailPolicy>

class ExceptionWrapper Лол :-) Это реально цепепе способствует таким изобретениям? :-) С таким подходом HTTP-сервер на цепепе будет писаться очень долго :-) Лол :-)

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

Теперь отказаться от всего, что за пределами std?

Да :-) Потому что всё остальное сомнительного качества :-) Все это понимают и изобретают свои велосипеды, начиная от функторов is_minus_one, заканчивая HTTP-серверами :-) Парадокс - люди пишут библиотеки на цепепе, но библиотеками на цепепе от других стараются не пользоваться, изобретают свои :-) Поэтому только std :-)

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

Что это за велосипед вместо православного https://github.com/google/benchmark

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

P.S. Режим работы микробенчмарка, когда весь код и данные находятся не просто в кеше, но для каждой инструкции кода закеширована микропрограмма его выполнения, а предсказатель переходов знает на 1 000 000 итераций вперед, куда ему переходить сильно отличается от режима работы кода в реальном приложении. С точки зрения микробенчмарка, например, нет разницы между виртуальной функцией и обычной.

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

Как правило, можно ввести единственное пространство ошибок и во всей программе обходиться только им, сложные ошибки пакуя в строки/доп. атрибуты. Пример от гугла — это grpc::Status / grpc::StatusCode.

Такое пространство нельзя ввести если используются сторонние библиотеки(т.е. почти всегда). Стоит ли в таком случае использовать std::variant<> со всеми возможными типами ошибок?

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

В том же goole C++ style guide написано, что

On their face, the benefits of using exceptions outweigh the costs, especially in new projects.

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

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

Т.е. парсер JSON для данных пришедших снаружи не должен бросать исключений при некорректных данных

Насколько я понял, для таких случаев следует использовать тип std::variant<json, parse_error>.

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

есть ли смысл в качестве ошибок использовать std::varian

Если вы используете C++17 - возможно.

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

Вы уверены, что вся информация об ошибке, содержащаяся в библиотеке, полезна Вашему вызывающему коду, и не получится привести их к единому виду в прослоечном коде? Те же errno почти во всех проектах можно разделить на EAGAIN / EPERM+EACCESS / ENOENT / всё остальное, что никогда не будет обрабатываться программой отдельно, но может иметь текстовое пояснение.

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

О, т.е. при #pragma once файл будет парсится только один раз?

И даже больше — он с диска будет читаться только один раз.

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

соответствует подходу golang

Зачем же косплеить убогих, когда система типов позволяет сделать по-человечески?

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

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

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

Такое пространство нельзя ввести если используются сторонние библиотеки

Что мешает?

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

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

В Golang просто нет variant, а с variant всё это становится элегантнее, особенно если использовать поверх variant шаблон вида std::expected. В Golang типичный паттерн: после возврата функции проверить `err != nil` условии if-блока обработки ошибок, так если булев оператор `std::expected` будет возвращать true только при наличии внутри корректного значения, то получится точно такая же проверка.

Для себя с исключениями я давно решил так:

  • в прикладном коде ошибка в создании файла, чтении данных и т.п. обычно фатальная и закончить крупную операцию не позволяет, поэтому легче бросить исключение и раскрутить стек, некоторые потери производительности в ненормальном пути выполнения допустимы
  • в коде демонов, серверов, компонентов ОС те же ошибки обычно терпимы и даже возможны массово, например, при нехватке ресурсов или атаке на сервер, поэтому следует из функции возвращать коды ошибок (а лучше либо код, либо значение, как с `std::expected`)

Шаблонно-функциональную магию из поста автора считаю задротством, таким банально пользоваться неудобно. Лучше переписать в явном стиле, пусть даже будет чуть-чуть дублирования кода. Вместо макроса просто объявить серию функций-обёрток, а также написать внутреннюю процедуру `throw_errno_if`, которая принимает условие и текст (чтобы знать, какой метод API привёл к выбросу). Эта процедура и сократит код функций-обёрток до 3 строк на каждую.

quiet_readonly ★★★★ ()
Последнее исправление: quiet_readonly (всего исправлений: 1)
Ответ на: комментарий от vzzo

В `std::filesystem` есть две версии каждой функции: одна бросает исключения, другая принимает out-параметр для кода ошибки. В итоге пользующийся программист может хоть с исключениями, хоть с кодами ошибок.

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

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

Это ещё более глупо. Логически как раз возвращается что-то одно: или результат или ошибка. Можно навелосипедить какой-нибудь result поверх variant, если последний смущает.

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

Имея try/catch использовать goto error. За что люблю плюсовиков, так это за верность традициям стрелять себе в ногу.

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

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

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