Why is sizeof(std::variant) the same size as a struct with the same members?
Asked Answered
D

4

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.

Dympha answered 8/8, 2017 at 18:47 Comment(4)
Are you on a 32 bit system or a 64 bit? Which compiler/standard library are you using?Kinsley
32bit, and Visual Studio with the /std:c++latest optionDympha
even if so, it would take 16 bytes if that is standard layout. because largest word in your struct would be 8 bytes ,in your case, 4 bytes will be padded to 8. remove double and its size will be 8.Orvah
Coincidence. Add more members.Meader
G
39

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;
};
Goldplate answered 8/8, 2017 at 18:52 Comment(3)
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, sizeof(std::variant<char, char[1000], char[2000]>) == 2001.Keelboat
@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.Goldplate
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.Keelboat
M
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.

Minatory answered 8/8, 2017 at 18:56 Comment(1)
sizeof(null) would also be 1 byte, not 0.Consensual
E
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.

Electrocardiogram answered 8/8, 2017 at 18:52 Comment(4)
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.Minatory
@NirFriedman, and what did I say? The size if bigger because of padding (for alignment purposes) and the need to hold an extra member.Electrocardiogram
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.Minatory
@NirFriedman, padding and alignment are to each other as the burger is to ketchup. If you search for padding: google.com/… the topmost result is data structure alignmentElectrocardiogram
S
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.

Sumerlin answered 8/8, 2017 at 18:54 Comment(3)
That internal variable is very unlikely to be 8 bytes. It's for alignment reasons.Minatory
@NirFriedman Sure. It's just that it's reserving that amount. I'll correct my statement.Sumerlin
why guessing if one can look in header. i've learned alot (of bad thing) by reading headers of standard library. It's implementation-defined of courseOrvah

© 2022 - 2024 — McMap. All rights reserved.