Is GCC9 avoiding valueless state of std::variant allowed?

Fla*_*ire 13 c++ variant c++-standard-library c++17

I recently followed a Reddit discussion which lead to a nice comparison of std::visit optimization across compilers. I noticed the following: https://godbolt.org/z/D2Q5ED

Both GCC9 and Clang9 (I guess they share the same stdlib) do not generate code for checking and throwing a valueless exception when all types meet some conditions. This leads to way better codegen, hence I raised an issue with the MSVC STL and was presented with this code:

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&) {}
}
Run Code Online (Sandbox Code Playgroud)

The claim was, that this makes any variant valueless, and reading the docu it should:

First, destroys the currently contained value (if any). Then direct-initializes the contained value as if constructing a value of type T_I with the arguments std::forward<Args>(args).... If an exception is thrown, *this may become valueless_by_exception.

What I don't understand: Why is it stated as "may"? Is it legal to stay in the old state if the whole operation throws? Because this is what GCC does:

  // 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>>
    { };
Run Code Online (Sandbox Code Playgroud)

And later it (conditionally) does something like:

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

Hence basically it creates a temporary, and if that succeeds copies/moves it into the real place.

IMO this is a violation of "First, destroys the currently contained value" as stated by the docu. As I read the standard, then after a v.emplace(...) the current value in the variant is always destroyed and the new type is either the set type or valueless.

I do get that the condition is_trivially_copyable excludes all types that have an observable destructor. So this can also be though as: "as-if variant is reinitialized with the old value" or so. But the state of the variant is an observable effect. So does the standard indeed allow, that emplace does not change the current value?

Edit in response to a standard quote:

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>(args)...}; this->value = std::move(tmp);真的算作以上的有效实施?这是“好像”的意思吗?

Pau*_*ulR 7

我认为标准的重要部分是:

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

23.7.3.4修改器

(...)

模板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)....
Run Code Online (Sandbox Code Playgroud)

T tmp {std?::?forward(args)...}吗?this-> value = std :: move(tmp); 真的算作上述的有效实施?这是“好像”的意思吗?

是的,因为对于普通可复制的类型,无法检测到差异,所以实现的行为就像该值已按所述初始化一样。如果该类型不可复制,则将无法使用。


L. *_* F. 5

那么,标准确实允许emplace不改变当前值吗?

是。 emplace应提供不泄漏的基本保证(即在构造和破坏产生可观察到的副作用时尊重对象的寿命),但在可能的情况下,可以提供有力的保证(即在操作失败时保留原始状态)。

variant要求其行为与联合相似—替代项分配在适当分配存储的一个区域中。不允许分配动态内存。因此,类型更改emplace无法在不调用其他move构造函数的情况下保留原始对象-它必须销毁它并构造新对象来代替它。如果这种构造失败,则变体必须进入异常的无价值状态。这样可以防止发生诸如破坏不存在的对象之类的怪异事件。

但是,对于小的平凡可复制类型,可以在没有太多开销的情况下提供有力的保证(在这种情况下,甚至可以提高性能来避免检查)。因此,实现可以做到这一点。这是符合标准的:实现仍然以标准的用户友好方式提供了标准所要求的基本保证。

根据标准报价进行编辑:

然后初始化包含的值,就好像直接使用参数初始化TI类型的值一样 std?::?forward<Args>(args)...

难道T tmp {std?::?forward<Args>(args)...}; this->value = std::move(tmp);真的算作以上的有效实施?这是“好像”的意思吗?

是的,如果移动分配没有产生可观察到的效果,那么对于琐碎可复制的类型就是这种情况。