Почему std :: get для варианта бросает на отказ, а не неопределенное поведение?
Согласно cppreference std::get
для variant
throws std::bad_variant_access
если тип, содержащийся в variant
, не является ожидаемым. Это означает, что стандартная библиотека должна проверять каждый доступ (libc++).
Каково было обоснование этого решения? Почему это не неопределенное поведение, как и везде в C++? Могу ли я обойти это?
Ответы
Ответ 1
Кажется, я нашел это!
Похоже, что причина может быть найдена в разделе "Различия с пересмотром 5" в предложении:
Компромисс Kona: f! V.valid(), make get <...> (v) и посещать (v) бросок.
Смысл - чтобы вариант должен был вставить состояние values_by_exception. Используя то же самое, if
мы всегда можем бросить.
Даже зная это рациональное, я лично хотел бы избежать этой проверки. *get_if
работающий от ответа Джастина, кажется мне хорошим enougth для меня (по крайней мере для библиотечного кода).
Ответ 2
В текущем API для std::variant
нет непроверенной версии std::get
. Я не знаю, почему это было стандартизировано именно так; все, что я говорю, просто догадывается.
Однако вы можете приблизиться к желаемому поведению, написав *std::get_if<T>(&variant)
. Если variant
не удерживает T
в то время, std::get_if<T>
возвращает nullptr
, поэтому разыменованием является неопределенное поведение. Таким образом, компилятор может предположить, что вариант имеет T
На практике это не самая простая оптимизация для компилятора. По сравнению с простым помеченным объединением, код, который он испускает, может быть не таким хорошим. Следующий код:
int const& get_int(std::variant<int, std::string> const& variant)
{
return *std::get_if<int>(&variant);
}
Испускает это с помощью clang 5.0.0:
get_int(std::variant<int, std::string> const&):
xor eax, eax
cmp dword ptr [rdi + 24], 0
cmove rax, rdi
ret
Он сравнивает индекс варианта и условно перемещает возвращаемое значение, когда индекс верен. Несмотря на то, что UB для индекса был неправильным, в настоящее время clang не может оптимизировать сравнение.
Интересно, что возвращение int
вместо ссылки оптимизирует проверку:
int get_int(std::variant<int, std::string> const& variant)
{
return *std::get_if<int>(&variant);
}
Выдает:
get_int(std::variant<int, std::string> const&):
mov eax, dword ptr [rdi]
ret
Вы можете помочь компилятору, используя __builtin_unreachable()
или __assume
, но gcc в настоящий момент является единственным компилятором, способным удалить проверки, когда вы это сделаете.
Ответ 3
Почему это не неопределенное поведение, как и везде в c++? Могу ли я обойти это?
Да, есть обходное решение. Если вам не нужна безопасность типа, используйте простой union
вместо std::variant
. Как говорится в ссылке, которую вы цитировали:
Шаблон класса std :: variant представляет собой тип безопасного объединения.
Цель union
заключалась в том, чтобы иметь единственный объект, который мог бы принимать значения из одного из нескольких разных типов. Только один тип union
был "действительным" в любой момент времени в зависимости от того, какие переменные-члены были назначены:
union example {
int i;
float f;
};
// code block later...
example e;
e.i = 10;
std::cout << e.f << std::endl; // will compile but the output is undefined!
std::variant
обобщает union
при добавлении безопасности типа, чтобы убедиться, что вы только получаете доступ к правильному типу данных. Если вы не хотите этой безопасности, вы всегда можете использовать union
.
Что было рациональным для этого решения?
Я лично не знаю, в чем причина этого решения, но вы всегда можете взглянуть на документы из комитета по стандартизации c++, чтобы получить представление о процессе.
Ответ 4
Каково было обоснование этого решения?
На этот вопрос всегда трудно ответить, но я дам ему шанс.
Много вдохновения для поведения std::variant
произошло из поведения std::optional
, как указано в предложении для std::variant
, P0088:
Это предложение пытается применить уроки, извлеченные из optional
...
И вы можете видеть параллели между двумя типами:
- Вы не знаете, что в настоящее время проводится
- в
optional
nullopt_t
это либо тип, либо ничего (nullopt_t
) - в
variant
это либо один из многих типов, либо ничего (см. valueless_by_exception
)
- Все функции для работы с типом отмечены как
constexpr
- Это может показаться совпадением или просто хорошей практикой проектирования, но было очень ясно, что этот
variant
следует за optional
руководством по этому вопросу (см. Связанное предложение выше)
- Каждый из них обеспечивает способ проверки пустоты
-
std::optional
имеет неявное преобразование в bool
или, альтернативно, функцию has_value
-
std::variant
имеет значение valueless_by_exception
которое сообщает вам, является ли вариант пустым, потому что построение активного типа вызвало исключение
- Каждый из них обеспечивает способ метания и не метания доступа
- Потенциально бросающий доступ для
std::optional
является value
и он может bad_optional_access
- Потенциально бросающий доступ для
std::variant
- get
и он может бросать bad_variant_access
- Non-throwing (я использую этот термин немного свободно) для
std::optional
является value_or
который может вернуть вам альтернативу (которую вы передаете), если optional
пуст -
get_if
доступ для std::variant
- get_if
который возвращает nullptr
если указатель или тип предоставлены плохо.
На самом деле сходства были настолько преднамеренными, что причиной несогласия в базовых классах, используемых для optional
и variant
были жалобы (см. Это обсуждение в Google Groups)
Поэтому, чтобы ответить на ваш вопрос, он выбрасывает, поскольку optional
броски. Имейте в виду, что поведение броска должно встречаться редко; вы должны использовать шаблон посетителя с вариантом, и даже если вы звоните get
это только броски, если вы предоставите ему индекс, который является размером списка типа, или запрошенный типа не является активным. Все другие злоупотребления считаются плохо сформированными и должны приводить к ошибке компилятора.
Что касается того, почему std::optional
бросает, если вы проверяете его предложение, N3793, имеющее метапотребитель, рекламируется как улучшение по сравнению с Boost.Optional, из которого родилась std::optional
. Я еще не нашел дискуссий о том, почему это является улучшением, поэтому на данный момент я буду размышлять: было легко предоставить как бросковые, так и неметающие аксессоры, которые удовлетворяют как лагерям обработки ошибок (дозорные значения, так и исключения), и это кроме того, помогает вывести некоторое неопределенное поведение из языка, поэтому вам не нужно стрелять в ногу, если вы решите пойти на потенциально метательный маршрут.