GCC9 是否允许避免 std::variant 的无价值状态?

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

我最近关注了 Reddit 的讨论,该讨论对 std::visit 跨编译器的优化进行了很好的比较。我注意到以下内容:https://godbolt.org/z/D2Q5ED

GCC9 和 Clang9(我猜他们共享相同的标准库)都不会生成代码来检查并在所有类型都满足某些条件时抛出无价值的异常。这导致了更好的代码生成方式,因此我提出了一个关于 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&) {}
}

声称,这会使任何变体变得毫无价值,阅读 docu 它应该:

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.

我不明白的是:为什么写成"may"?如果整个操作抛出,留在旧状态是否合法?因为这就是 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);

因此基本上它会创建一个临时文件,如果成功 copies/moves 它会进入真实位置。

国际海事组织,如文档所述,这违反了 "First, destroys the currently contained value"。当我阅读标准时,在 v.emplace(...) 之后,变体中的当前值总是被销毁,新类型要么是集合类型,要么是无值的。

我确实知道条件 is_trivially_copyable 排除了所有具有可观察析构函数的类型。所以这也可以是:"as-if variant is reinitialized with the old value" 左右。但是变体的状态是一种可观察到的效果。那么标准确实允许 emplace 不会改变当前值吗?

根据标准引用进行编辑:

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); 真的算作上述的有效实施吗?这是"as if"的意思吗?

So does the standard indeed allow, that emplace does not change the current value?

是的。 emplace 应提供不泄漏的基本保证(即在构造和销毁产生可观察到的副作用时尊重对象的生命周期),但在可能的情况下,允许提供强保证(即在以下情况下保持原始状态)操作失败)。

variant 需要与联合的行为类似——备选方案被分配在一个适当分配的存储区域中。不允许分配动态内存。因此,类型更改 emplace 无法在不调用额外的移动构造函数的情况下保留原始对象——它必须销毁它并构造新对象来代替它。如果此构造失败,则变体必须进入异常无值状态。这可以防止奇怪的事情发生,比如破坏一个不存在的对象。

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

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); 真的算作上述的有效实施吗? 这是"as if"的意思吗?

是的,如果移动赋值没有产生可观察到的效果,对于平凡可复制的类型就是这种情况。

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

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

23.7.3.4 Modifiers

(...)

template variant_alternative_t>& emplace(Args&&... args);

(...) If an exception is thrown during the initialization of the contained value, the variant might not hold a value

它说 "might" 而不是 "must"。我希望这是有意为之,以便允许像 gcc 使用的那样的实现。

正如您自己提到的,这只有在所有替代项的析构函数都是微不足道的且因此不可观察的情况下才有可能,因为需要销毁先前的值。

跟进问题:

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

Does T tmp {std​::​forward(args)...}; this->value = std::move(tmp); really count as a valid implementation of the above? Is this what is meant by "as if"?

是的,因为对于 trivially copyable 的类型,无法检测差异,因此实现的行为就好像值已按照描述进行了初始化。如果类型不可复制,这将不起作用。