Why do std::variant implementations take more than 1 byte for variant of empty types?
Asked Answered
C

1

10

This is mostly trivia question, as I doubt that I will ever need this space saving.

While playing around on godbolt I noticed that both libstdc++ and libc++ implementations of std::variant require more than 1 byte to store variant of empty structs.

libstc++ uses 2 bytes

libc++ uses 8 bytes

I presume it is just not worth the trouble to optimize this, but I wonder if there is any other reason. In particular is there something in standard wording for std::variant that prevents this optimization.

Coreycorf answered 3/9, 2021 at 19:24 Comment(7)
it wont make a difference but you have -O3 followed by -O2Nashville
Variant stores an index, that's at least 1 byte. Variant stores a value, that's also at least 1 byte because you're allowed to take an address of it. How could it be less than 2 bytes?Vandalism
@Vandalism one byte for index, 0 bits for storage.Coreycorf
@Vandalism Empty objects can share a storage location with the index.Lorinalorinda
Please provide a minimal reproducible example inline in your question, not via an external link! Also, what binary layout of that type would you expect?Isotherm
imho of someone who doesn't write standard libraries, size 2 is a reasonable expectation for optimization. size 1 could be exceedingly difficult, especially since no_unique_address current implementation is hit and miss.Alphabetical
@Alphabetical I think they can use conditional inheritance. Again I am also not writing std libs :)Coreycorf
T
10

Every object takes up at least 1 byte of space. The counter itself needs to take up at least 1 byte, but you also need space for the potential choices of object. Even if you use a union, it still needs to be one byte. And it can't be the same byte as the counter.

Now, you might think that no_unique_address could just come to the rescue, permitting the member union to overlap with the counter if all of the union elements are empty. But consider this code:

empty_type e{};
variant<empty_type> ve{in_place_index<0>}; //variant now stores the empty type.
auto *pve = ve.get_if<0>(); //Pointer to an `empty_type`.
memcpy(pve, &e, sizeof(empty_type)); //Copying from one trivial object to another.

The standard does not say that the members of a variant are "potentially-overlapping subojects" of variant or any of its internal members. Therefore, it is 100% OK for a user to do a memcpy from one trivial empty object to the other.

Which will overwrite the counter if it were overlapping with it. Therefore, it cannot be overlapping with it.

Tecumseh answered 3/9, 2021 at 20:18 Comment(5)
I did not know that std::variant is required to be trivially_copyable if all of its types are. Very good point.Alphabetical
So what you are saying is that I should write a proposal for C++23 that disables get(_if) when all members of variant are empty. ;)Coreycorf
@NoSenseEtAl: It would be better to write a proposal such that members of a tuple and/or variant may be potentially overlapping subobjects of their respective containing object.Tecumseh
@NicolBolas but wouldn't that break code you wrote?, like runtime UB?Coreycorf
@bolov: While that is true, that wasn't the point. I'm doing a trivial copy to the object inside the variant, not the variant itself.Tecumseh

© 2022 - 2024 — McMap. All rights reserved.