Обработка ошибок всегда была важной и одновременно сложной задачей в программировании на C++. На протяжении долгого времени разработчики использовали различные подходы: возвращаемые коды ошибок, исключения, специальные значения или указатели. Каждый метод имел свои недостатки: коды ошибок затрудняли читаемость, исключения вызывали проблемы с производительностью, а специальные значения не предоставляли достаточно информации о самой ошибке. C++23 предлагает решение этой проблемы — шаблонный класс std::expected, который представляет собой монадический тип для обработки ошибок. Этот инструмент позволяет функциям возвращать либо ожидаемое значение либо объект ошибки, причём оба варианта инкапсулированы в единой типобезопасной обёртке.
| C++ | 1
2
3
4
5
6
| std::expected<double, std::string> divide(double numerator, double denominator) {
if (denominator == 0.0) {
return std::unexpected("Ошибка: деление на ноль");
}
return numerator / denominator;
} |
|
В этом примере функция divide возвращает объект std::expected, который содержит либо результат деления (тип double), либо сообщение об ошибке (тип std::string). Пользователь может легко проверить успешность операции и соответствующим образом обработать результат.
std::expected был принят в стандарт C++23 после долгих обсуждений и экспериментов. Концепция пришла из функционального программирования, где монады для обработки ошибок (например, Maybe в Haskell или Result в Rust) давно стали нормой. Первоначально предложенный Висенте Бооетом в бумаге P0323R0, этот тип прошёл значительную эволюцию перед включением в стандарт.
Чем отличается std::expected от традиционных методов обработки ошибок? Сравним их:
С возвращаемыми кодами ошибок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Традиционный подход с кодами ошибок
bool divide(double numerator, double denominator, double& result) {
if (denominator == 0.0) {
return false;
}
result = numerator / denominator;
return true;
}
// Код использования
double result;
if (!divide(10, 0, result)) {
// Обработка ошибки
} |
|
С исключениями:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Подход с исключениями
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
throw std::runtime_error("Деление на ноль");
}
return numerator / denominator;
}
// Код использования
try {
double result = divide(10, 0);
} catch (const std::exception& e) {
// Обработка ошибки
} |
|
С std::expected:
| C++ | 1
2
3
4
5
6
7
8
9
| // Использование std::expected
auto result = divide(10, 0);
if (result) {
// Успешное выполнение
double value = result.value();
} else {
// Обработка ошибки
std::string error = result.error();
} |
|
Преимущества монадического подхода с std::expected:
1. Типобезопасность — ошибки становятся частью системы типов, что обнаруживает проблемы на этапе компиляции.
2. Явный контракт — сигнатура функции чётко указывает на возможность ошибки и её тип.
3. Отсутствие накладных расходов на исключения — нет затрат на разворачивание стека.
4. Улучшенная читаемость — обработка ошибок становится линейной и интуитивно понятной.
5. Компоновка операций — возможность создавать цепочки вызовов с элегантной обработкой ошибок.
std::expected занимает особую роль в экосистеме современного C++. Этот тип логически дополняет другие возможности стандартной библиотеки, такие как std::optional (для представления отсутствия значения) и std::variant (для представления одного из нескольких типов).
Философия монадического подхода к обработке ошибок идёт глубже, чем просто возврат двух возможных результатов. Монада — это по сути контейнер с определёнными правилами для преобразования содержимого. В случае со std::expected, у нас есть контейнер, который может содержать либо значение, либо ошибку, а также методы для безопасной работы с этим содержимым. Монадический подход к обработке ошибок меняет парадигму разработки, делая проверку на ошибки обязательной и явной частью кода. Это принцип "make errors explicit" (сделай ошибки явными), который дает несколько существенных преимуществ:
1. Программист не может "забыть" обработать ошибку — тип системы его направляет.
2. Код становится более предсказуемым и детерминированным.
3. Легче разделять "счастливый путь" и обработку исключительных ситуаций.
При всех своих достоинствах, std::expected имеет и ограничения:
1. Увеличение размера возвращаемого значения — объект std::expected содержит оба типа (успешный результат и ошибку) и дополнительный флаг.
2. Потенциальное усложнение интерфейсов — вместо простых типов функции возвращают обёртки.
3. Не подходит для катастрофических ошибок — для ошибок, которые не могут быть обработаны локально, исключения по-прежнему предпочтительнее.
4. Требует дисциплины — все функции в цепочке должны следовать одному подходу к обработке ошибок.
Важно понимать, что std::expected не является заменой всех других методов обработки ошибок, а скорее дополнением к существующей экосистеме. Его применение оправдано там, где явный контроль ошибок критичен, а накладные расходы на исключения неприемлемы.
Техническая анатомия
Для понимания механизма работы и эффективного использования std::expected необходимо разобраться в его внутреннем устройстве. По сути, std::expected<T, E> — это параметризованный шаблонный класс, где T представляет тип успешного значения, а E — тип ошибки. Эта конструкция напоминает объединение с дискриминатором, которое содержит либо объект типа T, либо объект типа E, а также флаг, указывающий на текущее состояние.
Структура и синтаксис std::expected
Объявление std::expected выглядит примерно так:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| template <class T, class E>
class expected {
public:
// Конструкторы, деструктор и операторы присваивания
// ...
// Методы доступа и проверки
constexpr const T& value() const &;
constexpr T& value() &;
constexpr const T&& value() const &&;
constexpr T&& value() &&;
constexpr const E& error() const &;
constexpr E& error() &;
constexpr const E&& error() const &&;
constexpr E&& error() &&;
constexpr explicit operator bool() const noexcept;
constexpr bool has_value() const noexcept;
// Методы трансформации
// ...
}; |
|
Ключевая особенность std::expected заключается в том, что он хранит в себе одно из двух значений — либо успешный результат типа T, либо ошибку типа E. При создании объекта std::expected мы должны явно указать, какой из двух типов мы хотим сохранить.
| C++ | 1
2
3
4
5
| // Создание с успешным значением
std::expected<int, std::string> success_result = 42;
// Создание с ошибкой
std::expected<int, std::string> error_result = std::unexpected("ошибка"); |
|
Обратите внимание на использование обертки std::unexpected для создания объекта с ошибкой. Эта вспомогательная структура необходима для явного указания, что мы хотим сохранить значение ошибки, а не успешный результат.
Ключевые методы и функциональность
std::expected предоставляет богатый набор методов для работы с его содержимым:
1. Методы проверки состояния:
- has_value() — возвращает true, если объект содержит успешное значение, и false, если ошибку.
- operator bool() — перегруженный оператор, аналогичный has_value().
2. Методы доступа к содержимому:
- value() — возвращает успешное значение или бросает исключение bad_expected_access, если объект содержит ошибку.
- error() — возвращает значение ошибки или бросает исключение, если объект содержит успешное значение.
- value_or(u) — возвращает успешное значение или значение u, если объект содержит ошибку.
3. Методы трансформации:
- and_then(f) — если объект содержит успешное значение, применяет функцию f к этому значению.
- or_else(f) — если объект содержит ошибку, применяет функцию f к этой ошибке.
- transform(f) — преобразует успешное значение с помощью функции f.
- transform_error(f) — преобразует ошибку с помощью функции f.
Рассмотрим примеры использования этих методов:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| std::expected<int, std::string> compute(int value) {
if (value < 0)
return std::unexpected("Отрицательное значение");
return value * 2;
}
// Проверка состояния
auto result = compute(5);
if (result.has_value()) {
std::cout << "Результат: " << result.value() << std::endl;
} else {
std::cout << "Ошибка: " << result.error() << std::endl;
}
// Использование value_or
auto safe_result = compute(-5).value_or(0); // Получим 0 в случае ошибки
// Цепочка операций с and_then
auto complex_result = compute(10)
.and_then([](int v) {
return (v > 15)
? std::expected<double, std::string>(std::sqrt(v))
: std::unexpected("Значение слишком мало");
}); |
|
Образцы использования с примерами кода
Одно из типичных применений std::expected — функции, которые могут завершиться с ошибкой, например, операции ввода-вывода:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| std::expected<std::vector<std::string>, std::string> read_lines_from_file(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
return std::unexpected("Невозможно открыть файл: " + filename);
}
std::vector<std::string> lines;
std::string line;
while (std::getline(file, line)) {
lines.push_back(line);
}
if (file.bad()) {
return std::unexpected("Ошибка при чтении файла");
}
return lines;
} |
|
Этот пример демонстрирует, как std::expected позволяет чётко различать успешное выполнение операции и различные типы ошибок, которые могут возникнуть.
Другой практичный пример — парсинг и валидация данных:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| std::expected<int, std::string> parse_positive_integer(const std::string& str) {
try {
int value = std::stoi(str);
if (value <= 0) {
return std::unexpected("Значение должно быть положительным");
}
return value;
} catch (const std::invalid_argument&) {
return std::unexpected("Невозможно преобразовать в число");
} catch (const std::out_of_range&) {
return std::unexpected("Число слишком большое");
}
} |
|
Здесь std::expected предоставляет удобный способ обработки различных ошибок парсинга и валидации.
Обработка специфических ошибок с помощью std::error
В C++23 вместе со std::expected вводится понятие std::error, которое представляет собой обобщенный тип ошибки. Это позволяет унифицировать обработку различных типов ошибок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| enum class ErrorCode {
InvalidInput,
FileNotFound,
NetworkError
};
struct ApplicationError {
ErrorCode code;
std::string message;
};
std::expected<Data, ApplicationError> process_data(const std::string& input) {
// Проверка входных данных
if (input.empty()) {
return std::unexpected(ApplicationError{
ErrorCode::InvalidInput,
"Входные данные пусты"
});
}
// Обработка
Data result;
// ...
return result;
} |
|
Такой подход позволяет создавать иерархии ошибок и обрабатывать их структурированным образом:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| auto result = process_data(user_input);
if (!result) {
switch (result.error().code) {
case ErrorCode::InvalidInput:
// Обработка неверного ввода
break;
case ErrorCode::FileNotFound:
// Обработка отсутствия файла
break;
case ErrorCode::NetworkError:
// Обработка сетевой ошибки
break;
}
} |
|
Шаблонные параметры и их ограничения
При использовании std::expected<T, E> есть несколько ограничений на типы T и E:
1. Тип E не должен быть void, std::unexpected<T> или специализацией std::unexpected.
2. Оба типа T и E должны быть полностью сконструированными типами (не должны содержать неопределенных типов).
3. Деструкторы обоих типов не должны бросать исключения.
Дополнительное ограничение возникает при использовании методов трансформации (and_then, or_else и др.) — они требуют, чтобы возвращаемые функциями-аргументами типы были совместимы с std::expected.
| C++ | 1
2
3
4
5
6
7
8
9
10
11
| std::expected<int, std::string> safe_sqrt(int value) {
if (value < 0) {
return std::unexpected("Корень из отрицательного числа");
}
return std::sqrt(value);
}
// Здесь transform должен возвращать int, а не double
auto result = safe_sqrt(25).transform([](double v) -> int {
return static_cast<int>(v * 2);
}); |
|
Важно помнить, что std::expected не предназначен для обработки исключительных ситуаций, которые не могут быть корректно обработаны на локальном уровне. Для таких случаев по-прежнему уместнее использовать механизм исключений C++.
Std::vector<std::pair<std::vector<int>::iterator, std::vector<int>::iterator> Вопрос по вектору.
Допустим есть вектор,
std::vector<int> vec;
на каком - то этапе заполнения я... ошибка error: cannot convert 'std::string {aka std::basic_string<char>}' to 'std::string* {aka std::basic_stri на вод поступают 2 строки типа string. определить количество вхождений строки 2 в строку 1
ошибка... STL std::set, std::pair, std::make_pair Я не знаю как описать тему в двух словах, поэтому не обращайте внимание на название темы.... Не воспринимает ни std::cout, ни std::cin. Вобщем ничего из std. Также не понимает iostream Здравствуйте!
Я хотел начать изучать язык C++. Набрал литературы. Установил Microsoft Visual C++...
Работа с составными типами ошибок в std::expected
Один из мощных аспектов std::expected — возможность работать с составными типами ошибок, что позволяет создавать богатые и информативные структуры для представления различных ситуаций. Рассмотрим подход с использованием иерархии ошибок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // Базовый класс для всех ошибок
struct ErrorBase {
virtual ~ErrorBase() = default;
virtual std::string message() const = 0;
};
// Специализированные типы ошибок
struct FileError : ErrorBase {
enum class Type { NotFound, AccessDenied, Corrupt };
Type type;
std::string path;
FileError(Type t, std::string p) : type(t), path(std::move(p)) {}
std::string message() const override {
switch(type) {
case Type::NotFound: return "Файл не найден: " + path;
case Type::AccessDenied: return "Доступ запрещен: " + path;
case Type::Corrupt: return "Файл поврежден: " + path;
}
return "Неизвестная ошибка файла";
}
};
struct NetworkError : ErrorBase {
int status_code;
std::string url;
NetworkError(int code, std::string u) : status_code(code), url(std::move(u)) {}
std::string message() const override {
return "Сетевая ошибка (" + std::to_string(status_code) + "): " + url;
}
}; |
|
Теперь можно использовать этот базовй тип ошибки в функциях:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| std::expected<std::vector<char>, std::unique_ptr<ErrorBase>> read_remote_file(const std::string& url) {
// Проверка доступности сети
if (!is_network_available()) {
return std::unexpected(std::make_unique<NetworkError>(0, url));
}
// Попытка загрузить файл
auto response = http_get(url);
if (response.status_code != 200) {
return std::unexpected(std::make_unique<NetworkError>(response.status_code, url));
}
return response.body;
} |
|
При обработке таких ошибок можно использовать динамический полиморфизм:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
| auto content = read_remote_file("https://example.com/data.txt");
if (!content) {
std::cerr << "Ошибка: " << content.error()->message() << std::endl;
// Можно также использовать dynamic_cast для определения типа ошибки
if (auto file_err = dynamic_cast<FileError*>(content.error().get())) {
// Специфическая обработка ошибки файла
} else if (auto net_err = dynamic_cast<NetworkError*>(content.error().get())) {
// Специфическая обработка сетевой ошибки
}
} |
|
Альтернативный подход — использование std::variant как типа ошибки:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| using ErrorVariant = std::variant<FileError, NetworkError, std::string>;
std::expected<std::vector<char>, ErrorVariant> read_file(const std::string& path) {
if (!std::filesystem::exists(path)) {
return std::unexpected(FileError{FileError::Type::NotFound, path});
}
try {
std::vector<char> content;
// Чтение файла...
return content;
} catch (const std::exception& e) {
return std::unexpected(std::string(e.what()));
}
} |
|
При таком подходе обработка ошибок становится более статически типизированной:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| auto content = read_file("/path/to/file.txt");
if (!content) {
std::visit([](auto&& err) {
using T = std::decay_t<decltype(err)>;
if constexpr (std::is_same_v<T, FileError>) {
std::cerr << "Ошибка файла: " << err.message() << std::endl;
} else if constexpr (std::is_same_v<T, NetworkError>) {
std::cerr << "Сетевая ошибка: " << err.message() << std::endl;
} else {
std::cerr << "Другая ошибка: " << err << std::endl;
}
}, content.error());
} |
|
Правила конструирования и деструктурирования объектов std::expected
std::expected имеет строгие правила для конструирования и деструктурирования, которые важно понимать для корректной работы с этим типом.
Конструкторы
std::expected предоставляет несколько конструкторов:
1. Конструктор по умолчанию — создаёт объект с успешным значением, сконструированным по умолчанию (при условии, что T имеет конструктор по умолчанию):
| C++ | 1
| std::expected<std::string, int> e; // содержит пустую строку |
|
2. Конструкторы копирования и перемещения — стандартное поведение для классов C++:
| C++ | 1
2
3
| std::expected<std::string, int> e1 = "success";
std::expected<std::string, int> e2 = e1; // копирование
std::expected<std::string, int> e3 = std::move(e1); // перемещение |
|
3. Конструирование из значения — создаёт объект с успешным значением:
| C++ | 1
| std::expected<std::string, int> e = "success"; |
|
4. Конструирование из std::unexpected — создаёт объект с ошибкой:
| C++ | 1
| std::expected<std::string, int> e = std::unexpected(42); |
|
5. Конструирование с помощью in-place — конструирует успешное значение на месте:
| C++ | 1
2
3
4
| std::expected<std::vector<int>, std::string> e{
std::in_place, // указывает на конструирование значения
{1, 2, 3, 4} // аргументы для конструктора вектора
}; |
|
6. Конструирование с помощью unexpect_t — конструирует значение ошибки на месте:
| C++ | 1
2
3
4
| std::expected<std::string, std::vector<int>> e{
std::unexpect, // указывает на конструирование ошибки
{1, 2, 3, 4} // аргументы для конструктора вектора
}; |
|
Деструктурирование и доступ к содержимому
Для доступа к содержимому std::expected существует несколько методов:
1. value() — возвращает ссылку на успешное значение или бросает исключение, если объект содержит ошибку:
| C++ | 1
2
3
4
5
| std::expected<int, std::string> e = 42;
int value = e.value(); // OK
e = std::unexpected("ошибка");
// e.value(); // бросит исключение bad_expected_access |
|
2. error() — возвращает ссылку на ошибку или бросает исключение, если объект содержит успешное значение:
| C++ | 1
2
3
4
5
| std::expected<int, std::string> e = std::unexpected("ошибка");
std::string error = e.error(); // OK
e = 42;
// e.error(); // бросит исключение |
|
3. operator* и operator-> — операторы доступа к успешному значению (без проверок):
| C++ | 1
2
3
4
5
6
7
8
9
10
| struct User { std::string name; int age; };
std::expected<User, std::string> get_user(int id) {
// ...
return User{"Алиса", 30};
}
auto user = get_user(123);
if (user) {
std::cout << "Имя: " << user->name << ", возраст: " << user->age << std::endl;
} |
|
4. value_or() — безопасный метод получения значения с резервным вариантом:
| C++ | 1
2
| std::expected<int, std::string> e = std::unexpected("ошибка");
int value = e.value_or(0); // получим 0 |
|
Управление жизненным циклом объектов
Важно понимать, что std::expected управляет жизненным циклом содержащихся объектов:
1. Конструирует ровно один из двух типов (значение или ошибку).
2. При присваивании может потребоваться деструкция одного объекта и конструирование другого.
3. При деструкции вызывает деструктор только активного объекта.
| C++ | 1
2
3
4
5
6
7
| std::expected<Resource, ErrorInfo> e = Resource(/* ... */);
// В этот момент сконструирован объект типа Resource
e = std::unexpected(ErrorInfo(/* ... */));
// Здесь деструктор Resource вызван, а затем сконструирован ErrorInfo
// При выходе из области видимости будет вызван деструктор ErrorInfo |
|
Это важный аспект при работе с типами, требующими управления ресурсами (RAII).
Оптимизации и тонкости реализации
std::expected разработан для эффективной работы и минимизации накладных расходов. Внутренняя реализация использует техники, аналогичные std::variant и std::optional:
1. Размер объекта — примерно равен сумме размеров типов T и E плюс небольшой дискриминатор для отслеживания активного состояния.
2. Семантика копирования и перемещения соответствует семантике содержащихся типов.
3. Методы доступа оптимизированы для минимизации накладных расходов.
Одной из тонкостей реализации является обработка случая, когда типы T и E имеют конструкторы преобразования друг для друга:
| C++ | 1
2
| std::expected<int, double> e = 42; // OK, однозначно выбирается тип T
e = 3.14; // Ошибка компиляции! Неоднозначность между T и E |
|
В таких случаях необходимо явно указывать, какой конструктор использовать:
| C++ | 1
2
| e = std::expected<int, double>(3); // Явно конструируем успешное значение
e = std::unexpected<double>(3.14); // Явно конструируем ошибку |
|
Реализация std::expected также учитывает возможность использования с типами, которые нельзя копировать или перемещать, но такие случаи требуют особой осторожности.
Применение на практике
Теоретическое понимание std::expected — лишь первый шаг. Настоящая его ценность раскрывается при практическом применении в реальных проектах. Рассмотрим, как этот инструмент трансформирует подход к обработке ошибок в C++.
Замена исключений и кодов ошибок
Традиционный код с исключениями часто приводит к разрывам в логике программы, делая её менее предсказуемой и трудной для отладки:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| std::vector<double> calculate_statistics(const std::vector<int>& data) {
if (data.empty()) {
throw std::invalid_argument("Пустой набор данных");
}
double sum = 0;
for (int value : data) {
sum += value;
}
double mean = sum / data.size();
double variance = 0;
for (int value : data) {
double diff = value - mean;
variance += diff * diff;
}
variance /= data.size();
return {mean, std::sqrt(variance)};
}
// Использование:
try {
auto stats = calculate_statistics(user_data);
process_statistics(stats);
} catch (const std::exception& e) {
display_error(e.what());
} |
|
Переписывая этот код с использованием std::expected, мы делаем поток выполнения более линейным и ясным:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| std::expected<std::vector<double>, std::string> calculate_statistics(
const std::vector<int>& data) {
if (data.empty()) {
return std::unexpected("Пустой набор данных");
}
double sum = 0;
for (int value : data) {
sum += value;
}
double mean = sum / data.size();
double variance = 0;
for (int value : data) {
double diff = value - mean;
variance += diff * diff;
}
variance /= data.size();
return std::vector<double>{mean, std::sqrt(variance)};
}
// Использование:
auto stats = calculate_statistics(user_data);
if (stats) {
process_statistics(stats.value());
} else {
display_error(stats.error());
} |
|
Аналогично, замена кодов ошибок на std::expected даёт более наглядный и типобезопасный код:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // Старый стиль с кодами ошибок
enum ErrorCode { SUCCESS, EMPTY_DATA, DIVIDE_BY_ZERO };
ErrorCode calculate_average(const std::vector<int>& data, double* result) {
if (data.empty()) {
return EMPTY_DATA;
}
double sum = 0;
for (int value : data) {
sum += value;
}
*result = sum / data.size();
return SUCCESS;
}
// Новый стиль с std::expected
std::expected<double, std::string> calculate_average(const std::vector<int>& data) {
if (data.empty()) {
return std::unexpected("Пустой набор данных");
}
double sum = 0;
for (int value : data) {
sum += value;
}
return sum / data.size();
} |
|
Преимущества при обработке ошибок
Практическое применение std::expected открывает множество преимуществ:
1. Явное указание на возможность ошибки в сигнатуре функции. Когда разработчик видит возвращаемый тип std::expected<T, E>, он сразу понимает, что функция может завершиться с ошибкой типа E.
2. Контекстная информация об ошибке. В отличие от исключений, которые могут содержать только текстовое сообщение, std::expected позволяет возвращать структурированные данные об ошибке:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| enum class FileErrorType { NotFound, AccessDenied, Corrupt };
struct FileError {
FileErrorType type;
std::string path;
int error_code;
std::string format_message() const {
std::string base = "Ошибка файла " + path + " (код " +
std::to_string(error_code) + "): ";
switch (type) {
case FileErrorType::NotFound:
return base + "файл не найден";
case FileErrorType::AccessDenied:
return base + "доступ запрещен";
case FileErrorType::Corrupt:
return base + "файл поврежден";
}
return base + "неизвестная ошибка";
}
};
std::expected<std::vector<std::byte>, FileError> read_binary_file(
const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return std::unexpected(FileError{
FileErrorType::NotFound,
path,
errno
});
}
// Чтение файла...
std::vector<std::byte> content;
// ...
return content;
} |
|
3. Принудительная обработка ошибок. В отличие от исключений, которые можно случайно пропустить, или кодов ошибок, которые легко игнорируются, std::expected требует явной проверки результата:
| C++ | 1
2
3
4
5
6
7
8
9
| auto file_content = read_binary_file("config.dat");
if (!file_content) {
// Ошибка должна быть обработана здесь
log_error(file_content.error().format_message());
return;
}
// Продолжаем работу с содержимым файла
process_data(file_content.value()); |
|
4. Легкость композиции функций. std::expected упрощает последовательное применение функций с возможными ошибками:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| std::expected<Config, std::string> load_config(const std::string& path) {
// Чтение файла
auto content = read_file(path);
if (!content) {
return std::unexpected("Ошибка чтения файла: " + content.error());
}
// Парсинг JSON
auto json = parse_json(content.value());
if (!json) {
return std::unexpected("Ошибка парсинга JSON: " + json.error());
}
// Валидация конфигурации
auto config = validate_config(json.value());
if (!config) {
return std::unexpected("Ошибка валидации: " + config.error());
}
return config.value();
} |
|
Потенциальные проблемы и сложности
При использовании std::expected могут возникнуть некоторые трудности:
1. Увеличение размера возвращаемого значения. Объект std::expected<T, E> имеет размер, приблизительно равный sizeof(T) + sizeof(E) + sizeof(bool), что может быть значительно больше, чем просто T. Эта проблема усугубляется при передаче больших объектов.
2. Возможные проблемы с производительностью. В некоторых случаях дополнительная проверка состояния и обработка ошибок могут приводить к снижению производительности, особенно в критических участках кода.
3. Распространение типа ошибки. Если функция использует результат другой функции с типом ошибки E1, но сама генерирует ошибки типа E2, то возникает вопрос согласования типов:
| C++ | 1
2
3
4
5
6
7
8
9
| std::expected<int, ErrorType1> func1();
std::expected<double, ErrorType2> func2() {
auto result = func1();
if (!result) {
// Как преобразовать ErrorType1 в ErrorType2?
return std::unexpected(convert_error(result.error()));
}
// ...
} |
|
4. Избыточная вложенность при обработке цепочек ошибок. Без использования методов трансформации код может становиться трудночитаемым из-за многократных проверок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
| // Без трансформации - много вложенных проверок
auto result1 = step1();
if (!result1) {
return std::unexpected(result1.error());
}
auto result2 = step2(result1.value());
if (!result2) {
return std::unexpected(result2.error());
}
// И так далее... |
|
Асинхронное программирование со std::expected
В асинхронном программировании std::expected особенно полезен для обработки результатов операций, выполняемых в фоновом режиме:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| using Response = std::vector<std::byte>;
using Error = std::string;
std::future<std::expected<Response, Error>> async_http_get(const std::string& url) {
return std::async(std::launch::async, [url]() -> std::expected<Response, Error> {
try {
// Выполнение HTTP-запроса
if (url.empty()) {
return std::unexpected("Пустой URL");
}
// ... код HTTP-запроса ...
Response response;
// ... заполнение ответа ...
return response;
} catch (const std::exception& e) {
return std::unexpected(std::string("Ошибка запроса: ") + e.what());
}
});
}
// Использование:
auto future_response = async_http_get("https://api.example.com/data");
// Другие операции...
auto response = future_response.get(); // Ожидание завершения
if (response) {
process_data(response.value());
} else {
handle_error(response.error());
} |
|
Это особенно удобно при работе с асинхронными цепочками операций, где каждый шаг может завершиться ошибкой:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| std::future<std::expected<UserProfile, std::string>> load_user_profile(int user_id) {
return std::async(std::launch::async, [user_id]() {
// Запрос базовой информации о пользователе
auto user_info = request_user_info(user_id);
if (!user_info) {
return std::expected<UserProfile, std::string>(
std::unexpected(user_info.error()));
}
// Запрос настроек пользователя
auto settings = request_user_settings(user_id);
if (!settings) {
return std::expected<UserProfile, std::string>(
std::unexpected(settings.error()));
}
// Запрос статистики пользователя
auto stats = request_user_stats(user_id);
if (!stats) {
return std::expected<UserProfile, std::string>(
std::unexpected(stats.error()));
}
// Объединение всех данных
return std::expected<UserProfile, std::string>(UserProfile{
user_info.value(),
settings.value(),
stats.value()
});
});
} |
|
Цепочки вызовов с использованием std::expected
Один из самых мощных аспектов std::expected — возможность создавать элегантные цепочки вызовов функций, где каждый шаг может завершиться ошибкой. Методы трансформации (and_then, or_else, transform и transform_error) позволяют писать выразительный код без избыточных проверок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| std::expected<User, std::string> find_user(int id) {
// Поиск пользователя в базе данных
if (id <= 0) {
return std::unexpected("Недопустимый ID пользователя");
}
return User{id, "Пользователь_" + std::to_string(id)};
}
std::expected<Permission, std::string> get_permission(const User& user) {
// Получение разрешений пользователя
if (user.id > 1000) {
return std::unexpected("У пользователя нет разрешений");
}
return Permission{user.id, "read"};
}
std::expected<Document, std::string> load_document(const Permission& perm, int doc_id) {
// Загрузка документа с проверкой разрешений
if (perm.type != "read" && perm.type != "write") {
return std::unexpected("Недостаточно прав для загрузки документа");
}
return Document{doc_id, "Содержимое документа " + std::to_string(doc_id)};
} |
|
Используя эти функции, мы можем создать цепочку вызовов с помощью and_then:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Громоздкий подход с явными проверками
auto user_result = find_user(user_id);
if (!user_result) {
return std::unexpected(user_result.error());
}
auto perm_result = get_permission(user_result.value());
if (!perm_result) {
return std::unexpected(perm_result.error());
}
return load_document(perm_result.value(), doc_id);
// Элегантный подход с and_then
return find_user(user_id)
.and_then([doc_id](User user) {
return get_permission(user)
.and_then([doc_id](Permission perm) {
return load_document(perm, doc_id);
});
}); |
|
Такой подход напоминает работу с монадами в функциональных языках, где цепочки операций строятся с помощью bind (аналог and_then). Это особенно полезно, когда функции могут завершиться различными типами ошибок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| std::expected<Response, NetworkError> make_request(const std::string& url);
std::expected<ParsedData, ParseError> parse_response(const Response& resp);
std::expected<Result, ValidationError> validate_data(const ParsedData& data);
// Объединение разных типов ошибок в один
using Error = std::variant<NetworkError, ParseError, ValidationError>;
std::expected<Result, Error> process_request(const std::string& url) {
return make_request(url)
.transform_error([](const NetworkError& e) -> Error { return e; })
.and_then([](const Response& resp) {
return parse_response(resp)
.transform_error([](const ParseError& e) -> Error { return e; });
})
.and_then([](const ParsedData& data) {
return validate_data(data)
.transform_error([](const ValidationError& e) -> Error { return e; });
});
} |
|
Реальные примеры рефакторинга кода с исключений на std::expected
Рассмотрим реальный пример рефакторинга кода, который изначально использовал исключения для обработки ошибок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // Исходная версия с исключениями
class ConfigParser {
public:
Config parse_file(const std::string& path) {
try {
auto file_content = read_file(path);
auto json = parse_json(file_content);
return convert_to_config(json);
} catch (const FileNotFoundException& e) {
throw ConfigError("Файл конфигурации не найден: " + path);
} catch (const JsonParseException& e) {
throw ConfigError("Ошибка синтаксиса JSON в файле: " + path);
} catch (const ConfigValidationException& e) {
throw ConfigError("Ошибка валидации конфигурации: " + e.what());
}
}
private:
std::string read_file(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
throw FileNotFoundException(path);
}
return std::string(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>()
);
}
Json parse_json(const std::string& content) {
// Парсинг JSON с возможным исключением
}
Config convert_to_config(const Json& json) {
// Конвертация с валидацией, может бросить исключение
}
}; |
|
После рефакторинга с использованием std::expected:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // Версия с std::expected
class ConfigParser {
public:
std::expected<Config, std::string> parse_file(const std::string& path) {
auto file_content = read_file(path);
if (!file_content) {
return std::unexpected("Файл конфигурации не найден: " + path);
}
auto json = parse_json(file_content.value());
if (!json) {
return std::unexpected("Ошибка синтаксиса JSON в файле: " + path);
}
return convert_to_config(json.value());
}
private:
std::expected<std::string, std::string> read_file(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
return std::unexpected(path);
}
return std::string(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>()
);
}
std::expected<Json, std::string> parse_json(const std::string& content) {
// Парсинг JSON с std::expected
}
std::expected<Config, std::string> convert_to_config(const Json& json) {
// Конвертация с валидацией через std::expected
}
}; |
|
Более изящная версия с использованием цепочки вызовов:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
| std::expected<Config, std::string> parse_file(const std::string& path) {
return read_file(path)
.and_then([&path](const std::string& content) {
return parse_json(content)
.or_else([&path](const std::string& err) {
return std::unexpected("Ошибка синтаксиса JSON в файле " + path + ": " + err);
});
})
.and_then([](const Json& json) {
return convert_to_config(json);
});
} |
|
Оптимизация производительности при работе со std::expected
При работе со std::expected производительность может стать проблемой, особенно если функция часто вызывается и создает много временных объектов. Вот несколько стратегий оптимизации:
1. Избегайте излишнего копирования. Используйте перемещение и ссылки где возможно:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
| // Неоптимально: копирование больших объектов
std::expected<LargeObject, std::string> process(const LargeObject& obj) {
// ...
return modified_obj; // Копирование
}
// Оптимально: использование перемещения
std::expected<LargeObject, std::string> process(LargeObject obj) {
// ...
return modified_obj; // Перемещение
} |
|
2. Используйте нешаблонные типы ошибок. Фиксированный тип ошибки позволяет компилятору лучше оптимизировать код:
| C++ | 1
2
3
4
5
6
7
8
9
10
| // Единый тип ошибки для всего приложения
struct Error {
enum class Code { FileNotFound, ParseError, ValidationError };
Code code;
std::string message;
};
// Все функции возвращают одинаковый тип ошибки
std::expected<Data, Error> process(); |
|
3. Применяйте Small Object Optimization (SOO). Для небольших объектов ошибок можно использовать техники оптимизации малых объектов:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class SmallStringError {
static constexpr size_t MaxInlineSize = 24;
union {
char inline_data[MaxInlineSize];
char* heap_data;
};
size_t length;
bool is_inline;
public:
// Реализация с SOO
};
std::expected<Result, SmallStringError> operation(); |
|
4. Рассмотрите специализированные библиотеки. Существуют оптимизированные реализации, подобные std::expected, с дополнительными возможностями и улучшенной производительностью, например, tl::expected или boost::outcome.
5. Профилируйте код. Перед оптимизацией всегда выполняйте профилирование, чтобы определить узкие места:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
| // Измерение времени выполнения
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; ++i) {
auto result = complex_operation(i);
if (result) process_value(result.value());
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> duration = end - start;
std::cout << "Время выполнения: " << duration.count() << " мс\n"; |
|
Эффективное использование std::expected в реальном коде требует баланса между выразительностью, обработкой ошибок и производительностью. Правильно примененный, этот инструмент может существенно повысить качество кода и сделать обработку ошибок более понятной и надежной.
Сравнительный анализ
Чтобы лучше понять место std::expected в экосистеме C++ и общей картине обработки ошибок в современных языках программирования, полезно провести сравнительный анализ с аналогичными механизмами. Такое сравнение помогает выбрать оптимальный инструмент для конкретных задач и понять сильные и слабые стороны разных подходов.
std::expected vs. std::optional
Как std::expected<T, E>, так и std::optional<T> представляют собой обертки над значением, которое может присутствовать или отсутствовать. Однако между ними есть существенные различия:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
| // std::optional сообщает только о наличии или отсутствии значения
std::optional<int> divide_optional(int a, int b) {
if (b == 0) return std::nullopt;
return a / b;
}
// std::expected предоставляет информацию о причине ошибки
std::expected<int, std::string> divide_expected(int a, int b) {
if (b == 0) return std::unexpected("Деление на ноль");
return a / b;
} |
|
Ключевые отличия:
1. Информативность ошибок: std::optional сообщает только факт отсутствия значения, но не причину. std::expected может содержать объект ошибки любого типа с детальной информацией.
2. Семантика использования: std::optional больше подходит для случаев, когда отсутствие значения — это нормальная ситуация (например, поиск элемента, который может отсутствовать). std::expected лучше использовать, когда отсутствие значения — это ошибка, требующая обработки.
3. Выразительность API: std::expected предлагает более богатый интерфейс с методами and_then, or_else, transform и другими, что делает цепочки операций более элегантными.
4. Размер: std::expected<T, E> обычно больше, чем std::optional<T>, поскольку должен хранить либо T, либо E, а также дискриминатор.
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Пример использования std::optional
auto result = find_user(username);
if (result) {
process_user(*result);
} else {
create_new_user(username); // Отсутствие пользователя — допустимый случай
}
// Пример использования std::expected
auto result = authenticate_user(username, password);
if (result) {
grant_access(result.value());
} else {
display_error(result.error()); // Ошибка аутентификации требует обработки
} |
|
std::expected vs. исключения
Исключения долгое время были основным механизмом обработки ошибок в C++, но у них есть свои недостатки, которые std::expected может помочь устранить:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Подход с исключениями
double compute_with_exceptions(const Data& data) {
try {
if (data.empty()) throw std::invalid_argument("Пустые данные");
double result = process(data);
if (result < 0) throw std::domain_error("Отрицательный результат");
return result;
} catch (const std::exception& e) {
log_error(e.what());
return default_value;
}
}
// Подход с std::expected
std::expected<double, std::string> compute_with_expected(const Data& data) {
if (data.empty()) return std::unexpected("Пустые данные");
double result = process(data);
if (result < 0) return std::unexpected("Отрицательный результат");
return result;
} |
|
Главные отличия:
1. Производительность: Исключения могут иметь значительные накладные расходы, особенно при разворачивании стека. std::expected предлагает более легковесную альтернативу.
2. Явность контракта: Тип std::expected<T, E> явно заявляет в сигнатуре функции, что она может завершиться ошибкой, тогда как исключения часто не документируются в типах.
3. Локальность обработки: std::expected принуждает к обработке ошибок на месте вызова, что предотвращает неожиданное распространение ошибок через стек вызовов.
4. Контроль потока выполнения: Исключения приводят к нелинейному потоку выполнения, в то время как std::expected поддерживает более последовательный и предсказуемый стиль.
5. Совместимость с кодом "без исключений": std::expected можно использовать в проектах, где исключения запрещены или ограничены (например, в системах реального времени или критических к безопасности приложениях).
Сравнение с Rust Result и Swift Result
Концепция std::expected имеет близкие аналоги в других современных языках, что свидетельствует о всеобщем признании этого подхода к обработке ошибок:
Rust Result
| Rust | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Пример на Rust
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("деление на ноль"))
} else {
Ok(a / b)
}
}
// Использование с оператором ?
fn calculate_avg(values: &[i32]) -> Result<f64, String> {
if values.is_empty() {
return Err(String::from("пустой массив"));
}
let sum: i32 = values.iter().sum();
let result = divide(sum, values.len() as i32)?;
Ok(result as f64)
} |
|
Swift Result
| Swift | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Пример на Swift
enum DivisionError: Error {
case divisionByZero
}
func divide(_ a: Int, by b: Int) -> Result<Int, DivisionError> {
guard b != 0 else {
return .failure(.divisionByZero)
}
return .success(a / b)
}
// Использование с do-catch
func calculateAverage(values: [Int]) throws -> Double {
do {
let sum = values.reduce(0, +)
let result = try divide(sum, by: values.count).get()
return Double(result)
} catch {
throw error
}
} |
|
C++ std::expected
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| std::expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return std::unexpected("деление на ноль");
}
return a / b;
}
std::expected<double, std::string> calculate_avg(const std::vector<int>& values) {
if (values.empty()) {
return std::unexpected("пустой массив");
}
int sum = std::accumulate(values.begin(), values.end(), 0);
return divide(sum, values.size())
.transform([](int result) {
return static_cast<double>(result);
});
} |
|
Основные отличия:
1. Синтаксический сахар: Rust предлагает оператор ? для краткой обработки ошибок, Swift использует конструкции try/throw/catch. C++ не имеет специального синтаксиса для std::expected.
2. Интеграция с языком: В Rust и Swift эти типы глубоко интегрированы в языки и стандартные библиотеки, тогда как в C++ это относительно новое дополнение.
3. Монадическое API: Rust и Swift предлагают богатые API для работы с их типами Result, и std::expected заимствовал многие концепции оттуда.
Производительность и потребление ресурсов
Выбор механизма обработки ошибок может существенно влиять на производительность приложения. Исследования показывают следующие тенденции:
1. Размер объектов: std::expected<T, E> займет примерно max(sizeof(T), sizeof(E)) + sizeof(дискриминатор), что делает его больше, чем просто T или код ошибки.
2. Время выполнения: При нормальном выполнении (без ошибок) std::expected обычно быстрее исключений, но может быть медленнее простого возврата значения. При обработке ошибок std::expected значительно быстрее исключений.
3. Компиляция: Код с std::expected обычно компилируется быстрее, чем код с исключениями, особенно когда исключения требуют генерации таблиц обработки исключений.
4. Инлайнинг и оптимизация: Компиляторы могут лучше оптимизировать код с std::expected, поскольку поток управления более прямолинеен и предсказуем.
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // Наивные бенчмарки для разных подходов к обработке ошибок
void benchmark_error_handling() {
constexpr int iterations = 1000000;
// Измерение с исключениями
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
try {
if (i % 100 == 0) throw std::runtime_error("ошибка");
process_value(i);
} catch (const std::exception&) {
handle_error();
}
}
auto end1 = std::chrono::high_resolution_clock::now();
// Измерение с std::expected
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
auto result = i % 100 == 0
? std::expected<int, std::string>(std::unexpected("ошибка"))
: std::expected<int, std::string>(i);
if (result) {
process_value(result.value());
} else {
handle_error();
}
}
auto end2 = std::chrono::high_resolution_clock::now();
// Сравнение времени выполнения
// ...
} |
|
Сопоставление с моделью обработки ошибок в Golang
Go представляет еще один подход к обработке ошибок, основанный на множественных возвращаемых значениях:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Пример на Go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("деление на ноль")
}
return a / b, nil
}
// Использование
func calculateAverage(values []int) (float64, error) {
if len(values) == 0 {
return 0, errors.New("пустой массив")
}
sum := 0
for _, v := range values {
sum += v
}
result, err := divide(sum, len(values))
if err != nil {
return 0, err
}
return float64(result), nil
} |
|
По сравнению с моделью Go, std::expected имеет следующие особенности:
1. Одно возвращаемое значение: std::expected объединяет результат и ошибку в один объект, в то время как Go использует кортежи возвращаемых значений.
2. Типобезопасность: std::expected обеспечивает строгую типизацию ошибок, тогда как в Go обычно используется интерфейс error.
3. Явная проверка: Как и Go, std::expected требует явной проверки на ошибку, что делает код более надежным, чем подход с исключениями.
4. Композиция функций: std::expected лучше подходит для функционального стиля программирования с цепочками вызовов, в то время как модель Go более процедурная.
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // C++ с подходом, похожим на Go
std::pair<int, std::optional<std::string>> divide_go_style(int a, int b) {
if (b == 0) {
return {0, "деление на ноль"};
}
return {a / b, std::nullopt};
}
// Использование
auto [result, error] = divide_go_style(10, 2);
if (error) {
// Обработка ошибки
} else {
// Использование результата
}
// С std::expected
auto res = divide(10, 2);
if (res) {
// Использование результата
} else {
// Обработка ошибки
} |
|
Выбор между std::expected, исключениями, std::optional или другими методами обработки ошибок зависит от конкретных требований проекта, включая производительность, читаемость кода, совместимость с существующим кодом и предпочтения команды. std::expected предлагает компромисс между явностью подхода Go, типобезопасностью Rust и Swift, при этом сохраняя совместимость с существующими паттернами C++.
Передовые практики и опыт
Внедрение std::expected в реальные проекты требует не только понимания технических аспектов, но и владения передовыми практиками. С ростом популярности этого подхода сообщество разработчиков накопило ценный опыт, который стоит учитывать при использовании монадического подхода к обработке ошибок.
Паттерны проектирования с std::expected
При работе со std::expected хорошо зарекомендовали себя несколько паттернов проектирования, адаптированных под специфику этого инструмента:
Factory Pattern с ожидаемыми значениями
Фабричные методы особенно выигрывают от использования std::expected, обеспечивая безопасное и информативное создание объектов:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| class DatabaseConnection {
public:
static std::expected<DatabaseConnection, ConnectionError> create(const ConnectionString& cs) {
if (!validate_connection_string(cs)) {
return std::unexpected(ConnectionError::InvalidString);
}
try {
DatabaseConnection conn;
if (!conn.connect(cs)) {
return std::unexpected(ConnectionError::ConnectionFailed);
}
return conn;
} catch (...) {
return std::unexpected(ConnectionError::UnknownError);
}
}
private:
bool connect(const ConnectionString& cs) { /* ... */ }
// Конструктор скрыт, чтобы предотвратить создание невалидных объектов
DatabaseConnection() = default;
}; |
|
Railway-oriented Programming
Эта парадигма представляет поток выполнения как два параллельных "пути" — успешный и ошибочный, между которыми код переключается при необходимости:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| template <typename T, typename E, typename Function>
auto bind(std::expected<T, E> exp, Function&& func) {
if (exp) {
return func(std::move(exp.value()));
} else {
return std::expected<decltype(func(std::declval<T>()).value()), E>(
std::unexpected(exp.error()));
}
}
// Применение
auto result = read_file(path)
| bind(parse_json)
| bind(extract_config)
| bind(validate_settings); |
|
Альтернативные реализации
До включения в стандарт C++23, разработчики могли использовать альтернативные реализации концепции expected:
1. Boost.Outcome — предоставляет типы outcome::result<T, E> и outcome::outcome<T, E, P> с расширенными возможностями для многоуровневой обработки ошибок и интеграции с оповещениями о нештатных ситуациях.
2. tl::expected — популярная реализация от Тристана Байи, которая послужила основой для стандартной версии. Многие проекты продолжают использовать эту библиотеку в кодовых базах, ориентированных на C++17.
3. folly::Expected — реализация от Facebook с дополнительными возможностями оптимизации для высоконагруженных систем.
Если вы работаете с проектом, который должен поддерживать различные версии C++, стоит рассмотреть эти альтернативы, многие из которых предлагают обратную совместимость со стандартной версией.
Интеграция в существующие проекты
Внедрение std::expected в существующую кодовую базу может быть вызовом, особенно если она полагается на другие механизмы обработки ошибок:
Поэтапная миграция от исключений
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Адаптер для преобразования функций с исключениями в std::expected
template <typename Func, typename... Args>
auto make_expected(Func&& func, Args&&... args) {
using ResultType = std::invoke_result_t<Func, Args...>;
try {
if constexpr (std::is_void_v<ResultType>) {
std::forward<Func>(func)(std::forward<Args>(args)...);
return std::expected<void, std::string>();
} else {
return std::expected<ResultType, std::string>(
std::forward<Func>(func)(std::forward<Args>(args)...));
}
} catch (const std::exception& e) {
if constexpr (std::is_void_v<ResultType>) {
return std::expected<void, std::string>(std::unexpected(e.what()));
} else {
return std::expected<ResultType, std::string>(std::unexpected(e.what()));
}
}
}
// Использование
auto result = make_expected(legacy_function_with_exceptions, arg1, arg2); |
|
Стандартизация типов ошибок
Важно разработать единую стратегию для типов ошибок, особенно в больших проектах:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Единый тип ошибки для всего проекта
struct Error {
enum class Domain {
Filesystem,
Network,
Database,
Validation
};
Domain domain;
int code;
std::string message;
// Вспомогательные фабричные методы
static Error filesystem(int code, std::string msg) {
return Error{Domain::Filesystem, code, std::move(msg)};
}
static Error network(int code, std::string msg) {
return Error{Domain::Network, code, std::move(msg)};
}
// ...
};
// Типичные шаблоны для функций проекта
template <typename T>
using Result = std::expected<T, Error>; |
|
Монадические комбинаторы для std::expected
Стандартная библиотека предоставляет базовые методы для работы с std::expected, но для более гибкой обработки ошибок полезно создать дополнительные операторы и функции:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Оператор "|" для цепочки операций
template <typename T, typename E, typename F>
auto operator|(std::expected<T, E> exp, F&& f) {
return exp.and_then(std::forward<F>(f));
}
// Функция recover для обработки ошибок с возможностью восстановления
template <typename T, typename E, typename F>
std::expected<T, E> recover(std::expected<T, E> exp, F&& recovery_func) {
if (exp) return exp;
return recovery_func(exp.error());
}
// Применение
auto result = read_file(path)
| parse_json
| recover([](const Error& e) -> std::expected<Json, Error> {
if (e.domain == Error::Domain::Filesystem && e.code == ENOENT) {
return Json::object(); // Возвращаем пустой объект, если файл не найден
}
return std::unexpected(e); // Иначе пробрасываем ошибку дальше
})
| extract_config; |
|
Инструменты статического анализа
Современные статические анализаторы кода могут значительно помочь в обнаружении проблем с обработкой ошибок:
1. Clang-Tidy предлагает проверки для выявления неправильного использования std::expected, в том числе пропущенные проверки на ошибки и неправильные преобразования типов.
2. Специализированные чекеры, такие как PCLint или Coverity, могут быть настроены для отслеживания потока ошибок и убеждения, что все пути обработки ошибок должным образом обработаны.
3. Собственные линтеры могут быть разработаны для проверки соблюдения командных соглашений по обработке ошибок с std::expected.
Совместимость с библиотеками обработки ошибок
При интеграции std::expected с существующими фреймворками обработки ошибок стоит учитывать:
1. Адаптеры для системных библиотек — для C API, которые используют коды ошибок (например, POSIX или Windows API), создайте обертки, возвращающие std::expected:
| C++ | 1
2
3
4
5
6
7
| std::expected<size_t, ErrnoError> posix_write(int fd, const void* buf, size_t count) {
ssize_t result = write(fd, buf, count);
if (result < 0) {
return std::unexpected(ErrnoError{errno});
}
return static_cast<size_t>(result);
} |
|
2. Интеграция с логированием — разработайте удобные утилиты для логирования ошибок:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| template <typename T, typename E>
auto log_if_error(std::expected<T, E> exp, const std::string& context) {
if (!exp) {
Logger::log_error("[{}] Error: {}", context, format_error(exp.error()));
}
return exp;
}
// Использование
auto result = read_file(path)
| log_if_error("File reading")
| parse_json
| log_if_error("JSON parsing"); |
|
Грамотное использование std::expected в сочетании с передовыми практиками может значительно повысить надежность, читаемость и сопровождаемость кода. Хотя полный переход на монадический подход к обработке ошибок требует изменения мышления и адаптации существующих практик, результат стоит затраченных усилий, особенно для проектов, где надежность и производительность критически важны.
(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const& astxx::manager::connection::connection(std::basic_string<char, std::char_traits<char>,... Ошибка: E2034 Cannot convert 'int' to 'std::vector<std::vector<TRabbitCell,std::allocator<TRabbitCell>>... Есть двухмерный вектор:
std::vector<std::vector<TRabbitCell> > *cells(5, 10);
Пытаюсь... На основе исходного std::vector<std::string> содержащего числа, создать std::vector<int> с этими же числами подскажите есть вот такая задача.
Есть список .
Создать второй список, в котором будут все эти же... Std::begin() ,std::end(),std::copy ...//
int main()
{
std::vector<double> data;//Работает
cout << std::begin(data);
... Std::bind, std::mem_fun, std::mem_fn В чем разница между функциями std::bind, std::mem_fun, std::mem_fn? Std::unordered_multimap<std::string, std::unordered_multimap<int, int>> Приветствую. Интересует вопрос, как можно обращаться к контейнеру?
Хотелось бы по map, но так не... std::pair<std::list<std::pair< >>::iterator, > ломается при возврате из функции #include <iostream>
#include <list>
#include <string>
#include <utility>
using lp =... Поиск в std::vector < std::pair<UInt32, std::string> > Подскажите пожалуйста, как осуществить поиск элемента в
std::vector < std::pair<UInt32,... std::shared_ptr и std::dynamic_pointer_cast, std::static_pointer_cast и т.д Добрый день. Появился вопрос, операции std::shared_ptr, std::dynamic_pointer_cast,... std::wstring и std::u16string и std::u32string Здравствуйте,
Подскажите пожалуйста, правильно ли я понимаю, что на Windows - std::wstring и... std::all_of, std::any_of, std::none_of Хочу проверить, что не все символы в строке цифры.
можно проверить так:
if... expected primary-expression before "bre" ; expected `;' before "bre" ; `bre' undeclared (first use this function) #include <iostream>
using namespace std;
struct point
{
int x;
int y;
};
int...
|