Почему 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. Я еще не нашел дискуссий о том, почему это является улучшением, поэтому на данный момент я буду размышлять: было легко предоставить как бросковые, так и неметающие аксессоры, которые удовлетворяют как лагерям обработки ошибок (дозорные значения, так и исключения), и это кроме того, помогает вывести некоторое неопределенное поведение из языка, поэтому вам не нужно стрелять в ногу, если вы решите пойти на потенциально метательный маршрут.