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

15
我最近关注了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应该做到以下几点:
首先,销毁当前包含的值(如果有)。然后,直接使用参数std::forward<Args>(args)....构造类型T_I的值。如果抛出异常,则*this可能会因异常而变得没有价值。
我不理解的是:为什么它被说明为“可能”?如果整个操作抛出异常,保持旧状态是否合法?因为这就是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);

因此,基本上它创建了一个临时变量,如果成功,则将其复制/移动到实际位置。
我认为这违反了文档中所述的“首先,销毁当前包含的值”。按照标准,然后在v.emplace(...)之后,变量中的当前值始终被销毁,新类型是设置的类型或无值的。
我确实知道条件is_trivially_copyable排除了所有具有可观察析构函数的类型。因此,这也可以理解为:“好像变量重新初始化为旧值”之类的东西。但是,变量的状态是一种可观察的效果。因此,标准确实允许emplace不更改当前值吗?
响应标准引用的编辑:

然后使用参数std​::​forward<Args>(args)...直接非列表初始化类型TI的值来初始化包含的值。

T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);真的算作上述内容的有效实现吗?这就是“好像”所指的吗?
2个回答

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

这个实现是否算是上述内容的有效实现?这是否就是所谓的“as if”?

是的,因为对于平凡可复制的类型来说,没有办法检测到区别,所以该实现的行为就好像按照描述初始化了该值。如果该类型不是平凡可复制的,则无法使用此方法。


有趣。我更新了问题,并提出了跟进/澄清请求。根本问题是:复制/移动是否允许?我对“可能”措辞感到非常困惑,因为标准没有说明替代方案是什么。 - Flamefire
接受这个标准报价,"没有办法检测出差异"。 - Flamefire

5
标准确实允许emplace不改变当前值吗?
是的,emplace应该提供无泄漏的基本保证(即在构造和销毁时尊重对象寿命产生可观察的副作用),但在可能的情况下,它可以提供强保证(即操作失败时保持原始状态)。 variant必须表现得类似于联合 - 备选项分配在适当分配的存储区域的一个区域中,不允许分配动态内存。因此,类型更改的emplace没有办法保留原始对象,除非调用额外的移动构造函数 - 它必须销毁它并在其处直接构造新对象。如果此构造失败,则变量必须进入异常的无值状态。这可以防止销毁不存在的对象等奇怪的事情。
但是对于小型的平凡可复制类型,可以在不太费力的情况下提供强保证(在这种情况下甚至可以提高性能以避免检查)。因此,实现会这样做。这符合标准:实现仍然像标准所要求的那样提供基本保证,只是以更加用户友好的方式。
针对标准引用的编辑回复:
然后像使用参数std​::​forward<Args>(args)...直接非列表初始化类型TI的值一样初始化包含的值。 T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp); 真的算是上述引用的有效实现吗?这是“好像”所指的吗?
是的,如果移动赋值没有产生可观察的效果,对于平凡可复制类型来说就是这种情况。

我完全同意逻辑推理。但我不确定这实际上是否符合标准?你能提供任何支持吗? - Flamefire
@Flamefire 嗯...一般来说,标准功能提供基本保证(除非用户提供的有问题),而std::variant没有理由打破这个保证。我同意在标准的措辞上可以更明确地表达这一点,但这基本上是标准库其他部分的工作方式。另外,FYI,P0088是最初的提案。 - L. F.
谢谢。内部有更明确的规范:如果在调用T的构造函数期间抛出异常,则valid()将为false;因此,这禁止了这种“优化”。 - Flamefire
是的。在 P0088 中,emplace 的规范在 异常安全性 下进行了说明。 - Flamefire
@Flamefire 看起来原始提案和投票版本之间存在差异。最终版本更改为“可能”措辞。 - L. F.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接