Дизайн обработки ошибок для С++ API
Я пишу С++ 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);
}
Я хочу сохранить дизайн простым, последовательным и чистым. Какой дизайн вы бы выбрали в качестве клиента? У вас есть другие предложения для лучшего дизайна? Можете ли вы привести некоторые причины, по которым один дизайн лучше или если это просто вопрос вкуса?
Примечание:
Здесь есть аналогичный вопрос: С++ API дизайн и обработка ошибок. Но я не хочу использовать решение в принятом ответе там или использовать что-то вроде boost:: tuple как возвращаемое значение.
Ответы
Ответ 1
Хорошо, поэтому ограничение исключений исключений создает проблему... Я не стану сомневаться в этом.
В целом, что лучше всего зависит от специфики ваших API-интерфейсов: имеют ли значения, которые вы, как правило, возвращаете, запасные значения дозорного значения, хотите ли вы заставить/побудить разработчика более явно подготовиться и справиться с кодами ошибок или иметь его более необязательный, неявный и/или краткий. Вы могли бы также рассмотреть практику других библиотек, которые вы уже используете, особенно если это не ослепительно очевидно для вызывающего объекта, из библиотеки которого задана данная функция. Поскольку нет единого наилучшего ответа на один размер, этот вопрос сохраняется в мире C после стольких десятилетий (и С++ имеет исключения).
Несколько точек обсуждения...
Значения Sentinel (в соответствии с вашими указателями в Design 1) работают очень хорошо, когда они ожидают вызывающего абонента (например, программисты, как правило, спрашивают себя: "Можно ли получить нулевой указатель, что бы это значило?" ), интуитивно понятный, согласованно, и предпочтительно, если они могут быть неявно конвертированы в booleans, тогда они принесут истину в успех! Это читается намного лучше, хотя очевидно, что многие функции библиотеки ОС и стандартного C возвращают -1 для отказа, так как 0 так часто действует в значимом результирующем наборе.
Еще одно соображение, которое вы неявно понимали: полагаясь на состояние вне функции, вы вводите проблемы безопасности потоков и асинхронных (обработки сигналов). Вы можете задокументировать, что разработчик не должен ограничивать использование объекта в потоковом/асинхронном коде или предоставлять конкретное хранилище для кодов ошибок, но это дополнительное измерение боли. Возврат или переход в объекты ErrorCode позволяет избежать этой проблемы. Передача в объектах ErrorCode становится постоянной небольшой нагрузкой на вызывающего, но побуждает их явно думать об обработке ошибок.
Ответ 2
В качестве альтернативного дизайна я бы предложил использовать унифицированный класс-оболочку для последовательного управления как возвращаемым значением, так и условием ошибки.
С С++ обычно возвращаемое значение не обрабатывается с помощью указателей или ссылочных аргументов. Если я правильно вас понимаю, вы
не хотят использовать исключения по техническим причинам, а не потому, что вы отклоняете дизайн дополнительного канала
для связи между абонентом и вызываемым абонентом.
Использование класса оболочки для возвращаемых значений будет эмулировать такой дополнительный канал, давая вам самый высокий уровень
последовательность.
(Вы также можете добавить 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;
};
Ответ 3
Я бы пошел на подход 2 по ряду причин.
- он использовался в большинстве проектов кросс-платформенного/кросс-компилятора С++, которые я видел в Интернете (включая такие большие материалы, как Gecko/Firefox/Thunderbird);
- если вы решите предложить привязки на другом языке программирования, этот дизайн не будет слишком медлительным;
- clang и g++ предлагают атрибут
warn_unused_result
, который может быть очень удобен в этом дизайне, чтобы быть абсолютно уверенным, что ошибки не игнорируются.
Ответ 4
У меня есть несколько крупных проектов, в том числе проект для предприятий и интернет-проектов, которые все обсуждали по вопросу: как использовать исключение в С++?
Когда происходит исключение, полезно иметь обработку исключений, поскольку в некотором случае, например, недопустимый запрос или пакет, не следует разбивать всю вашу службу и просто нужно поймать исключение и вернуть false. Но для других случаев вам лучше просто свернуть процесс. Исключение обработки всегда сложно, потому что вам нужно очень тщательно очистить ресурс и вернуть систему обратно в правильный статус. И обработка исключений заставит ваш код работать.
Для ваших решений, я предпочитаю 2 > 1 > 3
2 чист и прост, 1 больше похож на стиль Windows API (такой как GetErrorCode), 3 - худшее, что каждый раз, когда я должен сначала определить переменную ErrorCode.
Я предпочитаю простой и свободный интерфейс, поэтому все 3 решения не являются бегло. http://martinfowler.com/bliki/FluentInterface.html