Рассмотрите применение std::optional::and_then: почему его вызываемый объект должен возвращать std::optional и как это влияет на тип результата?
std::optional::and_then предназначен для последовательного выполнения операций, каждая из которых сама может завершиться отсутствием значения. Если исходный optional пуст, вызываемый объект не выполняется, а результатом становится пустой optional возвращаемого типа. Если значение есть, вызываемый объект получает его и должен вернуть другой optional; вложенность optional<optional<T>> при этом не появляется.
До C++23 последовательность потенциально неуспешных преобразований часто приходилось писать через вложенные проверки, временные переменные или собственные вспомогательные функции. Это увеличивало объём шаблонного кода и повышало риск случайно обратиться к отсутствующему значению.
В C++23 для std::optional появились монадические операции and_then, transform и or_else. Они выражают типовые операции над необязательными значениями, сохраняя проверку наличия значения в стандартном интерфейсе.
Предположим, первый этап разбора возвращает std::optional<int>, а следующий этап также может завершиться неудачей. Простая передача результата следующему вызову должна сохранять единую модель ошибки: отсутствие значения на любом этапе должно приводить к пустому итоговому результату.
Если вместо and_then бездумно использовать transform, функция, возвращающая std::optional<U>, создаст вложенный тип std::optional<std::optional<U>>. Такой результат усложняет дальнейшую обработку и обычно не соответствует смыслу последовательной валидации.
and_then сначала проверяет состояние исходного optional. Для пустого объекта вызываемый объект не вызывается. Стандарт создаёт пустой результат типа, определённого возвращаемым типом вызываемого объекта.
Для содержащего значение объекта вызываемый объект получает это значение с соответствующей категорией доступа: существуют перегрузки для lvalue, const lvalue, rvalue и const rvalue. Это позволяет корректно учитывать копирование, перемещение и квалификаторы исходного optional.
Возвращаемый тип вызываемого объекта должен быть std::optional<U> либо совместимым с требованиями стандартного optional-like-типа. Сам and_then возвращает именно этот тип, а не оборачивает его ещё раз. Поэтому операция «сплющивает» последовательность потенциально неуспешных этапов.
transform решает другую задачу: он применяет функцию к содержащемуся значению и сам оборачивает обычный результат в optional. Если функция уже возвращает optional, для последовательного отказа следует выбирать and_then, а не transform.
Ограничение важно учитывать при проектировании API: and_then не является универсальным обработчиком исключений и не преобразует исключения в nullopt. Если вызываемый объект выбросит исключение, оно распространяется обычным образом.
В сервисе нужно разобрать идентификатор, найти запись в кэше и получить её настройки. Каждый этап может не дать результата. Вариант с ручными if прозрачен, но быстро разрастается при добавлении новых этапов. Вариант с transform удобен для обычных преобразований, однако при возвращении optional приводит к вложенности.
Выбранное решение — цепочка and_then: каждый этап возвращает std::optional одного ожидаемого уровня. Плюсы такого подхода — раннее прекращение вычислений, отсутствие обращений к пустому значению и явная семантика неуспеха. Минусы — необходимость привести функции к совместимым сигнатурам и возможная потеря дополнительной информации об ошибке, поскольку optional различает только наличие и отсутствие значения.
Если диагностические причины отказа важны, вместо optional лучше рассмотреть std::expected, но это уже другая модель результата: она хранит либо значение, либо ошибку.
std::optional пуст?Вызываемый объект не будет вызван. and_then вернёт пустой объект того типа, который возвращает вызываемый объект, поэтому побочные эффекты функции и её возможные исключения в этом случае отсутствуют.
and_then отличается от transform, если callable возвращает обычное значение?and_then для такого callable некорректен, потому что его контракт требует optional-like результата. Для функции, возвращающей обычный U, нужно использовать transform: результатом будет std::optional<U>, если исходный объект содержит значение, и пустой optional в противном случае.
and_then категорию значения исходного объекта?Да, выбор перегрузки учитывает категорию и const-квалификацию исходного optional. Поэтому callable может принимать, например, T&, const T& или T&&; это влияет на возможность изменения или перемещения содержащегося объекта. Неправильно выбранная сигнатура callable приведёт к ошибке соответствия ограничениями, а не к скрытой копии по умолчанию.