TL;DR: посмотрите, как компиляторы не согласны с кодом по ссылке Godbolt: https://godbolt.org/z/f7G6PTEsh
Должен ли std::variant
быть разрушаемым, если его альтернатива имеет потенциально выбрасывающий деструктор? Clang и GCC, похоже, не согласны с этим (вероятно, разные реализации стандартной библиотеки в Godbolt?), MSVC считает, что так и должно быть. cppreference говорит, что деструктор варианта не имеет спецификации noException, и в этом случае деструктор должен быть безоговорочно noException, если только он не имеет потенциально выбрасывающих членов или баз (https://en.cppreference.com/w/cpp/language/noException_spec), который не указан для варианта (ага!).
Причина, по которой я спрашиваю, заключается не в том, чтобы возиться с выбрасыванием чего-либо из деструкторов, а в том, чтобы постепенно реорганизовать странные устаревшие вещи, которые сводятся к следующему фрагменту («спецификация исключений переопределяющей функции более слаба, чем базовая версия»), а также связанная ошибка в устаревшем компиляторе, которую мы пока не можем обновить.
#include <type_traits>
#include <variant>
struct S
{
~S() noexcept(false);
};
static_assert(std::is_nothrow_destructible_v<std::variant<S>>); // assertion failed with clang
struct Y
{
virtual ~Y() {}
};
struct Z: public Y
{
virtual ~Z() {}
// clang error because of this member:
// exception specification of overriding function is more lax than the base version
std::variant<int, S> member;
}
Честно говоря, я считаю, что STL и связанные с ним вещи вообще не смешиваются с выдачей деструкторов, просто иногда я полностью об этом забываю :).
Отвечая на этот вопрос, я теперь убежден, что если деструктор типа равен noexcept(false)
, он все равно может никогда не выдать ошибку, и тогда это допустимый альтернативный тип для варианта.
std::variant
не работает с генерацией деструкторовВо-первых, этот код может иметь неопределенное поведение, поскольку все альтернативные варианты должны соответствовать требованию Cpp17Destructible ([variant.variant.general] p2 ), которое требует ( [tab:cpp17.destructible]) для выражения u.~T()
:
Все ресурсы, принадлежащие
u
, возвращаются, исключение не распространяется.
Если бы ~S()
выдал исключение, поведение было бы неопределенным, хотя в принципе вы можете использовать деструктор noexcept(false)
с std::variant
, если он никогда не выбрасывает.
Я не верю, что результат static_assert(std::is_nothrow_destructible_v<std::variant<S>>);
указан; стандарт просто определяет std::variant::~variant() как:
constexpr ~variant();
... что noexcept
зависит от того, имеют ли члены данных выбрасывающий деструктор (подробнее см. Ниже).
Члены данных являются деталями реализации.
Однако это также похоже на ошибку GCC, потому что [кроме.spec] p8 гласит:
Спецификация исключения для неявно объявленного деструктора или деструктора без спецификатора noException является потенциально выбрасывающей тогда и только тогда, когда любой из деструкторов для любого из его потенциально созданных подобъектов имеет спецификацию потенциально выдающего исключения или деструктор является виртуальным. а деструктор любого виртуального базового класса имеет спецификацию потенциально выдающего исключения.
В libstdc++ деструктор явно задан по умолчанию (<вариант> строка 1512):
_GLIBCXX20_CONSTEXPR ~variant() = default;
В libstdc++ фактический член хранится внутри подобъекта _Uninitialized
, как
union {
_Empty_byte _M_empty;
_Type _M_storage;
};
// ...
~_Uninitialized() {}
Способ воспроизвести эту ситуацию:
#include <type_traits>
struct D {
~D() noexcept(false);
};
struct B {
union {
char c;
D d;
};
~B() {}
};
static_assert(std::is_nothrow_destructible_v<B>);
Утверждение проходит только для GCC и не работает для других компиляторов (https://godbolt.org/z/1doaTrxsG).
Поскольку S
имеет потенциально выбрасывающий подобъект типа D
(косвенно), S
не должен быть разрушаемым с помощью исключения.
Это известная ошибка компилятора; см. Ошибка GCC № 115222.
Но это дефект, да? Поскольку union
не вызывает автоматически деструктор члена, это не должно влиять на отсутствие исключений деструктора включающего класса.
@HolyBlackCat говорит «если есть какие-либо из его потенциально созданных подобъектов», поэтому я считаю, что намерение очень ясно; d
потенциально построен. Вы все равно можете явно отметить деструктор noexcept
. Также обратите внимание, что деструктор будет удален, если он был = default;
или объявлен неявно.
Удаление — это нормально. Просто странно, что что-то, что не выполняется автоматически (уничтожение члена объединения), все еще влияет на исключение.
Это будет неправильно, только если деструктор S
действительно выдаст ошибку. noexcept(false)
деструкторы Cpp17Destructible
есть до тех пор, пока исключение не ускользнет от вызова деструктора
@Артьер, ты прав, спасибо. Я обновил вопрос соответственно.
GCC ошибается. Согласно [res.on.Exception.handling]/3:
Операции деструктора, определенные в стандартной библиотеке C++, не должны вызывать исключений. Каждый деструктор в стандартной библиотеке C++ должен вести себя так, как если бы он имел спецификацию негенерирующих исключений.
То, как объявляется деструктор std::variant
, не имеет значения; разработчик библиотеки отвечает за то, чтобы деструктор вел себя так, как если бы он был noexcept
, везде, где может быть обнаружено наличие или отсутствие такого исключения.
Обратите внимание, что типы, используемые для создания экземпляров шаблонов стандартной библиотеки, не могут создавать исключения ([res.on.functions]/2.4), но это не обязательно относится к вашему примеру. Правило не говорит, что ваш тип S
должен иметь деструктор noexcept
. Там просто написано, что на самом деле бросать нельзя. Нарушение этого правила приводит к UB, только если действительно происходит выполнение, приводящее к выходу исключения из деструктора.
Если LWG3229 разрешается, как предложено, то GCC соответствует, поскольку std::variant
не имеет объявленного деструктора. Формулировка несовершенна.
Мне также не на 100% ясно, означает ли «вести себя так, как если бы у него была спецификация негенерирующего исключения», что noexcept(t.~T())
должно быть true
. Ведь такая проверка в пользовательском коде — это не поведение самого деструктора.
@JanSchultke Я не понимаю, что еще это может означать. Если бы оно просто относилось к тому, что происходит внутри функции, то оно было бы излишним, поскольку в предыдущем предложении уже говорилось, что функции стандартной библиотеки не могут генерировать исключения.
Это не было бы лишним, поскольку это еще больше ограничило бы поведение, подразумевая, что std::terminate
следует использовать, когда исключение, созданное пользователем, достигает деструктора стандартного типа библиотеки, тогда как отсутствие ответа может подразумевать любой возможный способ гарантировать, что оно ничего не выдает. Как бы то ни было, для меня более правдоподобно то, что тест noexcept
предназначен для прохождения, но не сформулирован так, чтобы я был уверен на 100%.
@JanSchultke Я не думаю, что он это сделает, потому что этого не произойдет, если не выдастся деструктор или функция освобождения, что приводит к UB, так что вы все равно не получите гарантию std::terminate
.
Из std::variant "...типы, которые могут храниться в этом варианте. Все типы должны соответствовать требованиям разрушаемости..." и из Разрушаемость "...
u.~T()
Все ресурсы, принадлежащие вам, reclaimed, никаких исключений не создается...» Таким образом, альтернативный тип, который может выдавать исключение при уничтожении, нарушает предварительные условия.