GCC9是否在避免std :: variant的无值状态?

时间:2019-11-13 09:54:09

标签: c++ c++17 variant c++-standard-library

我最近关注了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&) {}
}

声称,这使得任何变体都没有价值,并阅读docu,它应该:

  

首先,销毁当前包含的值(如果有)。然后   直接初始化包含的值,就像构造一个值   输入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(...)之后,变量中的当前值总是被破坏,而新类型要么是set类型,要么是无值。

我确实知道条件is_trivially_copyable排除了具有可观察的析构函数的所有类型。因此,这也可能是这样的:“如果变量以旧值重新初始化”。但是变体的状态是可观察到的效果。标准确实允许emplace不更改当前值吗?

根据标准报价进行编辑:

  

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

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

2 个答案:

答案 0 :(得分: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);真的算作上述的有效实施?这是“好像”的意思吗?

是的,因为对于trivially copyable类型,无法检测出差异,所以实现的行为就好像该值已按照所述进行了初始化。如果该类型不可复制,则将无法正常工作。

答案 1 :(得分:5)

  

该标准确实允许emplace不会更改   当前值?

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

要求

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

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

  

根据标准报价进行编辑:

     
    

然后初始化包含的值,就像     直接非列表初始化带有参数的TI类型的值     std​::​forward<Args>(args)...

  
     

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

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