В обработчике событий почему std::variant иногда может оказаться без активной альтернативы после исключения?
std::variant может перейти в состояние valueless_by_exception, если операция смены активной альтернативы сначала уничтожает старое значение, а создание или перемещение новой альтернативы завершается исключением. В этом состоянии у варианта нет активного значения: index() возвращает std::variant_npos, а std::visit и доступ через std::get завершаются исключением std::bad_variant_access.
Восстановить объект можно, успешно создав в нём другую альтернативу через emplace или присваивание. Код обязан явно учитывать такое состояние, если конструкторы или операции перемещения альтернатив могут выбрасывать исключения.
До появления std::variant для представления «одно значение из нескольких типов» часто применяли union, указатель на базовый класс или ручную систему тегов. Такие решения требовали самостоятельно отслеживать активный тип, корректно управлять временем жизни объектов и обрабатывать исключения.
std::variant, появившийся в C++17, объединил хранение значения, информацию об активной альтернативе и типобезопасный доступ. Однако сильная гарантия исключений не всегда возможна: при смене типа объекту иногда приходится сначала разрушить старую альтернативу, после чего создание новой может завершиться ошибкой.
Пусть вариант содержит объект типа A, а ему присваивают значение типа B. Если B нельзя безопасно подготовить заранее или его конструктор перемещения может выбросить исключение, реализация может разрушить A и начать создание B непосредственно внутри варианта.
Если создание B прерывается исключением, восстановить A автоматически может быть невозможно. Вариант остаётся корректным объектом, но временно не содержит ни одной альтернативы. Игнорирование этого состояния приводит к неожиданным исключениям в коде обработки событий или маршрутизации сообщений.
Состояние проверяется методом valueless_by_exception(). Дополнительно index() возвращает специальное значение std::variant_npos, но для проверки состояния предпочтителен именно именованный метод.
После неудачного emplace вызов std::visit для такого объекта выбросит std::bad_variant_access; пользовательский обработчик не получит фиктивное значение. Аналогично, std::get<T> и std::get<I> не могут вернуть альтернативу, которой фактически нет.
Состояние возникает не при любой ошибке. Если новая альтернатива может быть создана во временном объекте до разрушения старой, реализация способна сохранить исходное значение. Вероятность перехода в valueless_by_exception повышается при выбрасывающем перемещающем конструкторе или при операциях, где создание новой альтернативы происходит непосредственно в хранилище варианта.
Практическая стратегия состоит в том, чтобы делать альтернативы максимально безопасными для перемещения, проектировать их операции перемещения как noexcept, когда это корректно, и явно восстанавливать вариант после исключения. Нельзя считать, что проверка index() перед std::visit полностью решает задачу: между проверкой и использованием объект может быть изменён другим кодом, а в многопоточном сценарии доступ всё равно должен быть синхронизирован.
В диспетчере событий вариант хранит состояния соединения: рабочее состояние и состояние ошибки с дополнительным диагностическим объектом. При переходе в состояние ошибки его конструктор выделяет память и иногда выбрасывает исключение.
Первый вариант — вызывать std::visit без проверки. Он прост и быстр в обычном случае, но при неудачном переходе приводит к std::bad_variant_access уже в другом месте, скрывая исходную причину.
Второй вариант — хранить состояния через std::unique_ptr. Это уменьшает риск разрушения активной альтернативы при смене состояния, но добавляет динамическое выделение памяти, косвенный доступ и отдельную обработку владения.
Выбранный вариант — оставить std::variant, сделать перемещение диагностического объекта noexcept, где это допускает его инвариант, а после исключения перехода переводить вариант в заранее подготовленное безопасное состояние. Такой подход сохраняет компактное хранение и типобезопасный диспетчинг, но не маскирует невозможность построить новое состояние.
1. Всегда ли исключение при присваивании новой альтернативы делает variant пустым?
Нет. Реализация может сначала создать новую альтернативу во временном объекте, а затем заменить старую. В таком случае при исключении исходное значение сохраняется. Переход в valueless_by_exception зависит от конкретной операции и свойств типов альтернатив, а не является обязательным результатом любого исключения.
2. Можно ли вызвать std::visit и проверить результат внутри visitor?
Нет. Если вариант уже находится в состоянии valueless_by_exception, visitor вообще не будет вызван: std::visit выбросит std::bad_variant_access до передачи управления visitor. Проверять и восстанавливать состояние нужно до вызова visitor либо перехватывать исключение на границе обработчика.
3. Чем отличается valueless_by_exception от альтернативы, специально моделирующей ошибку?
valueless_by_exception — аварийное промежуточное состояние, возникшее из-за неудачной операции над самим вариантом. Альтернатива вроде Error — обычное значение доменной модели, которое можно обрабатывать штатно и передавать через std::visit.
Если ошибка является ожидаемой частью протокола, её обычно лучше представить отдельной альтернативой или использовать std::expected, когда требуется результат либо ошибка. Состояние valueless_by_exception всё равно нужно учитывать как отдельный сбой гарантии исключений, а не использовать вместо него как бизнес-состояние.