библиотека — типы общего назначенияlibrary — general-purpose types
Тип «значение T или его отсутствие» без указателей, без sentinel-значений (вроде -1 или nullptr) и без отдельного bool-флага рядом со значением. Стековый, не аллоцирует кучу.A "value T or its absence" type without pointers, without sentinel values (like -1 or nullptr), and without a separate bool flag next to the value. Stack-allocated, no heap allocation.
std::optional<T> — обёртка, хранящая либо значение типа T, либо ничего. В отличие от указателя, она не подразумевает владения кучей и не требует аллокации — значение (если оно есть) лежит прямо внутри optional, на стеке или там же, где лежал бы сам T. Проверка наличия — через has_value() или неявное преобразование к bool; явное «пусто» задаётся через std::nullopt.std::optional<T> is a wrapper holding either a value of type T or nothing at all. Unlike a pointer, it implies no heap ownership and requires no allocation — the value (if present) lives directly inside the optional, on the stack or wherever a plain T would live. Presence is checked via has_value() or an implicit conversion to bool; explicit "empty" is expressed with std::nullopt.
// вариант 1: указатель — неясно владение, нужна аллокация// option 1: a pointer — ownership is unclear, needs allocation
Config* findConfig(const std::string& key);
// вариант 2: sentinel-значение — теряет часть диапазона значений// option 2: a sentinel value — eats into the value range
size_t findIndex(const std::vector<int>& v, int x) {
// возвращает (size_t)-1, если не найдено — легко забыть проверить// returns (size_t)-1 if not found — easy to forget to check
}
// вариант 3: пара (T, bool) — многословно, легко перепутать порядок// option 3: a (T, bool) pair — verbose, easy to mix up the order
std::pair<int, bool> tryParse(const std::string& s);
std::optional<Config> findConfig(const std::string& key);
std::optional<size_t> findIndex(const std::vector<int>& v, int x);
std::optional<int> tryParse(const std::string& s);
if (auto cfg = findConfig("timeout")) {
use(*cfg); // разыменование — как у указателя, но без кучи// dereference — pointer-like, but no heap involved
}
int timeout = tryParse(s).value_or(30); // значение по умолчанию одной строкой// a default value in one line
«Может не быть значения» — одна из самых частых потребностей в API, и до C++17 все её выражения были компромиссом:"Might not have a value" is one of the most common needs in API design, and before C++17 every way of expressing it was a compromise:
Указатель одновременно кодирует и «значения может не быть», и «кто владеет объектом» (нужно ли delete). Функция, возвращающая T*, чтобы просто сказать «может, ничего нет» — заставляет вызывающего разбираться, что делать с этим указателем дальше, хотя владение тут вообще не при чём.A pointer simultaneously encodes both "the value might be absent" and "who owns the object" (does it need a delete). A function returning T* just to say "there might be nothing" forces the caller to figure out what to do with that pointer afterward, even though ownership was never actually the point.
-1 для «не найдено» в функции, возвращающей int, работает только пока -1 не является легальным результатом. Для size_t это ещё более хрупко: (size_t)-1 — валидное, хоть и огромное число, если его случайно использовать как индекс без проверки, будет UB или падение вместо ясной ошибки.Using -1 for "not found" in a function returning int only works as long as -1 isn't a legitimate result. For size_t it's even more fragile: (size_t)-1 is a valid, if enormous, number — used accidentally as an index without a check, it produces UB or a crash instead of a clear error.
std::optional<T> в возвращаемом типе или параметре функции документирует намерение прямо в сигнатуре — не нужно читать реализацию или комментарий, чтобы понять, что результат может отсутствовать и это нормальный, ожидаемый исход, а не ошибка.std::optional<T> in a return type or parameter documents intent right in the signature — you don't need to read the implementation or a comment to understand that the result might be absent, and that this is a normal, expected outcome rather than an error.
Как отмечено в заметках по TS-расширениям, optional — одна из утилит, доехавших до стандарта из экспериментального Library Fundamentals TS v1 (2015) почти без изменений семантики: std::experimental::optional в legacy-коде до C++17 ведёт себя практически идентично финальному std::optional.As noted in the TS-extensions notes, optional is one of the utilities that made it into the standard from the experimental Library Fundamentals TS v1 (2015) with almost no change in semantics — std::experimental::optional in pre-C++17 legacy code behaves nearly identically to the final std::optional.
Внутри — по сути тегированное хранилище: буфер размером с T (с учётом выравнивания) плюс bool-флаг наличия значения, обёрнутые так, чтобы конструктор/деструктор T вызывались вручную (через placement new) только тогда, когда значение реально присутствует. Разница между четырьмя основными способами доступа — в том, что происходит при отсутствии значения:Under the hood it's essentially a tagged storage: a buffer sized for T (respecting alignment) plus a bool flag for presence, wrapped so that T's constructor/destructor are invoked manually (via placement new) only when a value is actually present. The difference between the four main ways to access it is what happens when the value is absent:
std::optional<int> opt = std::nullopt;
// value() — кидает исключение std::bad_optional_access, если пусто// value() — throws std::bad_optional_access if empty
try {
int x = opt.value();
} catch (const std::bad_optional_access& e) {
// безопасно, но с overhead исключения// safe, but with exception overhead
}
// operator* / operator-> — UB, если пусто (как разыменование nullptr)// operator* / operator-> — UB if empty (like dereferencing nullptr)
int y = *opt; // UB — использовать только после проверки has_value()// UB — only use after checking has_value()
// value_or(default) — безопасно, без исключений, без UB// value_or(default) — safe, no exceptions, no UB
int z = opt.value_or(0);
// has_value() / operator bool — явная проверка перед разыменованием// has_value() / operator bool — an explicit check before dereferencing
if (opt.has_value()) { use(*opt); }
Ключевой момент: value() и operator* — не взаимозаменяемые способы «на всякий случай» получить значение. Это два разных контракта: value() — «я не уверен, что там есть значение, скажи мне об этом исключением», operator* — «я уже проверил, дай доступ максимально дёшево».The key point: value() and operator* aren't interchangeable "just in case" ways to get the value. They're two different contracts: value() means "I'm not sure there's a value, tell me via an exception"; operator* means "I've already checked, give me access as cheaply as possible."
Минимальная версия без allocator-параметра, без exception-safety гарантий на все операции и без constexpr — этого достаточно, чтобы показать, что optional не аллоцирует и вручную управляет временем жизни T через placement new/явный деструктор.A minimal version with no allocator parameter, no exception-safety guarantees on every operation, and no constexpr — enough to show that optional doesn't allocate and manually manages T's lifetime via placement new / an explicit destructor call.
my_optional.hpp
template <class T>
class my_optional {
alignas(T) unsigned char storage_[sizeof(T)];
bool has_value_ = false;
T* ptr() { return reinterpret_cast<T*>(storage_); }
public:
my_optional() noexcept = default;
my_optional(const T& v) { new (storage_) T(v); has_value_ = true; }
~my_optional() { reset(); }
bool has_value() const noexcept { return has_value_; }
explicit operator bool() const noexcept { return has_value_; }
T& value() {
if (!has_value_) throw std::bad_optional_access{};
return *ptr();
}
T& operator*() noexcept { return *ptr(); } // UB, если пусто — как у указателя// UB if empty — pointer-like
T* operator->() noexcept { return ptr(); }
T value_or(T fallback) const {
return has_value_ ? *reinterpret_cast<const T*>(storage_) : fallback;
}
void reset() noexcept {
if (has_value_) { ptr()->~T(); has_value_ = false; }
}
};
Что обычно проверяют дополнительным вопросом: почему нельзя просто хранить T value_; bool has_value_; без placement new — потому что тогда T обязан иметь конструктор по умолчанию (или пришлось бы всегда конструировать «мусорное» значение), и деструктор T вызывался бы для несуществующего объекта при уничтожении optional. Буфер + placement new позволяют не конструировать T вообще, пока значения нет.What's usually probed with a follow-up: why not just store T value_; bool has_value_; without placement new — because then T would need a default constructor (or you'd always have to construct a "garbage" value), and T's destructor would run on a non-existent object when the optional is destroyed. The raw buffer + placement new lets you avoid constructing T at all while there's no value.
Стандарт явно не разрешает std::optional<T&> — хранение ссылки внутри optional. Причина в том, что ссылку нельзя переприсвоить после инициализации, а optional должен поддерживать пересвоение (opt = other_value;), включая переход из «пусто» в «есть значение» и обратно — семантика ссылки этому противоречит. Частый обходной путь — std::optional<std::reference_wrapper<T>> либо просто указатель, если владение не нужно.The standard explicitly disallows std::optional<T&> — storing a reference inside an optional. The reason is that a reference can't be reseated after initialization, while optional must support reassignment (opt = other_value;), including transitioning from "empty" to "has a value" and back — reference semantics conflict with that. A common workaround is std::optional<std::reference_wrapper<T>>, or just a plain pointer if ownership isn't a concern.
Использовать value() внутри try/catch там, где до этого уже был явный if (opt) — лишний overhead исключений там, где он не нужен. Использовать *opt без предварительной проверки has_value() — UB, которое на практике часто «работает» на мусорных данных, пока не сработает в проде на пустом optional.Using value() inside a try/catch when there was already an explicit if (opt) beforehand is unnecessary exception overhead. Using *opt without first checking has_value() is UB that in practice often "works" on garbage data — until it fails in production on an empty optional.
std::optional<bool> технически валиден, но создаёт три состояния (нет значения / false / true) там, где читатель кода интуитивно ожидает два — источник логических ошибок при проверке if (opt) вместо if (opt.has_value()). optional<optional<T>> компилируется, но обычно сигнал, что стоит пересмотреть дизайн — два независимых «пусто» на разных уровнях редко нужны одновременно.std::optional<bool> is technically valid but creates three states (no value / false / true) where readers intuitively expect two — a source of logic bugs when checking if (opt) instead of if (opt.has_value()). optional<optional<T>> compiles, but is usually a signal to reconsider the design — two independent "empty" states at different levels are rarely both needed at once.