STL

std::optional в C++: где использовать вместо проверок на nullptr

1 просмотров

Введение

Долгое время C++-разработчики выражали «значение может отсутствовать» через указатели и nullptr, через специальные «магические» значения (-1, 0, пустая строка) или через пару «значение + флаг успешности». Все эти подходы работают, но они хрупкие: указатель может означать и «значения нет», и «объект существует где-то на куче», и легко забыть проверку на nullptr. C++17 принёс std::optional<T> — типобезопасную обёртку, которая явно говорит: «здесь либо есть значение типа T, либо его нет».

Концепция

std::optional<T> — это объект, который либо содержит значение типа T (хранится по значению, без выделения памяти на куче), либо находится в состоянии «пусто» (std::nullopt). В отличие от указателя, optional не предполагает владения через сырую память и не требует new/delete. Семантически он ближе к Maybe/Option из функциональных языков.

Главные операции:

  • has_value() или приведение к bool — есть ли значение;
  • value() — получить значение (бросает std::bad_optional_access, если значения нет);
  • operator* и operator-> — доступ без проверки (UB, если значения нет);
  • value_or(default) — получить значение или запасной вариант.

Пример кода

#include <optional>
#include <iostream>
#include <string>
#include <map>

struct User {
    std::string name;
    int age;
};

// Старый подход: указатель + nullptr
const User* findUserOld(const std::map<int, User>& db, int id) {
    auto it = db.find(id);
    if (it == db.end()) {
        return nullptr;
    }
    return &it->second;
}

// Новый подход: std::optional
std::optional<User> findUser(const std::map<int, User>& db, int id) {
    auto it = db.find(id);
    if (it == db.end()) {
        return std::nullopt;
    }
    return it->second; // копия найденного значения
}

std::optional<int> parseInt(const std::string& s) {
    try {
        size_t pos = 0;
        int value = std::stoi(s, &pos);
        if (pos != s.size()) {
            return std::nullopt; // не вся строка распознана
        }
        return value;
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    std::map<int, User> db = {
        {1, {"Alice", 30}},
        {2, {"Bob", 25}}
    };

    if (auto user = findUser(db, 1)) {
        std::cout << "Found: " << user->name << ", age " << user->age << "n";
    } else {
        std::cout << "Not foundn";
    }

    // value_or — удобная альтернатива тернарному оператору
    auto missing = findUser(db, 99);
    std::cout << "Name or default: "
              << (missing ? missing->name : "Unknown") << "n";

    // Парсинг с понятным результатом
    auto n1 = parseInt("42");
    auto n2 = parseInt("abc");
    std::cout << "n1 = " << n1.value_or(-1) << "n";
    std::cout << "n2 = " << n2.value_or(-1) << "n";

    return 0;
}

Пояснения к коду

В findUserOld мы возвращаем сырой указатель на элемент map, и вызывающая сторона обязана не забыть проверить его на nullptr. Кроме того, такой указатель может «протухнуть», если контейнер изменится после вызова. findUser с std::optional<User> явно сигнализирует через тип, что значение может отсутствовать, и при этом возвращает копию данных, а не ссылку на внутреннее хранилище контейнера — то есть результат безопасен в использовании даже после изменения db.

Функция parseInt — классический пример: вместо исключений или флага-указателя на ошибку мы возвращаем optional<int>, и вызывающий код через value_or получает запасное значение без ветвлений try/catch.

Подводные камни и советы

1. optional — это не указатель: при копировании optional<T> копируется и T. Если T — тяжёлый объект, рассмотрите std::optional<std::reference_wrapper<T>> или передачу по указателю, если избыточное копирование критично. 2. operator* и operator-> не проверяют наличие значения — обращение к пустому optional это undefined behavior. Используйте value() (бросает исключение) там, где безопасность важнее производительности, либо явно проверяйте has_value(). 3. Не путайте optional<bool> с bool — тройная логика (нет значения / false / true) иногда неочевидна для читающего код. 4. optional не подходит для отличия «ошибка А» от «ошибки Б» — если нужно несколько причин отсутствия значения, используйте std::expected (C++23) или std::variant<T, Error>. 5. Используйте optional в сигнатурах функций, где раньше был bool tryGet(T& out) или пары (value, found) — это значительно улучшает читаемость вызывающего кода.

================================================================================