Дизайн обработки ошибок для API C ++ - PullRequest
15 голосов
/ 17 февраля 2012

Я пишу C ++ API для Windows с VS 2010, который экспортирует несколько классов из DLL. Мы планируем поддерживать другие платформы позже (MacOS, Linux).

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

До сих пор я придумал следующие три дизайна.

Дизайн 1:

Для каждого метода возвращаемое значение будет указывать, была ли операция успешной или неудачной, возвращая либо true/false, либо pointer/nullptr соответственно. Затем клиент может вызвать GetLastError() для получения кода ошибки (enum), который подробно описывает последний сбой.

typedef std::shared_ptr<Object> ObjectPtr;

class APIClass
{
    bool SetSomething(int i);
    bool IsSomethingSet();
    bool DoSomething();
    ObjectPtr GetSomething();

    ErrorCode GetLastError();
}

Дизайн 2:

Каждый метод возвращает код ошибки. Параметры [out] должны передаваться как указатели, а параметры [in] - по значению или по (const) ссылке.

typedef std::shared_ptr<Object> ObjectPtr;

class APIClass
{
    ErrorCode SetSomething(int i);
    ErrorCode IsSomethingSet(bool* outAsk);
    ErrorCode DoSomething();
    ErrorCode GetSomething(ObjectPtr* outObj);
}

Дизайн 3:

Как и дизайн 1, но вы можете передать необязательный код ошибки в качестве параметра [out] для каждой функции.

typedef std::shared_ptr<Object> ObjectPtr;

class APIClass
{
    bool SetSomething(int i, ErrorCode* err = nullptr);
    bool IsSomethingSet(ErrorCode* err = nullptr);
    bool DoSomething(ErrorCode* err = nullptr);
    ObjectPtr GetSomething(ErrorCode* err = nullptr);
}

Я хочу, чтобы дизайн был простым, последовательным и чистым. Какой дизайн вы бы предпочли в качестве клиента? У вас есть другие предложения по улучшению дизайна? Можете ли вы объяснить, почему один дизайн лучше или это просто вопрос вкуса?

Примечание: Здесь есть похожий вопрос: Разработка C ++ API и обработка ошибок . Но я не хочу использовать решение в принятом ответе или использовать что-то вроде boost :: tuple в качестве возвращаемого значения.

Ответы [ 4 ]

5 голосов
/ 17 февраля 2012

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

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

Несколько вопросов для обсуждения ...

Значения Sentinel (в соответствии с вашими указателями в Design 1) работают довольно хорошо, когда их ожидает вызывающий (например, программисты, как правило, задают себе вопрос: «Могу ли я получить нулевой указатель? Что бы это значило?»), Интуитивно понятный, непротиворечивы, и предпочтительно, если они могут быть неявно преобразованы в логические значения, тогда они дадут истину в случае успеха! Это выглядит намного лучше, хотя очевидно, что многие функции ОС и стандартных библиотек С возвращают -1 при сбое, так как 0 часто допустимо в значимом наборе результатов.

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

1 голос
/ 25 августа 2016

Я бы выбрал подход 2 по ряду причин.

  • он используется в большинстве кросс-платформенных / кросс-компиляторных проектов C ++, которые я видел в Интернете (включая такие большие вещи, какGecko / Firefox / Thunderbird);
  • , если вы решите предложить привязки на другом языке программирования, этот дизайн не будет слишком мешать;
  • clang и g ++ предлагают атрибут warn_unused_result это может быть очень удобно с этим дизайном, чтобы быть абсолютно уверенным, что ошибки не игнорируются.
1 голос
/ 25 августа 2016

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

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

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

(Вы также можете добавить operator T(), но это может сбить с толку в тех случаях, когда в качестве возвращаемого значения указывается bool)

template<typename T>
class ReturnValue
{
    public:
    ReturnValue(T&& value, bool succeeded, int error_code):
        value_{std::move(value)},
        succeeded_{succeeded},
        error_code_{error_code}
    /* Some more constructors possibly... */

    T& value() noexcept { return value_; }
    T const& value() const noexcept { return value_; }
    bool succeeded() const noexcept { return succeeded_; }
    operator bool() const noexcept { return succeeded_; }
    int error_code() const noexcept { return error_code_; }

    private:
    T value_;
    bool succeeded_=false;
    int error_code_=0;
};

template<>
class ReturnValue<void>
{
    public:
    ReturnValue(bool succeeded, int error_code):
        succeeded_{succeeded},
        error_code_{error_code}

    bool succeeded() const noexcept { return succeeded_; }
    operator bool() const noexcept { return succeeded_; }
    int error_code() const noexcept { return error_code_; }

    private:
    bool succeeded_=false;
    int error_code_=0;
};
0 голосов
/ 25 августа 2016

У меня есть опыт нескольких крупных проектов, включая корпоративный проект и интернет-проект, в которых все обсуждали вопрос: как использовать исключение в C ++?

Когда происходит исключение, для некоторых полезно иметь обработку исключений.Случай, например, неверный запрос или пакет, не должен привести к сбою всей службы, а просто должен перехватить исключение и вернуть false.Но в других случаях вам лучше напрямую прервать процесс.Обработка исключений всегда трудна, потому что вы должны очень тщательно очистить ресурс и вернуть систему в правильное состояние.А обработка исключений сделает ваш код трудным для поддержки.

Для ваших решений я предпочитаю 2> 1> 3

2 - это чисто и просто, 1 больше похоже на стиль Windows API (GetErrorCodeвот так), 3 худшее, что каждый раз, когда мне нужно сначала определить переменную ErrorCode.

Я предпочитаю простой и удобный интерфейс, поэтому все 3 решения не являются свободными.http://martinfowler.com/bliki/FluentInterface.html

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...