GCC9 позволяет избежать бесполезного состояния std :: option?

14

Недавно я следил за обсуждением Reddit, которое привело к хорошему сравнению std::visitоптимизации по компиляторам. Я заметил следующее: https://godbolt.org/z/D2Q5ED

И GCC9, и Clang9 (я полагаю, они используют один и тот же stdlib) не генерируют код для проверки и создания бесполезного исключения, когда все типы удовлетворяют некоторым условиям. Это приводит к улучшению кода, поэтому я поднял проблему с MSVC STL и получил следующий код:

template <class T>
struct valueless_hack {
  struct tag {};
  operator T() const { throw tag{}; }
};

template<class First, class... Rest>
void make_valueless(std::variant<First, Rest...>& v) {
  try { v.emplace<0>(valueless_hack<First>()); }
  catch(typename valueless_hack<First>::tag const&) {}
}

Утверждалось, что это делает любой вариант бесполезным, и, читая документ, он должен:

Во-первых, уничтожает текущее значение (если оно есть). Затем выполняется прямая инициализация содержащегося значения, как если бы оно создавало значение типа T_Iс аргументами. std::forward<Args>(args)....Если выброшено исключение, *thisможет стать valueless_by_exception.

Что я не понимаю: почему это называется «май»? Законно ли оставаться в старом состоянии, если вся операция скинута? Потому что это то, что делает GCC:

  // For suitably-small, trivially copyable types we can create temporaries
  // on the stack and then memcpy them into place.
  template<typename _Tp>
    struct _Never_valueless_alt
    : __and_<bool_constant<sizeof(_Tp) <= 256>, is_trivially_copyable<_Tp>>
    { };

И позже он (условно) делает что-то вроде:

T tmp  = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);

Следовательно, в основном это создает временное и, если это удается, копирует / перемещает его в реальное место.

ИМО это нарушение "Во-первых, уничтожает текущее значение", как указано в документе. Когда я прочитал стандарт, то после a v.emplace(...)текущее значение в варианте всегда уничтожается, и новый тип является либо установленным типом, либо не имеет значения.

Я понимаю, что условие is_trivially_copyableисключает все типы, которые имеют наблюдаемый деструктор. Так что это также может быть, как: «как-будто вариант повторно инициализируется со старым значением» или около того. Но состояние варианта является наблюдаемым эффектом. Так действительно ли стандарт позволяет, что emplaceне меняет текущее значение?

Изменить в ответ на стандартную цитату:

Затем инициализирует содержащееся в нем значение, как если бы он выполнял прямую инициализацию без списка значений типа TI с аргументами std​::​forward<Args>(args)....

T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);Действительно ли это считается верной реализацией вышесказанного? Это то, что подразумевается под «как будто»?

Flamefire
источник

Ответы:

7

Я думаю, что важной частью стандарта является следующее:

С https://timsong-cpp.github.io/cppwp/n4659/variant.mod#12

23.7.3.4 Модификаторы

(...)

шаблон option_alternative_t> & emplace (Args && ... args);

(...) Если во время инициализации содержащегося значения генерируется исключение, вариант может не содержать значение

Он говорит «может», а не «должен». Я ожидал бы, что это будет намеренно, чтобы позволить реализации, подобные той, что используется gcc.

Как вы упомянули сами, это возможно только в том случае, если деструкторы всех альтернатив тривиальны и, следовательно, ненаблюдаемы, поскольку требуется уничтожение предыдущего значения.

Дополнительный вопрос:

Then initializes the contained value as if direct-non-list-initializing a value of type TI with the arguments std​::​forward<Args>(args)....

Есть ли T tmp {std :: forward (args) ...}; this-> value = std :: move (tmp); действительно считается действительной реализацией вышесказанного? Это то, что подразумевается под «как будто»?

Да, потому что для типов, которые легко копируются, нет способа обнаружить разницу, поэтому реализация ведет себя так, как если бы значение было инициализировано, как описано. Это не сработало бы, если бы тип не был тривиально копируемым.

PaulR
источник
Интересно. Я обновил вопрос с последующей просьбой / разъяснением. Корень: разрешено ли копирование / перемещение? Я очень озадачен might/mayформулировкой, поскольку в стандарте не указывается, что является альтернативой.
Flamefire
Принимая это для стандартной цитаты и there is no way to detect the difference.
Пламенный огонь
5

Так действительно ли стандарт позволяет, что emplaceне меняет текущее значение?

Да. emplaceдолжен обеспечивать основную гарантию отсутствия утечек (то есть соблюдение срока службы объекта, когда строительство и разрушение вызывают наблюдаемые побочные эффекты), но, когда это возможно, разрешается предоставлять надежную гарантию (т. е. исходное состояние сохраняется в случае сбоя операции).

variantдолжен вести себя аналогично объединению - альтернативы размещаются в одном регионе с соответствующим распределенным хранилищем. Не разрешается выделять динамическую память. Следовательно, изменение типа emplaceне может сохранить исходный объект без вызова дополнительного конструктора перемещения - оно должно уничтожить его и создать новый объект вместо него. Если эта конструкция терпит неудачу, то вариант должен перейти в исключительное бесполезное состояние. Это предотвращает такие странные вещи, как уничтожение несуществующего объекта.

Однако для небольших тривиально копируемых типов можно обеспечить надежную гарантию без слишком больших накладных расходов (в данном случае даже повышения производительности для избежания проверки). Поэтому реализация делает это. Это соответствует стандарту: реализация по-прежнему обеспечивает базовую гарантию, как того требует стандарт, просто в более удобной для пользователя форме.

Изменить в ответ на стандартную цитату:

Затем инициализирует содержащееся в нем значение, как если бы он выполнял прямую инициализацию без списка значений типа TI с аргументами std​::​forward<Args>(args)....

T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);Действительно ли это считается верной реализацией вышесказанного? Это то, что подразумевается под «как будто»?

Да, если назначение перемещения не дает видимого эффекта, как в случае тривиально копируемых типов.

LF
источник
Я полностью согласен с логическими рассуждениями. Я просто не уверен, что это на самом деле в стандарте? Вы можете поддержать это чем-нибудь?
Flamefire
@ Flamefire Хмм ... В общем, стандартные функциональные возможности обеспечивают основную гарантию (если нет ничего плохого в том, что предоставляет пользователь), и std::variantне имеют причин нарушать это. Я согласен, что это можно сделать более четко в формулировке стандарта, но именно так работают другие части стандартной библиотеки. И к вашему сведению, P0088 был первоначальным предложением.
LF
Спасибо. Внутри есть более четкая спецификация: if an exception is thrown during the call toT’s constructor, valid()will be false;так что это запретило эту «оптимизацию»
Flamefire
Да. Спецификация emplaceв P0088 подException safety
Flamefire
@Flamefire Кажется, что есть несоответствие между первоначальным предложением и версией, за которую проголосовали. Окончательная версия изменилась на формулировку «может».
LF