- PVSM.RU - https://www.pvsm.ru -
Я долго думал, нужно ли делать перевод этого, уже известного, цикла статей под названием «System error support in C++0x», повествующего о <system_error>
и обработке ошибок. С одной стороны он написан в 2010 году и меня попросту могут счесть некрофилом, а с другой стороны в рунете очень мало информации по этой теме и многие довольно свежие статьи ссылаются на этот цикл, что говорит о том, что он не теряет актуальности и по сей день.
Потому я решил, что увековечить сей труд в граните на хабре будет неплохой идеей.
Сразу хочу предупредить, что опыта переводчика у меня нет и вообще май инглиш из бед. И огорчений. Так что буду рад вашей критике и предложениям, желательно в личку.
Итак, приступим.
Среди новых функций стандартной библиотеки в C++0x есть небольшой заголовочный файл под названием <system_error>
. Он предоставляет набор инструментов для управления, внезапно, системными ошибками.
Основными компонентами, определенными в нем, являются:
class error_category
class error_code
class error_condition
class system_error
enum class errc
Я приложил руку к дизайну этого модуля, так что в серии своих статей я постараюсь рассказать о причинах появления, истории и предполагаемом использовании его компонентов.
Полная реализация, а также поддержка C++03, включена в Boost [1]. Я предполагаю, что на данный момент это, вероятно, лучшая проверенная реализация с точки зрения переносимости. Разумеется, вы должны писать boost::system::
, а не std::
.
Реализация включена в GCC [2] 4.4 и более поздних версиях. Однако вы должны скомпилировать свою программу с опцией -std=c++0x, чтобы ее использовать.
Наконец, Microsoft Visual Studio 2010 [3] будет поставляться с реализацией данных классов [но с ограничениями]. Основное ограничение заключается в том, что system_category()
не представляет ошибки Win32 так как это было задумано. Подробнее о том, что это значит будет сказано позже.
(Обратите внимание, что это только те реализации, о которых я знаю. Могут быть и другие).
[Примечание переводчика: конечно же, эта информация уже давно устарела, теперь <system_error>
является неотъемлемой частью современной стандартной библиотеки]
Ниже приведены типы и классы, определенные в <system_error>
, в двух словах:
class error_category
— базовый класс, используется для определения источников ошибок, а так же категорий кодов ошибок и условий.сlass error_code
— представляет собой конкретное значение ошибки, возвращаемое операцией (например, системным вызовом).class error_condition
— что-то, что вы хотите проверить и, возможно, среагировать на это в своем коде.class system_error
— исключение, используемое для обертывания error_code
, когда ошибка будет передана с помощью throw/catch.enum class errc
— набор общих условий ошибок, унаследованный от POSIX.is_error_code_enum<>, is_error_condition_enum<>, make_error_code, make_error_condition
— механизм преобразования перечислений с значениями ошибок в error_code
или error_condition
.generic_category()
— возвращает объект категории, используемый для классификации кодов ошибок и условий основанных на errc
.system_category()
— возвращает объект категории, используемый для [классификации] кодов ошибок исходящих от операционной системы.В этом разделе перечислены некоторые основные принципы, которых я придерживался при проектировании модуля. (Я не могу говорить за остальных участников). Как и в большинстве программных проектов, некоторые из них были целями с самого начала, а некоторые возникли в процессе.
Проще говоря, исключения — это не всегда правильный способ обработки ошибок. (В некоторых кругах это утверждение является противоречивым, хотя я действительно не понимаю, почему.)
Например, в сетевом программировании обычно встречаются такие ошибки, как:
Конечно, они могут быть исключительными обстоятельствами, но в равной степени они могут обрабатываться и как часть нормального потока управления. Если вы разумно ожидаете, что это произойдет, это не является исключительным. Соответственно:
Другим требованием, в случае asio, был способ передать результат асинхронной операции обработчику завершения. В этом случае я хочу, чтобы код ошибки был аргументом для обработчика обратного вызова.
(Альтернативный подход заключается в том, чтобы предоставить средство для реконструирования исключения внутри обработчика, такого как асинхронный шаблон .NET BeginXYZ/EndXYZ
. На мой взгляд, такой дизайн добавляет сложности и делает API более подверженным ошибкам.)
[Примечание переводчика: теперь таким средством может быть std::exception_ptr
из C++11]
И последнее, но не менее важное: в некоторых областях нельзя использовать исключения из-за размера кода и ограничений производительности.
В общем, нужно быть прагматичным, а не догматичным. Использовать любой механизм ошибок лучше всего с точки зрения ясности, правильности, ограничений и, да, даже личного вкуса. Часто правильным критерием для принятия решения между исключением и кодом ошибки является способ использования. Это означает, что представление системной ошибки должно поддерживать оба [варианта].
Стандарт C++03 распознает errno как источник кодов ошибок. Это используется функциями stdio, некоторыми математическими функциями и так далее.
На POSIX платформах многие системные операции используют errno для передачи ошибок. POSIX определяет дополнительные коды ошибок errno для покрытия этих случаев.
Windows, с другой стороны, не использует errno за пределами стандартной библиотеки C. Вызовы Windows API обычно сообщают об ошибках через GetLastError()
.
Если рассматривать сетевое программирование, семейство функций getaddrinfo
использует собственный набор кодов ошибок (EAI_...) на POSIX, но разделяет «пространство имен» GetLastError()
в Windows. Программы, интегрирующие другие библиотеки (для SSL, регулярных выражений и так далее), столкнутся с другими категориями кодов ошибок.
Программы должны иметь возможность управлять этими кодами ошибок согласованным образом. Я особенно заинтересован тем [способом], что позволит сочетать операции для создания абстракций более высокого уровня. Объединение системных вызовов, getaddrinfo
, SSL и регулярных выражений в один API не должно заставлять пользователя этого API бороться со «взрывом» типов кодов ошибок. Добавление нового источника ошибок в реализацию этого API не должно изменять его интерфейс.
Пользователи стандартной библиотеки должны иметь возможность добавлять свои собственные источники ошибок. Эта возможность может быть использована просто для интеграции сторонней библиотеки, но также она связана с желанием создавать абстракции более высокого уровня. При разработке реализации протокола, такого как HTTP, я хочу иметь возможность добавлять набор кодов ошибок, соответствующих ошибкам, определенным в RFC.
Это не было одной из моих первоначальных целей: я думал, что стандарт предоставит набор хорошо известных кодов ошибок. Если системная операция вернула ошибку, библиотека должна была перевести ошибку в хорошо известный код (если такое отображение имело смысл).
К счастью, кто-то указал мне на проблему моего подхода. Перевод кода ошибки сбрасывает информацию: ошибка, возвращаемая основным системным вызовом, теряется. Это может быть не очень важно с точки зрения потока управления программой, но это имеет большое значение для поддержки программы. Нет никаких сомнений в том, что программисты будут использовать стандартизованный код ошибки для логирования и трассировки, а изначальная ошибка может быть жизненно важна для диагностики проблем.
Этот окончательный принцип прекрасно сочетается с моей темой для второй части: error_code
vs error_condition
. Будьте на связи.
Из 1000+ страниц стандарта C++0x случайный читатель должен подметить одну вещь: error_code и error_condition выглядят практически идентичными! Что происходит? Это последствия бездумной копипасты?
Давайте посмотрим на описания, которые я давал в первой части, еще раз:
сlass error_code
— представляет собой конкретное значение ошибки, возвращаемое операцией (например, системным вызовом).class error_condition
— что-то, что вы хотите проверить и, возможно, среагировать на это в своем коде.Классы различаются, потому что они предназначены для разных целей. В качестве примера рассмотрим гипотетическую функцию под названием create_directory()
:
void create_directory
(
const std::string & pathname,
std::error_code & ec
);
Которую вы вызываете следующим образом:
std::error_code ec;
create_directory("/some/path", ec);
Операция может завершиться неудачей по разным причинам, например:
Какова бы ни была причина сбоя, когда функция create_directory()
вернет управление переменная ec
будет содержать код ошибки, специфичный для ОС. С другой стороны, если вызов был успешным, то в ec
будет находиться нулевое значение. Это дань традиции (используемой errno и GetLastError()
), когда ноль указывает на успех, а любые другие значения на ошибку.
Если вас интересует только была ли операция успешной или неудачной, вы можете использовать тот факт, что error_code
легко конвертируется в bool
:
std::error_code ec;
create_directory("/some/path", ec);
if(!ec)
{
//Success.
}
else
{
//Failure.
}
Однако предположим, что вы заинтересованы в проверке ошибки «каталог уже существует». Если случится эта ошибка, то наша гипотетическая программа может продолжать работать. Давайте попробуем реализовать это:
std::error_code ec;
create_directory("/some/path", ec);
if(ec.value() == EEXIST) //No!
...
Этот код неправильный. Он может заработать на POSIX платформах, но не забывайте, что ec
будет содержать платформозависимую ошибку. В Windows ошибка, скорее всего, будет ERROR_ALREADY_EXISTS
. (Хуже того, код не проверяет категорию кода ошибки, но мы поговорим об этом позже.)
Практическое правило: если вы вызываете error_code::value()
, то вы делаете что-то не так.
Итак у вас есть платформозависимый код ошибки (EEXIST
или ERROR_ALREADY_EXISTS
), который вы хотите сопоставить с [платформонезависимым] условием ошибки («каталог уже существует»). Да, правильно, вам нужен error_condition
.
Вот что происходит при сравнении объектов error_code
и error_condition
(то есть при использовании оператора == или оператора !=):
error_code
и error_code
— проверяется точное соответствие.error_condition
и error_condition
— проверяется точное соответствие.error_code
и error_condition
— проверяется эквивалентность.Я надеюсь, теперь очевидно, что вы должны сравнивать свой платформозависимый код ошибки ec
с объектом error_condition
, который представляет ошибку «каталог уже существует». Как раз для такого случая C++0x предоставляет std::errc::file_exists
. Это означает, что вы должны писать:
std::error_code ec;
create_directory("/some/path", ec);
if(std::errc::file_exists == ec)
...
Это работает потому что разработчик стандартной библиотеки определил эквивалентность между кодами ошибок EEXIST
или ERROR_ALREADY_EXISTS
и условием ошибки std::errc::file_exists
. Позже я покажу как вы можете добавить свои собственные коды ошибок и условия с соответствующими определениями эквивалентности.
(Обратите внимание, что, если быть точным, std::errc::file_exists
это одно из перечисляемых значений из enum class errc
. Пока что вы должны думать о перечисляемых значениях std::errc::*
как о метках для констант error_condition
. В следующей части я объясню как это работает.)
Некоторые из новых библиотечных функций в C++0x имеют раздел «Условия ошибок». В подобных разделах перечисляются константы error_condition
и условия, при которых генерируются эквивалентные коды ошибок.
Первоначальный класс error_code
был предложен для TR2 как вспомогательный компонент в библиотеках файловой системы и сетевых библиотеках. В том дизайне константа error_code
была реализована так, чтобы она, по возможности, соответствовала платформозависимой ошибке. Если соответствие невозможно или существует несколько соответствий, реализация библиотеки переводит платформозависимую ошибку в стандартный error_code
.
В обсуждениях по электронной почте я узнал о ценности сохранения изначального кода ошибки. Впоследствии был прототипирован класс generic_error
, но он меня не устраивал. Удовлетворительное решение было найдено при переименовании generic_error
в error_condition
. По моему опыту, именование — одна из самых сложных проблем в области компьютерных наук, и выбор хорошего имени является основной работой.
В следующей части мы рассмотрим механизм, который заставляет enum class errc
работать как набор констант для error_condition
.
Как мы видели, заголовочный файл <system_error>
определяет class enum errc
следующим образом:
enum class errc
{
address_family_not_supported,
address_in_use,
...
value_too_large,
wrong_protocol_type,
};
Перечисляемые значения которого являются константами для error_condition
:
std::error_code ec;
create_directory("/some/path", ec);
if(std::errc::file_exists == ec)
...
Очевидно, здесь используется неявное преобразование из errc
в error_condition
с помощью конструктора с одним аргументом. Просто. Верно?
Есть несколько причин, из-за которых всё немного сложнее:
error_condition
необходимо знать еще и категорию ошибки. Модуль <system_error>
использует категории для поддержки нескольких источников ошибок. Категория является атрибутом как для error_code
, так и для error_condition
.error_code
и для error_condition
. Хотя enum class errc
предоставляет константы только для error_condition
, в других случаях использования могут потребоваться константы типа error_code
.error_code
или error_condition
. Портируемым программам может потребоваться создание кодов ошибок, унаследованных от std::errc::*
.Итак, хотя верно, что строка:
if(std::errc::file_exists == ec)
неявно преобразуется из errc
в error_condition
, есть еще несколько шагов.
Для регистрации типов перечислений используются два шаблона:
template<class T>
struct is_error_code_enum:
public false_type {};
template<class T>
struct is_error_condition_enum:
public false_type {};
Если тип зарегистрирован с использованием is_error_code_enum<>
, то он может быть неявно преобразован в error_code
. Аналогично, если тип зарегистрирован с использованием is_error_condition_enum<>
, он может быть неявно преобразован в error_condition
. По умолчанию типы регистрируются без преобразования (отсюда и использование false_type
выше), но enum class errc
зарегистрирован следующим образом:
template<>
struct is_error_condition_enum<errc>
: true_type {};
Неявное преобразование выполняется с помощью условно разрешенных конструкторов преобразования. Это, вероятно, реализовано с использованием SFINAE [4], но для простоты вам нужно думать об этом как:
class error_condition
{
...
//Only available if registered
//using is_error_condition_enum<>.
template<class ErrorConditionEnum>
error_condition(ErrorConditionEnum e);
...
};
class error_code
{
...
//Only available if registered
//using is_error_code_enum<>.
template<class ErrorCodeEnum>
error_code(ErrorCodeEnum e);
...
};
Поэтому, когда мы пишем:
if(std::errc::file_exists == ec)
Компилятор выбирает между этими двумя перегрузками:
bool operator ==
(
const error_code & a,
const error_code & b
);
bool operator ==
(
const error_code & a,
const error_condition & b
);
Он выберет последний, поскольку конструктор преобразования error_condition
доступен, а error_code
нет.
Объект error_condition
содержит два атрибута: значение и категорию. Теперь, когда мы добрались до конструктора, они должны быть правильно инициализированы.
Это достигается благодаря конструктору имеющему вызов функции make_error_condition()
.
Возможность пользовательского расширение реализована с помощью ADL [5] механизма. Конечно, поскольку errc
расположен в пространстве имен std
, ADL находит make_error_condition()
там же.
Реализация make_error_condition()
проста:
error_condition make_error_condition(errc e)
{
return error_condition
(
static_cast<int>(e),
generic_category()
);
}
Как вы можете видеть, эта функция использует конструктор error_condition
с двумя аргументами, чтобы явно указать как значение ошибки, так и категорию.
Если бы мы были в конструкторе преобразования error_code
(для правильно зарегистрированного типа перечисления), вызываемая функция была бы make_error_code()
. В остальном конструкция error_code
и error_condition
одинакова.
Хотя error_code
в первую очередь предназначен для использования с платформозависимыми ошибками, переносимый код может захотеть создать error_code
из перечисляемого значения errc
. По этой причине предусмотрены [функции] make_error_code(errc)
и make_error_condition(errc)
. Переносимый код может использовать их следующим образом:
void do_foo(std::error_code & ec)
{
#if defined(_WIN32)
//Windows implementation ...
#elif defined(linux)
//Linux implementation ...
#else
//do_foo not supported on this platform
ec = make_error_code(std::errc::not_supported);
#endif
}
Изначально в <system_error>
константы error_code
были определены как объекты:
extern error_code address_family_not_supported;
extern error_code address_in_use;
...
extern error_code value_too_large;
extern error_code wrong_protocol_type;
LWG была обеспокоена издержками из-за большого количества глобальных объектов и запросила альтернативное решение. Мы исследовали возможность использования constexpr
, но в итоге это оказалось несовместимым с некоторыми другими аспектами <system_error>
. Таким образом осталось только преобразование из перечисления, так как это был лучший доступный дизайн.
Далее я начну показывать, каким образом вы можете добавить свои собственные коды ошибок и условия.
Как я уже сказал в первой части, одним из принципов <system_error>
является расширяемость. Это означает, что вы можете использовать только что описанный механизм, чтобы определить свои собственные коды ошибок.
В этом разделе я опишу, что вам нужно сделать. В качестве основы для рабочего примера предположим, что вы пишете HTTP библиотеку и вам нужны ошибки, соответствующее кодам состояния HTTP [6].
Сначала вам нужно определить набор значений ошибок. Если вы используете C++0x, вы можете использовать class enum
, аналогичный std::errc
:
enum class http_error
{
continue_request = 100,
switching_protocols = 101,
ok = 200,
...
gateway_timeout = 504,
version_not_supported = 505
};
Ошибкам присваиваются значения в соответствии с кодами состояния HTTP. Важность этого станет очевидной, когда дело дойдет до использования кодов ошибок. Независимо от того, какие значения вы выберете, ошибки должны иметь ненулевые значения. Как вы помните, объект <system_error>
использует соглашение, в котором нуль означает успех.
Вы можете использовать обычный (то есть C++03-совместимый) enum
, отбросив ключевое слово class
:
enum http_error
{
...
};
Примечание: class enum
отличается от enum
тем, что первый заключает имена перечисляемых значений в классовой области видимости [в то время как второй «выбрасывает» их в глобальную область видимости]. Чтобы получить доступ к перечисляемому значению, вы должны указать имя класса, например: http_error::ok
. Вы можете эмулировать это поведение, обернув обычный enum
в пространство имен [namespace
]:
namespace http_error
{
enum http_error_t
{
...
};
}
В оставшейся части этого примера я буду использовать enum class
. Применение подхода, использующего пространство имен, остается в качестве упражнения для читателя.
[Примечание переводчика: на самом деле, они отличаются не только областью видимости — enum class
так же запрещает неявное приведение перечисляемых значений к другим типам]
Объект error_code
состоит из значения ошибки и категории. Категория ошибки определяет, что конкретно означает данное перечисляемое значение. Например, 100 может означать как http_error::continue_request
, так и std::errc::network_down
(ENETDOWN в Linux), а может и что-то другое.
Чтобы создать новую категорию, нужно отнаследовать класс от error_category
:
class http_category_impl:
public std::error_category
{
public:
virtual const char * name() const;
virtual std::string message(int ev) const;
};
На данный момент этот класс будет реализовывать только чистые виртуальные функции error_category
.
Виртуальная функция error_category::name()
должна возвращать строку, идентифицирующую категорию:
const char * http_category_impl::name() const
{
return "http";
}
Это имя не обязательно должно быть полностью уникальным, поскольку оно используется только при записи кода ошибки в std::ostream
. Однако было бы желательно сделать его уникальным в рамках данной программы.
Функция error_category::message()
преобразует значение ошибки в описывающую её строку:
std::string http_category_impl::message(int ev) const
{
switch(ev)
{
case http_error::continue_request:
return "Continue";
case http_error::switching_protocols:
return "Switching protocols";
case http_error::ok:
return "OK";
...
case http_error::gateway_timeout:
return "Gateway time-out";
case http_error::version_not_supported:
return "HTTP version not supported";
default:
return "Unknown HTTP error";
}
}
Когда вы вызываете функцию error_code::message()
, error_code
, в свою очередь, вызывает указанную выше виртуальную функцию для получения сообщения об ошибке.
Важно помнить, что эти сообщения об ошибках должны быть автономными. Они могут быть записаны (в файл лога, скажем) в той точке программы, где дополнительный контекст не доступен. Если вы обертываете существующий API, который использует сообщения об ошибках с «вставками», вам придется создавать свои собственные сообщения. Например, если HTTP API использует строку сообщения "HTTP version %d.%d not supported"
, эквивалентное автономное сообщение будет "HTTP version not supported"
.
Модуль <system_error>
не предоставляет никакой помощи, когда дело доходит до локализации этих сообщений. Вполне вероятно, что сообщения, исходящие из категорий ошибок стандартной библиотеки, будут основаны на текущей локали. Если в вашей программе требуется локализация, я рекомендую использовать тот же подход.
(Немного истории: LWG осознавала необходимость локализации, но не было никакого дизайна, который удовлетворительно согласовал локализацию с расширяемостью. Вместо того, чтобы собирать комитеты для решения этого вопроса, LWG предпочла ничего не говорить о локализации сообщений об ошибках в стандарте.)
Идентификатор объекта, унаследованного от error_category
, определяется его адресом. Это означает, что когда вы пишете:
const std::error_category & cat1 = ...;
const std::error_category & cat2 = ...;
if(cat1 == cat2)
...
Условие if
оценивается так, как если бы вы написали:
if(&cat1 == &cat2)
...
Следуя примеру, установленному стандартной библиотекой, вы должны предоставить функцию для возврата ссылки на объект категории:
const std::error_category & http_category();
Эта функция всегда должна возвращать ссылку на один и тот же объект. Один из способов это сделать — определить глобальный объект в файле исходного кода и возвращать ссылку на него:
http_category_impl http_category_instance;
const std::error_category & http_category()
{
return http_category_instance;
}
Тем не менее, глобальные переменные вызывают проблемы, связанные с порядком инициализации между модулями. Альтернативный подход заключается в использовании локальной статической переменной:
const std::error_category & http_category()
{
static http_category_impl instance;
return instance;
}
В этом случае объект категории инициализируется при первом использовании. C++0x также гарантирует, что инициализация потокобезопасна. (C++03 не давал такой гарантии).
История: На ранних этапах проектирования мы рассматривали использование целого числа или строки для идентификации категорий. Основная проблема с этим подходом заключалась в обеспечении уникальности в сочетании с расширяемостью. Если категория будет идентифицирована целым числом или строкой, что предотвратит коллизии между двумя несвязанными библиотеками? Using object identity leverages the linker in preventing different categories from having the same identity. Furthermore, storing a pointer to a base class allows us to make error_codes polymorphic while keeping them as copyable value types.
Как я показал в части 3, реализация <system_error>
требует функцию с названием make_error_code()
, чтобы связать значение ошибки с категорией. Для ошибок HTTP эта функция могла бы выглядеть следующим образом:
std::error_code make_error_code(http_error e)
{
return std::error_code
(
static_cast<int>(e),
http_category()
);
}
Для полноты картины вы также должны предоставить эквивалентную функцию для построения error_condition
:
std::error_condition make_error_condition(http_error e)
{
return std::error_condition
(
static_cast<int>(e),
http_category()
);
}
Поскольку реализация <system_error>
находит эти функции используя ADL, вы должны поместить их в то же пространство имен, что и тип http_error
.
Чтобы перечисляемые значения http_error
могли использоваться как константы error_code
, включите конструктор преобразования, используя шаблон is_error_code_enum
:
namespace std
{
template<>
struct is_error_code_enum<http_error>:
public true_type {};
}
Некоторые из описанных вами ошибок могут иметь аналогичные по смыслу условия ошибок из errc
. Например, код состояния HTTP 403 Forbidden означает то же самое, что и std::errc::permission_denied
.
Виртуальная функция error_category::default_error_condition()
позволяет определить условие ошибки, эквивалентное данному коду ошибки. (Определение эквивалентности было описано во второй части.) Для ошибок HTTP вы можете написать:
class http_category_impl:
private std::error_category
{
public:
...
virtual std::error_condition default_error_condition(int ev) const;
};
...
std::error_condition http_category_impl::default_error_condition(int ev) const
{
switch(ev)
{
case http_error::forbidden:
return std::errc::permission_denied;
default:
return std::error_condition(ev, *this);
}
}
Если вы решите не переопределять эту виртуальную функцию, то error_condition
будет имеет такую же ошибку и категорию как и error_code
. Это поведение по умолчанию, как в примере, показанном выше.
Теперь вы можете использовать перечисляемые значения http_error
как константы error_code
, как при установке ошибки:
void server_side_http_handler
(
...,
std::error_code & ec
)
{
...
ec = http_error::ok;
}
так и при ее проверке:
std::error_code ec;
load_resource("http://some/url", ec);
if(http_error::ok == ec)
...
[Примечание переводчика: следует заметить, что при такой реализации не будет работать принцип о котором сказано выше — нулевое значение = успех — соответственно, приведение к bool
тоже не будет работать]
Поскольку значения ошибок основаны на кодах состояния HTTP, мы также можем установить error_code
непосредственно из ответа:
std::string load_resource
(
const std::string & url,
std::error_code & ec
)
{
//send request
...
//receive response
...
int response_code;
parse_response(..., &response_code);
ec.assign(response_code, http_category());
...
}
Кроме того, вы можете использовать этот метод при обертке ошибок, создаваемых уже существующей библиотекой.
Наконец, если вы определили отношение эквивалентности на шаге 8, вы можете написать:
std::error_code ec;
data = load_resource("http://some/url", ec);
if(std::errc::permission_denied == ec)
...
без необходимости знать точный источник условия ошибки. Как поясняется в части 2, изначальный код ошибки (например, http_error::forbidden
) сохраняется, так что никакая информация не теряется.
В следующей части я покажу как создавать и использовать error_condition
.
Расширяемость модуля <system_error>
не ограничена кодами ошибок: error_condition
так же можно расширить.
Чтобы ответить на этот вопрос, давайте вернемся к различиям между error_code
и error_condition
:
сlass error_code
— представляет собой конкретное значение ошибки, возвращаемое операцией (например, системным вызовом).class error_condition
— что-то, что вы хотите проверить и, возможно, среагировать на это в своем коде.Это предлагает некоторые варианты использования для пользовательских условий ошибок:
getaddrinfo()
. Два интересных условия ошибки: предварительная «имя не разрешается в данный момент, повторите попытку позже» и точная «имя не разрешено». Функция getaddrinfo()
сообщает об этих ошибках следующим образом:
EAI_AGAIN
и EAI_NONAME
, соответственно. Значения ошибок находятся в отдельном «пространстве имен» для значений errno. Это означает, что вам придется внедрить новую error_category
для них.WSAEAI_AGAIN
и WSAEAI_NONAME
. Хотя имена похожи на ошибки POSIX, они разделяют «пространство имен» GetLastError()
. Следовательно, вы можете повторно использовать std::system_category()
для представления ошибок getaddrinfo()
на этой платформе.Чтобы избежать утраты информации, вы хотите сохранить изначальный код платформозависимой ошибки, одновременно предоставляя два условия ошибки (называемые, скажем, name_not_found_try_again
и name_not_found
), которые могут быть проверены пользователями API.
Скажем, вы хотите реализовать простую базу данных, где каждая запись хранится в отдельном файле. Когда вы пытаетесь прочитать запись, база данных вызывает open()
для доступа к файлу. Эта функция устанавливает errno в ENOENT
, если файл не существует.
Поскольку механизм хранения базы данных абстрагируется от пользователя, было бы удивительно просить их проверять условие no_such_file_or_directory
. Вместо этого вы можете создать собственное контекстозависимое условие ошибки no_such_entry
, эквивалентное ENOENT
.
not_enough_memory
resource_unavailable_try_again
too_many_files_open
too_many_files_open_in_system
в нескольких местах, но последующее действие отличается в каждой точке использования. Это показывает, что существует более общее условие: «не хватает системных ресурсов», которое вы хотите проверить и отреагировать на него в своем коде.
Пользовательское условие ошибки low_system_resources
может быть определено таким образом, чтобы его эквивалентность основывалась на сочетании других условий ошибки. Это позволит писать вам проверки следующим образом:
if(low_system_resources == ec)
...
и таким образом исключить повторение отдельных проверок.
Как вы увидите ниже, определение error_condition
аналогично определению error_code
.
Вам нужно создать enum
для значений ошибок, аналогично std::errc
:
enum class api_error
{
low_system_resources = 1,
...
name_not_found,
...
no_such_entry
};
Фактические значения, которые вы используете, не важны, но вы должны убедиться, что они различны и отличны от нуля.
Объект error_condition
состоит из значения ошибки и категории. Чтобы создать новую категорию, нужно отнаследовать класс от error_category
:
class api_category_impl:
public std::error_category
{
public:
virtual const char * name() const;
virtual std::string message(int ev) const;
virtual bool equivalent(const std::error_code & code, int condition) const;
};
Виртуальная функция error_category::name()
должна возвращать строку, идентифицирующую категорию:
const char * api_category_impl::name() const
{
return "api";
}
Функция error_category::message()
преобразует значение ошибки в описывающую её строку:
std::string api_category_impl::message(int ev) const
{
switch(ev)
{
case api_error::low_system_resources:
return "Low system resources";
..
}
}
Однако, в зависимости от вашего варианта использования, вызов error_condition::message()
может быть маловероятным. В этом случае вы можете воспользоваться сокращением и просто написать:
std::string api_category_impl::message(int ev) const
{
return "api error";
}
Виртуальная функция error_category::equivalent()
используется для определения эквивалентности кодов ошибок и условий. Есть две перегрузки этой функции. Первая:
virtual bool equivalent(int code, const error_condition & condition) const;
используется для установления эквивалентности между error_code
в текущей категории и произвольными error_condition
. Вторая перегрузка:
virtual bool equivalent(const error_code & code, int condition) const;
определяет эквивалентность между error_condition
в текущей категории и произвольными error_code
. Поскольку вы создаете условия ошибки, вам нужно переопределить вторую перегрузку.
Определение эквивалентности простое: верните true
, если вы хотите, чтобы error_code
был эквивалентен вашему условию, иначе верните false
.
Если вы намерены абстрагироваться от платформозависимых ошибок, вы можете реализовать error_category::equivalent()
следующим образом:
bool api_category_impl::equivalent(const std::error_code & code, int condition) const
{
switch(condition)
{
...
case api_error::name_not_found:
#if defined(_WIN32)
return code == std::error_code(WSAEAI_NONAME, system_category());
#else
return code == std::error_code(EAI_NONAME, getaddrinfo_category());
#endif
...
default:
return false;
}
}
(Очевидно, что getaddrinfo_category()
тоже нужно где-то определить.)
Проверки могут комплексными, а так же могут повторно использовать другие константы error_condition
:
bool api_category_impl::equivalent(const std::error_code & code, int condition) const
{
switch(condition)
{
case api_error::low_system_resources:
return code == std::errc::not_enough_memory
|| code == std::errc::resource_unavailable_try_again
|| code == std::errc::too_many_files_open
|| code == std::errc::too_many_files_open_in_system;
...
case api_error::no_such_entry:
return code == std::errc::no_such_file_or_directory;
default:
return false;
}
}
Вы должны определить функцию для возврата ссылки на объект категории:
const std::error_category & api_category();
которая всегда возвращает ссылку на один и тот же объект. Как и в случае с кодами ошибок, вы можете использовать глобальную переменную:
api_category_impl api_category_instance;
const std::error_category & api_category()
{
return api_category_instance;
}
или вы можете использовать статические потокобезопасные переменные из C++0x:
const std::error_category & api_category()
{
static api_category_impl instance;
return instance;
}
Реализация <system_error>
требует функцию с названием make_error_code()
, чтобы связать значение ошибки с категорией:
std::error_condition make_error_condition(api_error e)
{
return std::error_condition
(
static_cast<int>(e),
api_category()
);
}
Для полноты картины вам также необходимо определить эквивалентную функцию для построения error_code
. Я оставлю это как упражнение для читателя.
Наконец, чтобы перечисляемые значения api_error
могли использоваться как константы error_condition
, включите конструктор преобразования, используя шаблон is_error_condition_enum
:
namespace std
{
template<>
struct is_error_condition_enum<api_error>:
public true_type {};
}
Теперь перечисляемые значения api_error
могут использоваться как константы error_condition
, так же как те, которые определены в std::errc
:
std::error_code ec;
load_resource("http://some/url", ec);
if(api_error::low_system_resources == ec)
...
Как я уже несколько раз говорил, изначальный код ошибки сохраняется и информация не теряется. Не имеет значения, пришел ли этот код ошибки из операционной системы или из библиотеки HTTP со своей собственной категорией ошибок. Ваши пользовательские условия ошибки смогут одинаково хорошо работать в любом случае.
В следующей, вероятно, последней, части я расскажу как создавать API, которые используют <system_error>
.
Увы, несмотря на обещания автора, цикл статей не был закончен. Следующая часть так и не вышла. И уже вряд ли выйдет.
Автор: zumm
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/262808
Ссылки в тексте:
[1] Boost: http://www.boost.org/
[2] GCC: http://gcc.gnu.org/
[3] Microsoft Visual Studio 2010: https://www.microsoft.com/ru-ru/SoftMicrosoft/VisualStudioExpress.aspx
[4] SFINAE: https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error
[5] ADL: https://en.wikipedia.org/wiki/Argument-dependent_name_lookup
[6] кодам состояния HTTP: https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BA%D0%BE%D0%B4%D0%BE%D0%B2_%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D1%8F_HTTP
[7] Источник: https://habrahabr.ru/post/336012/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.