In my first example I have two bitfields using int64_t
. When I compile and get the size of the class I get 8.
class Test
{
int64_t first : 40;
To add to what others have already said:
If you want to examine it, you can use a compiler option or external program to output the struct layout.
Consider this file:
// test.cpp
#include <cstdint>
class Test_1 {
int64_t first : 40;
int64_t second : 24;
};
class Test_2 {
int64_t first : 40;
int32_t second : 24;
};
// Dummy instances to force Clang to output layout.
Test_1 t1;
Test_2 t2;
If we use a layout output flag, such as Visual Studio's /d1reportSingleClassLayoutX
(where X
is all or part of the class or struct name) or Clang++'s -Xclang -fdump-record-layouts
(where -Xclang
tells the compiler to interpret -fdump-record-layouts
as a Clang frontend command instead of a GCC frontend command), we can dump the memory layouts of Test_1
and Test_2
to standard output. [Unfortunately, I'm not sure how to do this directly with GCC.]
If we do so, the compiler will output the following layouts:
cl /c /d1reportSingleClassLayoutTest test.cpp
// Output:
tst.cpp
class Test_1 size(8):
+---
0. | first (bitstart=0,nbits=40)
0. | second (bitstart=40,nbits=24)
+---
class Test_2 size(16):
+---
0. | first (bitstart=0,nbits=40)
8. | second (bitstart=0,nbits=24)
| <alignment member> (size=4)
+---
clang++ -c -std=c++11 -Xclang -fdump-record-layouts test.cpp
// Output:
*** Dumping AST Record Layout
0 | class Test_1
0 | int64_t first
5 | int64_t second
| [sizeof=8, dsize=8, align=8
| nvsize=8, nvalign=8]
*** Dumping IRgen Record Layout
Record: CXXRecordDecl 0x344dfa8 <source_file.cpp:3:1, line:6:1> line:3:7 referenced class Test_1 definition
|-CXXRecordDecl 0x344e0c0 <col:1, col:7> col:7 implicit class Test_1
|-FieldDecl 0x344e1a0 <line:4:2, col:19> col:10 first 'int64_t':'long'
| `-IntegerLiteral 0x344e170 <col:19> 'int' 40
|-FieldDecl 0x344e218 <line:5:2, col:19> col:10 second 'int64_t':'long'
| `-IntegerLiteral 0x344e1e8 <col:19> 'int' 24
|-CXXConstructorDecl 0x3490d88 <line:3:7> col:7 implicit used Test_1 'void (void) noexcept' inline
| `-CompoundStmt 0x34912b0 <col:7>
|-CXXConstructorDecl 0x3490ee8 <col:7> col:7 implicit constexpr Test_1 'void (const class Test_1 &)' inline noexcept-unevaluated 0x3490ee8
| `-ParmVarDecl 0x3491030 <col:7> col:7 'const class Test_1 &'
`-CXXConstructorDecl 0x34910c8 <col:7> col:7 implicit constexpr Test_1 'void (class Test_1 &&)' inline noexcept-unevaluated 0x34910c8
`-ParmVarDecl 0x3491210 <col:7> col:7 'class Test_1 &&'
Layout: <CGRecordLayout
LLVMType:%class.Test_1 = type { i64 }
NonVirtualBaseLLVMType:%class.Test_1 = type { i64 }
IsZeroInitializable:1
BitFields:[
<CGBitFieldInfo Offset:0 Size:40 IsSigned:1 StorageSize:64 StorageOffset:0>
<CGBitFieldInfo Offset:40 Size:24 IsSigned:1 StorageSize:64 StorageOffset:0>
]>
*** Dumping AST Record Layout
0 | class Test_2
0 | int64_t first
5 | int32_t second
| [sizeof=8, dsize=8, align=8
| nvsize=8, nvalign=8]
*** Dumping IRgen Record Layout
Record: CXXRecordDecl 0x344e260 <source_file.cpp:8:1, line:11:1> line:8:7 referenced class Test_2 definition
|-CXXRecordDecl 0x344e370 <col:1, col:7> col:7 implicit class Test_2
|-FieldDecl 0x3490bd0 <line:9:2, col:19> col:10 first 'int64_t':'long'
| `-IntegerLiteral 0x344e400 <col:19> 'int' 40
|-FieldDecl 0x3490c70 <line:10:2, col:19> col:10 second 'int32_t':'int'
| `-IntegerLiteral 0x3490c40 <col:19> 'int' 24
|-CXXConstructorDecl 0x3491438 <line:8:7> col:7 implicit used Test_2 'void (void) noexcept' inline
| `-CompoundStmt 0x34918f8 <col:7>
|-CXXConstructorDecl 0x3491568 <col:7> col:7 implicit constexpr Test_2 'void (const class Test_2 &)' inline noexcept-unevaluated 0x3491568
| `-ParmVarDecl 0x34916b0 <col:7> col:7 'const class Test_2 &'
`-CXXConstructorDecl 0x3491748 <col:7> col:7 implicit constexpr Test_2 'void (class Test_2 &&)' inline noexcept-unevaluated 0x3491748
`-ParmVarDecl 0x3491890 <col:7> col:7 'class Test_2 &&'
Layout: <CGRecordLayout
LLVMType:%class.Test_2 = type { i64 }
NonVirtualBaseLLVMType:%class.Test_2 = type { i64 }
IsZeroInitializable:1
BitFields:[
<CGBitFieldInfo Offset:0 Size:40 IsSigned:1 StorageSize:64 StorageOffset:0>
<CGBitFieldInfo Offset:40 Size:24 IsSigned:1 StorageSize:64 StorageOffset:0>
]>
Note that the version of Clang I used to generate this output (the one used by Rextester) appears to default to optimising both bitfields into a single variable, and I'm unsure how to disable this behaviour.
In your first example
int64_t first : 40;
int64_t second : 24;
Both first
and second
use the 64 bits of a single 64 bit integer. This causes the size of the class to be a single 64 bit integer. In the second example you have
int64_t first : 40;
int32_t second : 24;
Which is two separate bit fields being stored in two different chunks of memory. You use 40 bits of the 64 bit integer and then you use 24 bit of another 32 bit integer. This means you need at least 12 bytes(this example is using 8 bit bytes). Most likely the extra 4 bytes you see is padding to align the class on 64 bit boundaries.
As other answers and comments have pointed out this is implementation defined behavior and you can/will see different results on different implementations.
The C Standard's rules for bitfields aren't precise enough to tell programmers anything useful about the layout, but nonetheless deny implementations what might otherwise be useful freedoms.
In particular, bitfields are required to be stored within objects that are of the indicated types, or the signed/unsigned equivalent thereof. In your first example, the first bitfield must be stored in an int64_t or uint64_t object, and the second one likewise, but there's enough room for them to fit into the same object. In the second example, the first bitfield must be stored in an int64_t or uint64_t, and the second one in an int32_t or uint32_t. The uint64_t will have 24 bits that would be "stranded" even if additional bit fields were added to the end of the struct; the uint32_t has 8 bits which aren't presently used, but would be available for use of another int32_t or uint32_t bitfield whose width was less than 8 were added to the type.
IMHO, the Standard strikes just about the worst possible balance here between giving compilers freedom vs giving programmers useful information/control, but it is what it is. Personally I think bitfields would be much more useful if the preferred syntax let programmers specify their layout precisely in terms of ordinary objects (e.g. bitfield "foo" should be stored in 3 bits, starting at bit 4 (value of 16), of field "foo_bar") but I know of no plans to define such a thing in the Standard.
Standard says:
§ 9.6 bit-fields
Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. [Note: Bit-fields straddle allocation units on some machines and not on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others. — end note]
c++11 paper
So layout depends on compiler implementation, compilation flags, target arch and so on. Just checked several compilers and output mostly is 8 8
:
#include <stdint.h>
#include <iostream>
class Test32
{
int64_t first : 40;
int32_t second : 24;
};
class Test64
{
int64_t first : 40;
int64_t second : 24;
};
int main()
{
std::cout << sizeof(Test32) << " " << sizeof(Test64);
}