Программирование C++Современный C++Разработчик C++ прикладных и библиотечных систем

Что произойдёт с выражением аргументом по умолчанию при вызове std::optional::value or, если объект уже сод...

Что произойдёт с выражением-аргументом по умолчанию при вызове std::optional<T>::value_or, если объект уже содержит значение?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Выражение, переданное в value_or как значение по умолчанию, будет вычислено в любом случае — даже если std::optional уже содержит значение. Метод получает уже вычисленный аргумент обычным способом вызова функции, поэтому value_or не обеспечивает ленивое вычисление запасного варианта.

Исторический контекст

std::optional, появившийся в C++17, предназначен для представления значения, которое может отсутствовать, без использования специальных значений вроде пустой строки, нулевого указателя или «магического» числа. Метод value_or добавили как удобный способ получить хранимое значение либо заранее подготовленную замену.

Исходная проблема — типичный шаблон с проверкой наличия значения: сначала проверить optional, затем вернуть его содержимое или вычислить запасной результат. value_or сокращает такой код, но его интерфейс принимает именно готовое значение, а не функцию, отложенную вычисление.

Постановка проблемы

Если запасной вариант дешёвый, немедленная оценка обычно незаметна. Но выражение по умолчанию может выполнять запрос к базе данных, читать файл, выделять память, менять состояние счётчика или выбрасывать исключение.

В таком случае использование value_or может привести к лишней работе, побочным эффектам или исключению даже при наличии корректного значения внутри optional. Ошибка особенно неприятна тем, что внешне вызов выглядит как условное получение значения.

Подробное решение

Аргументы функции в C++ вычисляются до входа в тело функции. Поэтому сначала вычисляется выражение по умолчанию, затем вызывается value_or, который выбирает между уже вычисленным аргументом и значением из optional.

#include <iostream> #include <optional> #include <string> std::string load_from_disk() { std::cout << "disk access "; return "from disk"; } int main() { std::optional<std::string> name{"from cache"}; auto result = name.value_or(load_from_disk()); std::cout << result << ' '; }

Даже при результате 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-поведения, устраняет лишние сетевые обращения и сохраняет предсказуемую обработку ошибок.

Что кандидаты часто упускают

1. Можно ли передать функцию в value_or, чтобы она вызвалась только при отсутствии значения?

Нет. value_or принимает значение типа, из которого можно получить T, а не специальный ленивый поставщик. Объект-функция сам по себе обычно не преобразуется в T, поэтому такой вызов либо не скомпилируется, либо передаст уже заранее вызванную функцию, если добавить круглые скобки.

Для ленивого вычисления используют явную проверку, собственную функцию-обёртку или в подходящих случаях or_else из C++23. Важно, что or_else сохраняет модель optional: результатом будет optional, который при необходимости затем преобразуют или распаковывают.

2. Как категория значения optional влияет на копирование или перемещение результата?

Вызов на lvalue-объекте обычно копирует хранимое значение, потому что доступ к нему происходит как к lvalue. Вызов на rvalue-объекте может переместить значение из optional, что полезно для тяжёлых или некопируемых ресурсов, если тип и выбранная перегрузка это допускают.

Это влияет на стоимость получения результата, но не на вычисление fallback-аргумента. Даже при перемещении хранимого значения выражение по умолчанию уже было вычислено до входа в value_or.

3. Что произойдёт, если вычисление fallback выбросит исключение, а optional заполнен?

Исключение будет выброшено. Оно возникает во время вычисления аргумента до вызова value_or, поэтому метод не успевает проверить, содержит ли optional значение.

Если исключение допустимо только для пустого optional, вычисление нужно поместить внутрь явной ветки или ленивого механизма. Это также относится к любым побочным эффектам: запись в журнал, сетевой запрос или изменение глобального состояния произойдут независимо от заполненности optional.