Чи дозволяється GCC9 уникати безцінного стану std :: variant?


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);

Отже, в основному він створює тимчасовий характер, і якщо це вдасться копіювати / переміщує його на справжнє місце.

IMO це порушення "По-перше, знищує значення, що міститься в даний момент", як заявляє документ. Коли я читаю стандарт, то після v.emplace(...)поточного значення у варіанті завжди руйнується, а новий тип або заданий тип, або безцінний.

Я розумію, що умова is_trivially_copyableвиключає всі типи, у яких спостерігається деструктор. Тож це може бути хоч як: "варіант-as-if реініціалізується зі старим значенням" чи так. Але стан варіанту - ефект, що спостерігається. Тож чи дійсно стандарт дозволяє, що emplaceне змінює поточне значення?

Редагувати у відповідь на стандартну цитату:

Потім ініціалізує міститься значення, як ніби прямо-не-список - ініціалізує значення типу TI з аргументами std​::​forward<Args>(args)....

Чи T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);дійсно вважається дійсною реалізацією вищезазначеного? Це те, що означає "наче"?

Відповіді:


7

Я думаю, що важливою частиною стандарту є:

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

23.7.3.4 Моделі

(...)

template variant_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 :: вперед (args) ...}; this-> value = std :: move (tmp); насправді розцінюються як дійсне виконання вищезазначеного? Це те, що означає "наче"?

Так, оскільки для типів, які можна скопіювати тривіально , немає можливості виявити різницю, тому реалізація веде себе так, ніби значення було ініціалізовано, як описано. Це не працює, якби тип не був тривіально скопійований.


Цікаво. Я оновив запитання із подальшим запитом на уточнення / уточнення. Корінь такий: чи дозволено копіювання / переміщення? Мене дуже бентежить might/mayформулювання, оскільки стандарт не вказує, що таке альтернатива.
Полум'я вогню

Приймаючи це за стандартну цитату і 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);дійсно вважається дійсною реалізацією вищезазначеного? Це те, що означає "наче"?

Так, якщо призначення переміщення не дає ефекту спостереження, що стосується тривіально копіюваних типів.


Я повністю згоден з логічними міркуваннями. Я просто не впевнений, що це насправді в стандарті? Чи можете ви підкріпити це чим-небудь?
Полум'я вогню

@Flamefire Хм ... Взагалі, стандартні функції забезпечують основну гарантію (якщо тільки щось не так з тим, що надає користувач), і std::variantнемає причин цього порушувати. Я погоджуюся, що це можна зробити більш чітким у формулюванні стандарту, але в основному це працює як частина частини стандартної бібліотеки. І FYI, 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.