Форум программистов, компьютерный форум, киберфорум
bytestream
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

std::expected в C++: Управление ошибками

Запись от bytestream размещена 12.04.2025 в 19:16
Показов 5952 Комментарии 0
Метки c++, c++23, std::expected

Нажмите на изображение для увеличения
Название: 9aacaf76-f3e3-4128-a329-189c8f3d92cb.jpg
Просмотров: 188
Размер:	174.0 Кб
ID:	10583
Обработка ошибок всегда была важной и одновременно сложной задачей в программировании на 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&lt;int&gt; 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&lt;char, std::char_traits&lt;char&gt;,...

Ошибка: E2034 Cannot convert 'int' to 'std::vector<std::vector<TRabbitCell,std::allocator<TRabbitCell>>...
Есть двухмерный вектор: std::vector&lt;std::vector&lt;TRabbitCell&gt; &gt; *cells(5, 10); Пытаюсь...

На основе исходного std::vector<std::string> содержащего числа, создать std::vector<int> с этими же числами
подскажите есть вот такая задача. Есть список . Создать второй список, в котором будут все эти же...

Std::begin() ,std::end(),std::copy
...// int main() { std::vector&lt;double&gt; data;//Работает cout &lt;&lt; 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 &lt;iostream&gt; #include &lt;list&gt; #include &lt;string&gt; #include &lt;utility&gt; using lp =...

Поиск в std::vector < std::pair<UInt32, std::string> >
Подскажите пожалуйста, как осуществить поиск элемента в std::vector &lt; std::pair&lt;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 &lt;iostream&gt; using namespace std; struct point { int x; int y; }; int...

Метки c++, c++23, std::expected
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
[golang] Угол между стрелками часов
alhaos 12.05.2026
По заданным значениям часа и минуты необходимо определить значение меньшего угла между стрелками аналогового циферблата часов. import "math" func angleClock(hour int, minutes int) float64 { . . .
Debian 13: Установка Lazarus QT5
ВитГо 09.05.2026
Эта инструкция моя компиляция инструкций volvo https:/ / www. cyberforum. ru/ blogs/ 203668/ 10753. html и его же старой инструкции по установке Lazarus с gtk2. . .
Нейросеть на алгоритме "эстафета хвоста" как перспектива.
Hrethgir 06.05.2026
На десерт, когда запущу сервер. Статья тут https:/ / habr. com/ ru/ articles/ 1030914/ . Автор я сам, нейросеть только помогает в вопросах которые мне не известны - не знаю людей которые знали-бы. . .
Асинхронный приём данных из COM-порта
Argus19 01.05.2026
Асинхронный приём данных из COM-порта Купил на aliexpress термопринтер QR701. Он оказался странным. Поключил к Arduino Nano. Был очень удивлён. Наотрез отказывается печатать русские буквы. Чтобы. . .
попытка написать игровой сервер на C++
pyirrlicht 29.04.2026
попытка написать игровой сервер на плюсах с открытым бесконечным миром. возможно получится прикрутить интерпретатор питон для кастомизации игровой логики. что есть на текущий момент:. . .
Контроль уникальности выбранного документа-основания при изменении реквизита
Maks 28.04.2026
Алгоритм из решения ниже разработан на примере нетипового документа "ЗаявкаНаРемонтСпецтехники", разработанного в КА2. Задача: уведомлять пользователя, если указанная заявка (документ-основание). . .
Благородство как наказание
Maks 24.04.2026
У хорошего человека отношения с женщинами всегда складываются трудно. А я человек хороший. Заявляю без тени смущения, потому что гордиться тут нечем. От хорошего человека ждут соответствующего. . .
Валидация и контроль данных табличной части документа перед записью
Maks 22.04.2026
Алгоритм из решения ниже реализован на примере нетипового документа, разработанного в КА2. Задача: контроль и валидация данных табличной части документа перед записью с учетом регламента компании. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru