Если нельзя бросать исключения, используйте коды ошибок систематически
Причина
Систематическое использование любой стратегии обработки ошибок минимизирует вероятность забыть обработать ошибку.
Смотрите также: Симуляция RAII
Примечание
Необходимо решить несколько вопросов:
- Как передать индикатор ошибки из функции?
- Как освободить все ресурсы из функции перед выходом с ошибкой?
- Что использовать в качестве индикатора ошибки?
В общем, возврат индикатора ошибки предполагает возврат двух значений: результата и индикатора ошибки. Индикатор ошибки может быть частью объекта, например объект может иметь индикатор valid(), или можно вернуть пару значений.
Пример
Gadget make_gadget(int n)
{
// ...
}
void user()
{
Gadget g = make_gadget(17);
if (!g.valid()) {
// обработка ошибки
}
// ...
}
Этот подход согласуется с симулированным RAII управлением ресурсами. Функция valid() могла бы возвращать error_indicator (например, член перечисления error_indicator).
Пример
Что если мы не можем или не хотим изменять тип Gadget? В таком случае нам нужно вернуть пару значений. Например:
std::pair<Gadget, error_indicator> make_gadget(int n)
{
// ...
}
void user()
{
auto r = make_gadget(17);
if (!r.second) {
// обработка ошибки
}
Gadget& g = r.first;
// ...
}
Как показано, std::pair является возможным типом возврата. Некоторые предпочитают конкретный тип. Например:
Gval make_gadget(int n)
{
// ...
}
void user()
{
auto r = make_gadget(17);
if (!r.err) {
// обработка ошибки
}
Gadget& g = r.val;
// ...
}
Одна из причин предпочесть конкретный тип возврата — иметь имена для его членов, а не несколько загадочные first и second, и избежать путаницы с другими применениями std::pair.
Пример
В общем, необходимо выполнить очистку перед выходом с ошибкой. Это может быть запутанным:
std::pair<int, error_indicator> user()
{
Gadget g1 = make_gadget(17);
if (!g1.valid()) {
return {0, g1_error};
}
Gadget g2 = make_gadget(31);
if (!g2.valid()) {
cleanup(g1);
return {0, g2_error};
}
// ...
if (all_foobar(g1, g2)) {
cleanup(g2);
cleanup(g1);
return {0, foobar_error};
}
// ...
cleanup(g2);
cleanup(g1);
return {res, 0};
}
Симуляция RAII может быть нетривиальной, особенно в функциях с несколькими ресурсами и несколькими возможными ошибками. Не редкая техника — собрать очистку в конце функции, чтобы избежать повторений (заметьте, что дополнительная область видимости вокруг g2 нежелательна, но необходима, чтобы версия с goto компилировалась):
std::pair<int, error_indicator> user()
{
error_indicator err = 0;
int res = 0;
Gadget g1 = make_gadget(17);
if (!g1.valid()) {
err = g1_error;
goto g1_exit;
}
{
Gadget g2 = make_gadget(31);
if (!g2.valid()) {
err = g2_error;
goto g2_exit;
}
if (all_foobar(g1, g2)) {
err = foobar_error;
goto g2_exit;
}
// ...
g2_exit:
if (g2.valid()) cleanup(g2);
}
g1_exit:
if (g1.valid()) cleanup(g1);
return {res, err};
}
Чем больше функция, тем более заманчивой становится эта техника. finally может немного облегчить боль. Также чем больше становится программа, тем сложнее систематически применять стратегию обработки ошибок на основе индикатора ошибок.
Мы предпочитаем обработку ошибок на основе исключений и рекомендуем держать функции короткими.
Смотрите также: Обсуждение
Смотрите также: Возврат нескольких значений
Контроль
Затруднено.