Is GCC9 avoiding valueless state of std::variant allowed?
Asked Answered
K

2

15

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&) {}
}

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>>
    { };

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

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

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

Kumar answered 13/11, 2019 at 9:54 Comment(0)
K
7

I think the important part of the standard is this:

From 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

It says "might" not "must". I would expect this to be intentional in order to allow implementations like the one used by gcc.

As you mentioned yourself, this is only possible if the destructors of all alternatives are trivial and thus unobservable because destroying the previous value is required.

Followup question:

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"?

Yes, because for types that are trivially copyable there is no way to detect the difference, so the implementation behaves as if the value was initialized as described. This would not work if the type was not trivially copyable.

Kweisui answered 13/11, 2019 at 10:45 Comment(2)
Interesting. I updated the question with a follow-up/clarification request. The root is: Is the copy/move allowed? I'm very confused by the might/may wording as the standard does not state what the alternative is.Kumar
Accepting this for the standard quote and there is no way to detect the difference.Kumar
E
5

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

Yes. emplace shall provide the basic guarantee of no leaking (i.e., respecting object lifetime when construction and destruction produce observable side effects), but when possible, it is allowed to provide the strong guarantee (i.e., the original state is kept when an operation fails).

variant is required to behave similarly to a union — the alternatives are allocated in one region of suitably allocated storage. It is not allowed to allocate dynamic memory. Therefore, a type-changing emplace has no way to keep the original object without calling an additional move constructor — it has to destroy it and construct the new object in place of it. If this construction fails, then the variant has to go to the exceptional valueless state. This prevents weird things like destroying a nonexistent object.

However, for small trivially copyable types, it is possible to provide the strong guarantee without too much overhead (even a performance boost for avoiding a check, in this case). Therefore, the implementation does it. This is standard-conforming: the implementation still provides the basic guarantee as required by the standard, just in a more user-friendly way.

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

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

Yes, if the move assignment produces no observable effect, which is the case for trivially copyable types.

Exhibitor answered 13/11, 2019 at 10:36 Comment(5)
I fully agree with the logic reasoning. I'm just unsure this is actually in the standard? Can you back this up with anything?Kumar
@Kumar Hmm ... In general, the standard functionalities provide the basic guarantee (unless there's something wrong with what the user provides), and std::variant has no reason to break that. I agree that this can be made more explicit in the wording of the standard, but this is basically how others part of the standard library work. And FYI, P0088 was the initial proposal.Exhibitor
Thanks. There is a more explicit specification inside: if an exception is thrown during the call toT’s constructor, valid()will be false; So that did prohibit this "optimization"Kumar
Yes. Specification of emplacein P0088 under Exception safetyKumar
@Kumar Seems to be a discrepancy between the original proposal and the version voted in. The final version changed to the "may" wording.Exhibitor

© 2022 - 2024 — McMap. All rights reserved.