12

The class template std::variant represents a type-safe union. An instance of std::variant at any given time either holds a value of one of its alternative types, or it holds no value.

sizeof(std::variant<float, int32_t, double>) == 16

But if it is a union, why does it take so much space?

struct T1 {
    float a;
    int32_t b;
    double c;
};

struct T2 {
    union {
        float a;
        int32_t b;
        double c;
    };
};

The variant has the same size as the struct

sizeof(T1) == 16
sizeof(T2) == 8

I would expect the size of the union, plus 4 bytes to store, which type is active.

zett42
  • 25,437
  • 3
  • 35
  • 72
Iter Ator
  • 8,226
  • 20
  • 73
  • 164

4 Answers4

38

Here the type with the largest alignment in the variant is double, with an alignment of 8. This means the entire variant must have an alignment of 8. This also means its size must be a multiple of 8, ensuring that in an array each variant will have its double aligned by 8.

But a variant must store more than just the largest type: it must also store an identifier or tag to know which type is currently instantiated. So even if only 1 byte is used for that identifier, the structure as a whole is padded to 16 so that it is a multiple of 8 in size.

A more correct comparison would be:

struct
{
    union
    {
        float a;
        int32_t b;
        double c;
    };
    int identifier;
};
Cory Nelson
  • 29,236
  • 5
  • 72
  • 110
  • 1
    Worth noting is that, at least on some systems, the overhead is only one byte (at least if there are few enough options). On [this machine](https://godbolt.org/g/zCK7Qz), `sizeof(std::variant) == 2001`. – Daniel H Aug 08 '17 at 23:09
  • 6
    @DanielH The largest alignment of all your types is `1`, which allows an `identifier` that takes only a single byte to not have any padding. Make one of those an `int` and you'll see padding. You are demonstrating a stdlib implementation detail, not really a machine detail here. – Cory Nelson Aug 08 '17 at 23:38
  • 1
    Yes; my point wasn’t that there would be no padding or anything like hat, it was that with large types like that it’s clear the storage *was* reused, or you’d need 3002 bytes for everything. And I suppose you’re right that I referenced the wrong part of the implementation; I meant to just say it was a feature of that implementation. – Daniel H Aug 09 '17 at 13:39
7

The basic answer is: for alignment reasons. Since one of your types is a double, and doubles are 8 bytes with 8 byte alignment requirements, this means that the variant also has an 8 byte alignment requirement. So it can only be multiples of 8 bytes in size. As you yourself noted, the minimum size is the largest type + something additional to indicate which member is active. That means it cannot fit in 8 bytes, but then alignment requirements force it all the way up to 16 bytes.

That is why it cannot be 12 bytes, as you reason. By the way, there is also no rule that says the active type storage must be 4 bytes. In fact, it's pretty clear that in the vast majority of cases, you can get by with a single byte, which can discriminate between 255 types, plus the empty state. We can test this:

struct null {};
std::cerr << sizeof(std::variant<bool, null>);

I just tested this on coliru, and it printed out "2". In this case, 1 byte for the largest type (bool), and 1 byte to determine which type is active.

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
6

While variants logically are type-safe unions, no guarantees are made that their sizes would be equal to the sizes of raw unions with the same members.

It is clear that since variant (unlike raw union) needs to store information on currently active type, it has to be bigger. The actual size of the variant would depend on the architecture (padding) and implementation, since Standard imposes no restrictions on the size.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • Sorry to pick on you again today, but while this answer is "technically correct, the best kind of correct", and thus seems to be favored by the language lawyers, it doesn't really answer the question. standard library implementers are very smart and would not willingly waste space for no reason. The actual reason is alignment. – Nir Friedman Aug 08 '17 at 18:58
  • @NirFriedman, and what did I say? The size if bigger because of padding (for alignment purposes) and the need to hold an extra member. – SergeyA Aug 08 '17 at 19:00
  • Well, you don't mention the word alignment (which is the single most important word), and in addition, I don't think it makes clear that the size for the variant the OP mentions is actually the absolute smallest possible. – Nir Friedman Aug 08 '17 at 19:04
  • @NirFriedman, padding and alignment are to each other as the burger is to ketchup. If you search for padding: https://www.google.com/search?q=c%2B%2B+padding&oq=c%2B%2B+padd&aqs=chrome.1.69i57j0l5.4000j0j7&sourceid=chrome&ie=UTF-8 the topmost result is `data structure alignment` – SergeyA Aug 08 '17 at 19:10
3

Funnily enough, you were tricked by coincidence. The return of the following is 16:

sizeof(std::variant<float, int32_t, double, int64_t>)

And this too:

sizeof(std::variant<float, int32_t, double, int64_t, double>)

So basically there's an internal variable in std::variant that has the size of 8 bytes (or less, but aligned to 8 bytes). That, in addition to your union, makes 16.

The Quantum Physicist
  • 24,987
  • 19
  • 103
  • 189