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排除了所有具有可观察析构函数的类型。因此,这也可能是这样的:“如果变量以旧值重新初始化”。但是变体的状态是可观察到的效果。那么,标准确实允许emplace不改变当前值吗?

根据标准报价进行编辑:

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

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

Answers:


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

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

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


有趣。我通过后续/澄清请求更新了问题。根本原因是:允许复制/移动吗?我对might/may措辞感到非常困惑,因为该标准并未说明替代方案。
Flamefire '19

接受此作为标准报价和there is no way to detect the difference
Flamefire '19

5

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

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

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

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

根据标准报价进行编辑:

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

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

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


我完全同意逻辑推理。我只是不确定这实际上在标准中吗?你能用什么备份吗?
Flamefire '19

@Flamefire Hmm ...通常,标准功能提供了基本保证(除非用户提供的内容有问题),并且std::variant没有理由破坏它。我同意可以在标准的措辞中使这一点更加明确,但这基本上是标准库的其他部分的工作方式。仅供参考,P0088是最初的建议。
LF

谢谢。内部有一个更明确的规范:if an exception is thrown during the call toT’s constructor, valid()will be false;确实禁止了这种“优化”
Flamefire '19

是。的规范emplace在P0088下Exception safety
Flamefire

@Flamefire似乎与原始提案和投票的版本之间存在差异。最终版本更改为“可以”的措词。
LF
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.