Что произойдёт с выражением-аргументом по умолчанию при вызове std::optional<T>::value_or, если объект уже содержит значение?
Выражение, переданное в value_or как значение по умолчанию, будет вычислено в любом случае — даже если std::optional уже содержит значение. Метод получает уже вычисленный аргумент обычным способом вызова функции, поэтому value_or не обеспечивает ленивое вычисление запасного варианта.
std::optional, появившийся в C++17, предназначен для представления значения, которое может отсутствовать, без использования специальных значений вроде пустой строки, нулевого указателя или «магического» числа. Метод value_or добавили как удобный способ получить хранимое значение либо заранее подготовленную замену.
Исходная проблема — типичный шаблон с проверкой наличия значения: сначала проверить optional, затем вернуть его содержимое или вычислить запасной результат. value_or сокращает такой код, но его интерфейс принимает именно готовое значение, а не функцию, отложенную вычисление.
Если запасной вариант дешёвый, немедленная оценка обычно незаметна. Но выражение по умолчанию может выполнять запрос к базе данных, читать файл, выделять память, менять состояние счётчика или выбрасывать исключение.
В таком случае использование value_or может привести к лишней работе, побочным эффектам или исключению даже при наличии корректного значения внутри optional. Ошибка особенно неприятна тем, что внешне вызов выглядит как условное получение значения.
Аргументы функции в C++ вычисляются до входа в тело функции. Поэтому сначала вычисляется выражение по умолчанию, затем вызывается value_or, который выбирает между уже вычисленным аргументом и значением из optional.
Даже при результате from cache программа напечатает disk access. Запасная строка создаётся до вызова value_or, а затем отбрасывается.
Для ленивого поведения нужно явно разделить проверку и вычисление: если optional содержит значение, вернуть его; иначе вызвать функцию получения запасного результата. В C++23 для некоторых сценариев можно использовать optional::or_else, который принимает вызываемый объект и запускает его только при отсутствии значения, но он возвращает optional, а не непосредственно T.
Есть и дополнительные особенности. value_or возвращает значение типа T, поэтому получение из const или обычного lvalue-optional обычно приводит к копированию содержащегося объекта, а вызов на rvalue-optional позволяет переместить его. Это не меняет главного свойства: аргумент по умолчанию вычисляется заранее.
Сервис сначала ищет настройки пользователя в памяти, а при отсутствии должен читать их из удалённого хранилища. Разработчик записывает получение настройки через value_or, передавая вызов клиента хранилища как запасной аргумент.
Рассматривались два варианта:
value_or напрямую. Код короткий и хорошо подходит для уже готового дешёвого значения, но удалённое хранилище будет запрашиваться даже при попадании в кэш. Это создаёт задержку и лишнюю нагрузку.value_or. Вычисление действительно можно было бы отложить, но value_or не вызывает переданный объект автоматически: он ожидает значение, совместимое с T, а не фабрику значения.Выбран третий вариант. Он точно выражает требование ленивого fallback-поведения, устраняет лишние сетевые обращения и сохраняет предсказуемую обработку ошибок.
value_or, чтобы она вызвалась только при отсутствии значения?Нет. value_or принимает значение типа, из которого можно получить T, а не специальный ленивый поставщик. Объект-функция сам по себе обычно не преобразуется в T, поэтому такой вызов либо не скомпилируется, либо передаст уже заранее вызванную функцию, если добавить круглые скобки.
Для ленивого вычисления используют явную проверку, собственную функцию-обёртку или в подходящих случаях or_else из C++23. Важно, что or_else сохраняет модель optional: результатом будет optional, который при необходимости затем преобразуют или распаковывают.
optional влияет на копирование или перемещение результата?Вызов на lvalue-объекте обычно копирует хранимое значение, потому что доступ к нему происходит как к lvalue. Вызов на rvalue-объекте может переместить значение из optional, что полезно для тяжёлых или некопируемых ресурсов, если тип и выбранная перегрузка это допускают.
Это влияет на стоимость получения результата, но не на вычисление fallback-аргумента. Даже при перемещении хранимого значения выражение по умолчанию уже было вычислено до входа в value_or.
optional заполнен?Исключение будет выброшено. Оно возникает во время вычисления аргумента до вызова value_or, поэтому метод не успевает проверить, содержит ли optional значение.
Если исключение допустимо только для пустого optional, вычисление нужно поместить внутрь явной ветки или ленивого механизма. Это также относится к любым побочным эффектам: запись в журнал, сетевой запрос или изменение глобального состояния произойдут независимо от заполненности optional.