Предисловие


Я долго думал, нужно ли делать перевод этого, уже известного, цикла статей под названием «System error support in C++0x», повествующего о <system_error> и обработке ошибок. С одной стороны он написан в 2010 году и меня попросту могут счесть некрофилом, а с другой стороны в рунете очень мало информации по этой теме и многие довольно свежие статьи ссылаются на этот цикл, что говорит о том, что он не теряет актуальности и по сей день.

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

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

Итак, приступим.

Часть 1


Введение


Среди новых функций стандартной библиотеки в C++0x есть небольшой заголовочный файл под названием <system_error>. Он предоставляет набор инструментов для управления, внезапно, системными ошибками.

Основными компонентами, определенными в нем, являются:

  • class error_category
  • class error_code
  • class error_condition
  • class system_error
  • enum class errc

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

Где взять?


Полная реализация, а также поддержка C++03, включена в Boost. Я предполагаю, что на данный момент это, вероятно, лучшая проверенная реализация с точки зрения переносимости. Разумеется, вы должны писать boost::system::, а не std::.

Реализация включена в GCC 4.4 и более поздних версиях. Однако вы должны скомпилировать свою программу с опцией -std=c++0x, чтобы ее использовать.

Наконец, Microsoft Visual Studio 2010 будет поставляться с реализацией данных классов [но с ограничениями]. Основное ограничение заключается в том, что 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() — возвращает объект категории, используемый для [классификации] кодов ошибок исходящих от операционной системы.

Принципы


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

Не все ошибки являются исключительными


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

Например, в сетевом программировании обычно встречаются такие ошибки, как:

  • Вы не смогли подключиться к удаленному IP-адресу.
  • Ваше соединение упало.
  • Вы попытались открыть IPv6-сокет, но у вас нет доступных сетевых IPv6-интерфейсов.

Конечно, они могут быть исключительными обстоятельствами, но в равной степени они могут обрабатываться и как часть нормального потока управления. Если вы разумно ожидаете, что это произойдет, это не является исключительным. Соответственно:

  • IP-адрес является лишь одним из списка адресов, соответствующих имени узла. Вы хотите попробовать подключиться к следующему адресу в списке.
  • Сеть ненадежна. Вы хотите попытаться восстановить соединение и сдаться только после N сбоев.
  • Ваша программа может вернуться к использованию IPv4-сокета.

Другим требованием, в случае 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. Будьте на связи.

Часть 2


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_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.

Часть 3


Перечисляемые значения как константы класса


Как мы видели, заголовочный файл <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, есть еще несколько шагов.

Шаг 1: определить, является ли перечисляемое значение кодом ошибки или же условием


Для регистрации типов перечислений используются два шаблона:

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>:
    public true_type {};

Неявное преобразование выполняется с помощью условно разрешенных конструкторов преобразования. Это, вероятно, реализовано с использованием SFINAE, но для простоты вам нужно думать об этом как:

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 нет.

Шаг 2: сопоставить значение ошибки с категорией ошибки


Объект error_condition содержит два атрибута: значение и категорию. Теперь, когда мы добрались до конструктора, они должны быть правильно инициализированы.

Это достигается благодаря конструктору имеющему вызов функции make_error_condition().
Возможность пользовательского расширение реализована с помощью ADL механизма. Конечно, поскольку 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_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>. Таким образом осталось только преобразование из перечисления, так как это был лучший доступный дизайн.

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

Часть 4


Создание собственных кодов ошибок


Как я уже сказал в первой части, одним из принципов <system_error> является расширяемость. Это означает, что вы можете использовать только что описанный механизм, чтобы определить свои собственные коды ошибок.

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

Шаг 1: определить значения ошибок


Сначала вам нужно определить набор значений ошибок. Если вы используете 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 так же запрещает неявное приведение перечисляемых значений к другим типам]

Шаг 2: определить класс error_category


Объект 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.

Шаг 3: дать категории человеко-читаемое имя


Виртуальная функция error_category::name() должна возвращать строку, идентифицирующую категорию:

const char * http_category_impl::name() const
{
    return "http";
}

Это имя не обязательно должно быть полностью уникальным, поскольку оно используется только при записи кода ошибки в std::ostream. Однако было бы желательно сделать его уникальным в рамках данной программы.

Шаг 4: конвертировать коды ошибок в строки


Функция 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 предпочла ничего не говорить о локализации сообщений об ошибках в стандарте.)

Шаг 5: уникальная идентификация категории


Идентификатор объекта, унаследованного от 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.

Шаг 6: построить error_code из enum


Как я показал в части 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.

Шаг 7: запись для неявного преобразования в error_code


Чтобы перечисляемые значения http_error могли использоваться как константы error_code, включите конструктор преобразования, используя шаблон is_error_code_enum:

namespace std
{
    template<>
    struct is_error_code_enum<http_error>:
        public true_type {};
}


Шаг 8 (опциональный): определить условия ошибок по умолчанию


Некоторые из описанных вами ошибок могут иметь аналогичные по смыслу условия ошибок из 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.

Часть 5


Создание собственных условий ошибок


Расширяемость модуля <system_error> не ограничена кодами ошибок: error_condition так же можно расширить.

Зачем создавать свои условия ошибок?


Чтобы ответить на этот вопрос, давайте вернемся к различиям между error_code и error_condition:

  • сlass error_code — представляет собой конкретное значение ошибки, возвращаемое операцией (например, системным вызовом).
  • class error_condition — что-то, что вы хотите проверить и, возможно, среагировать на это в своем коде.


Это предлагает некоторые варианты использования для пользовательских условий ошибок:

  • Абстракция платформозависимых ошибок.
    Предположим, вы пишете переносимую обертку вокруг getaddrinfo(). Два интересных условия ошибки: предварительная «имя не разрешается в данный момент, повторите попытку позже» и точная «имя не разрешено». Функция getaddrinfo() сообщает об этих ошибках следующим образом:

    • На платформах POSIX это ошибки EAI_AGAIN и EAI_NONAME, соответственно. Значения ошибок находятся в отдельном «пространстве имен» для значений errno. Это означает, что вам придется внедрить новую error_category для них.
    • В Windows это ошибки WSAEAI_AGAIN и WSAEAI_NONAME. Хотя имена похожи на ошибки POSIX, они разделяют «пространство имен» GetLastError(). Следовательно, вы можете повторно использовать std::system_category() для представления ошибок getaddrinfo() на этой платформе.

    Чтобы избежать утраты информации, вы хотите сохранить изначальный код платформозависимой ошибки, одновременно предоставляя два условия ошибки (называемые, скажем, name_not_found_try_again и name_not_found), которые могут быть проверены пользователями API.
  • Представление контекстозависимых значений для общих кодов ошибок.
    Большинство системных вызовов POSIX используют errno для сообщения об ошибках. Вместо того, чтобы определять новые коды ошибок для каждой функции, используются одни и те же коды, и вам может потребоваться просмотреть соответствующую страницу руководства, чтобы определить значение. Если вы реализуете свои собственные абстракции поверх этих системных вызовов, этот контекст теряется для пользователя.

    Скажем, вы хотите реализовать простую базу данных, где каждая запись хранится в отдельном файле. Когда вы пытаетесь прочитать запись, база данных вызывает 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.

Шаг 1: определить значения ошибок


Вам нужно создать enum для значений ошибок, аналогично std::errc:

enum class api_error
{
    low_system_resources = 1,
    ...
    name_not_found,
    ...
    no_such_entry
};

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

Шаг 2: определить класс error_category


Объект 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;
};

Шаг 3: дать категории человеко-читаемое имя


Виртуальная функция error_category::name() должна возвращать строку, идентифицирующую категорию:

const char * api_category_impl::name() const
{
    return "api";
}

Шаг 4: конвертировать условия ошибок в строки


Функция 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";
}

Шаг 5: реализовать эквивалентность ошибок


Виртуальная функция 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;
    }
}

Шаг 6: уникальная идентификация категории


Вы должны определить функцию для возврата ссылки на объект категории:

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;
}

Шаг 7: построить error_condition из enum


Реализация <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. Я оставлю это как упражнение для читателя.

Шаг 8: запись для неявного преобразования в error_condition


Наконец, чтобы перечисляемые значения 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>.

Послесловие


Увы, несмотря на обещания автора, цикл статей не был закончен. Следующая часть так и не вышла. И уже вряд ли выйдет.

Комментарии (10)


  1. Darell_Ldark
    26.08.2017 18:50
    +2

    Годнота! Добавил себе в закладки.


  1. Antervis
    26.08.2017 20:08
    +1

    Запоздалая, но полезная статья.

    От себя добавлю, что error_category::message не обязывает возвращать константную строку. Возможны прокси-категории, скажем, для группировки ошибок по типам, локализации, преобразования кодировок (привет Microsoft), конструирование сообщений на лету и многое другое.


    1. monah_tuk
      28.08.2017 08:16

      Из-за того, что message возвращает std::string вместо const char*, пришлось отказаться от этого удобного способа обработки ошибок на микроконтроллере. Сильно напрягает по ресурсам, когда их мало.


      1. Antervis
        28.08.2017 15:38

        все стандартные контейнеры позволяют переопределять аллокатор


        1. monah_tuk
          28.08.2017 16:17

          И что это даст? Всё равно останется аллокация и копирование. А текст описания ошибки в подавляющем числе случает вполне себе вписывается в рамки const char*.


  1. demp
    26.08.2017 20:59
    +1

    Недавно Andrzej Krzemienski опубликовал пару статей про создание собственных error_code и error_condition:
    Your own error code
    Your own error condition


    На мой взгляд, там более понятно объясняется разница между ними.


    1. Cyapa Автор
      26.08.2017 21:04

      Спасибо за наводку. Прочту на днях, как раз сам сейчас пытаюсь вникнуть во все тонкости темы.
      Возможно, переведу и эти статьи. Или, быть может, уже кто-то перевел?

      P.S.: Забавно, что этот автор тоже ссылается на переведенный здесь цикл.


  1. somov
    27.08.2017 12:39
    +1

    Увы, у технологии есть и минусы:


    1. При сравнении error_code и error_condition делается минимум один виртуальный вызов, если они эквивалентны, и минимум два, если нет. Это дорого.


    2. В libstd++ (стандартная библиотека C++ на debian) между версиями 3.4.18 и 3.4.21 есть проблемы с совместимостью. Если такие два теста собрать на Ubuntu Trusty, а запустить на Ubuntu Xenial, первый выдаст неверный результат, а второй упадёт.

    С такими минусами, в частности, столкнулись в LLVM и обходят их так:
    https://github.com/llvm-mirror/llvm/commit/803fe1968007aa7a9c4f9674af648c9840f02a11
    https://github.com/llvm-mirror/llvm/blob/master/include/llvm/Support/Errc.h


    1. Cyapa Автор
      27.08.2017 14:40

      Согласен, технология дорогая. Но за удобство абстракций всегда приходится платить, верно?

      А вот накладку в реализации стандартной библиотеке сложно относить к минусам именно самой технологии. Кстати, спасибо что поделились этой информацией, не находил упоминаний об этом ранее.


      1. somov
        27.08.2017 15:07
        +2

        Лично мне кажется, что это не те абстракции, за которые я хотел бы платить. Мне вполне достаточно статического отображения платформозависимых кодов в платформонезависимые, и не каждый раз при каждом сравнении, а один раз, при первом их появлении. Дальше можно сравнивать коды по значению, писать `switch`-и и это будет дёшево.

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

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

        В одной из своих статьей Страуструп писал:
        > In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

        Для разработчиков LLVM и для меня это место так не выглядит. Но интересно было бы увидеть примеры активного расширения system_error в настоящих проектах (boost предлагать не надо).