Давайте возьмём пару от двух типов <YourType, bool> — что вы можете сделать с композицией подобного рода? В этой статье я расскажу вам про std::optional — новый вспомогательный тип, добавленный в C++17. Это обёртка для вашего типа и флаг показывает, инициализировано ваше значение или нет. Давайте посмотрим, где это может быть полезно. Добавлением логических флагов к другим типам вы можете достичь то, что называется "Nullable типы". Как было сказано ранее, флаг используется для обозначения того, доступно значение или нет. Такая обёртка выразительно представляет объект, который может быть пустым (не через комментарии :). Вы можете достигнуть пустого значения объекта с помощью использования уникальных идентификаторов (-1, бесконечность, nullptr), но это не так точно выражает мысль, как отдельный тип-обёртка. Вы даже можете использовать std::unique_ptr<Type> и трактовать пустой указатель как неинициализированный объект — это сработает, но вместе с этим вы должны будете смириться с затратами на выделения памяти для объекта там, где в этом нет необходимости. Опциональные типы — это то, что пришло из мира функционального программирования, принеся с собой безопасность типов и выразительность. Большинство других языков имеют что-то похожее: например std::option в Rust, Optional<T> в Java, Data.Maybe в Haskell. std::optional был добавлен в C++17 из boost::optional, где был доступен многие годы. Начиная с C++17, вы можете просто написать #include <optional> для использования этого типа. Этот тип является типом-значением (value-type) (таким образом, вы можете копировать его). Более того, для std::optional не нужно отдельно выделять память. std::optional является частью словарных типов C++ на ряду с std::any, std::variant и std::string_view. Обычно, опциональный тип может быть использован в следующих сценариях: Мне нравится определение опционального типа из boost, которое подводит итог по тем ситуациям, когда нам следует его использовать. Из документации boost: Хотя иногда тяжело решить, стоит ли использовать опциональный тип, вы точно не должны его использовать для обработки ошибок. Он лучше всего подходит для тех случаев, когда отсутствие значения является нормальным поведением программы. Ниже вы можете увидеть простой пример того, что можно сделать с использованием опционального типа: В коде выше мы объявили функцию, которая возвращает опциональную строку. Если имя пользователя доступно, она вернёт строку. Если нет, то вернёт std::nullopt. Позже мы сможем присвоить это значение опциональному типу и проверить его (у std::optional есть оператор приведения к типу bool), содержит оно реальное значение или нет. Тип std::optional также перегружает operator*() для более простого доступа к содержащемуся значению. В следующих параграфах вы сможете увидеть как создавать std::optional, работать с ним, передавать и даже его производительность, которую вам наверняка интересно увидеть. Эта статья является частью моей серии про библиотечные утилиты C++17. Вот список других тем, про которые я рассказываю: Ресурсы по C++17 STL: OK, теперь давайте поработаем с std::optional. Есть несколько вариантов создания std::optional: Как вы можете видеть в примере выше, вам доступна удивительная гибкость создания объекта. Создание объекта очень простое как для примитивных типов, так и для более сложных. Создание через in place особенно интересно и тег std::in_place также поддерживается в других типах, таких как std::any и std::variant. Например, вы можете написать: Это экономит создание временного объекта Point. Я расскажу про std::in_place позже, не переключайте канал и оставайтесь с нами. Если вы возвращаете опциональное значение из функции, то очень удобно вернуть или std::nullopt, или результирующее значение: В примере выше вы можете видеть, что я возвращаю std::string, полученную из input.asString() и оборачиваю её в std::optional. Если значение недоступно, то функция просто вернёт std::nullopt. Конечно, вы также можете просто объявить пустой опциональный объект в начале вышей функции и присвоить ему посчитанное значение, если оно корректно. Таким образом, мы можем переписать код выше следующим образом: Какая версия лучше, зависит от контекста. Я предпочитаю короткие функции, поэтому мой выбор — версия №1 (с несколькими return). Возможно, самая важная операция для опционального типа (помимо его создания) — это то, как вы можете получить сохранённое значение. Для этого есть несколько вариантов: Чтобы проверить, есть ли реальное значение в объекте, вы можете использовать метод has_value() или просто проверить объект с помощью if (optional) {...}, так как у опционального типа перегружен оператор приведения к bool. Например: Таким образом, наиболее удобно, возможно, будет проверить, есть ли реальное значение в опциональном объекте, и затем использовать его: Давайте посмотрим, какие ещё есть возможности у опционального типа: Если у вас уже существует опциональный объект, вы можете легго поменять его значение с помощью методов emplace, reset, swap и assign. Если вы присваиваете (или обнуляете) объекту std::nullopt, то у реального объекта, который хранится в опциональном, будет вызван деструктор. Вот небольшой пример: Этот код доступен здесь: @Coliru. std::optional позволяет вам сравнивать содержащиеся в нём объекты почти "нормально", но с небольшими исключениями, когда операнды являются std::nullopt. См. ниже: При выполнении кода выше, будет выведено: Этот код доступен здесь: @Coliru. Ниже вы найдёте два примера, где std::optional подходит идеально. Этот код доступен здесь: @Coliru. Этот код доступен здесь: @Coliru. Код выше использует опциональный тип данных для того, чтобы показать, успешно ли выполнено преобразование. Обратите внимание, что на самом деле мы обернули исключения, которые могут быть выброшены C++, в опциональный тип данных, поэтому мы пропустим все ошибки, связанные с этим. Этот момент достаточно спорный, так как обычно мы должны сообщать пользователю об ошибках. Когда вы используете std::optional, вы платите за это увеличенным использованием памяти. Как минимум, одним дополнительным байтом. Если подходить абстрактно, то ваша версия STL может реализовывать опциональный тип данных как: В кратце, std::optional просто оборачивает ваш тип, подготавливает место для него и добавляет один логический параметр. Это означает, что он увеличит размер вашего типа в соответствии с правилами выравнивания. Есть один коментарий для этой конструкции: "Ни одна стандартная библиотека не сможет реализовать std::optional так (она должна использовать union из-за constexpr)". Поэтому код выше просто демонстрирует пример, а не реальную реализацию. Правила выравнивания важны, как говорит стандарт: Например: В то время как bool обычно занимает один байт, опциональный тип занных вынужден подчиняться правилам выравнивания. Таким образом, размер std::optional<T> больше, чем sizeof(T) + 1. Например, если у вас есть такой тип: То он займёт больше места, чем если бы вы использовали свой тип вместо std::optional: В первом случае размер структуры равен 32 байтам! Во втором случае всего лишь 24. Тестовый пример на Compiler Explorer. По ссылке великолепное объяснение насчёт производительности и использованию памяти, взятое из документации boost: вопросы производительности. И в статье "Эффективные опциональные значения" автор рассуждает, как написать обёртку для опционального типа, которая может быть немного быстрее. Интересно, есть ли шанс использовать хоть какую-то магию компилятора и повторно использовать некоторое пространство, чтобы поместить этот дополнительный флаг "инициализации объекта” внутри опционального типа. Тогда бы никакого дополнительного пространства не было бы необходимо. В то время как вы можете использовать std::optional для любого типа, которого захотите, вам надо проявить особое внимание при использовании опционального типа с логическим типом и указателями. std::optional<bool> ob — о чём это говорит? С этой конструкцией вы имеете логический тип с тремя состояниями. Поэтому, если он вам и правда нужен, возможно лучше использовать настоящий троичный тип — std::tribool boost::tribool (правка: Antervis). Более того, использование такого типа может сбивать с толку, потому что ob преобразуется в bool если в нём внутри есть значение и *ob возвращает хранимое значение (если оно доступно). Похожая ситуация может проявиться с указателями: Указатель на int на самом деле является nullable типом, поэтому оборачивание в опциональный тип только усложнит его использование. Фух! Да, это было очень много текста про опциональный тип, но это не всё. Тем не менее, мы рассмотрели основное использование, создание и оперирование эти удобным типом. Я считаю, что у нас есть много случаев, когда опциональный тип подходит намного лучше, чем использование некоторых предопределённых значений для представления nullable типов. Я бы хотел напомнить следующие вещи про опциональный тип: Expert Software Engineer