C++ class size
Table of contents
Introduction
Data alignment is a key feature in computing on modern computer hardware. The CPU reads and writes to memory most efficiently when the data is naturally aligned, which generally means that the data’s memory address is a multiple of the data size. For instance, in a 32-bit architecture, the data may be aligned if the data is stored in four consecutive bytes and the first byte lies on a 4-byte boundary.
In addition to the performance, data alignment is also the assumption of many programming languages. Even though the programming languages try to take care of data alignment for us as much as possible, some low-level programming languages can have misaligned data access while the behavior is undefined.
Data alignment
A memory address
Suppose we have a piece of
byte data.
Accessing
A memory access is said to be aligned when the data being
accessed is
However, because C and C++ standards assumed aligned memory access, accessing a misaligned address might result in undefined behaviors.
Helpers
The helper macro CHECK_SIZE_AND_ALIGNMENT
is used to display the
size of a type or object and check its size and alignment.
#define CHECK_SIZE_AND_ALIGNMENT(type, size, alignment) \
cout << setw(70) << "sizeof(" #type ") = " << sizeof(type) << " | " \
<< alignment << endl; \
static_assert(sizeof(type) == size, \
"The size of " #type " should be equal to " #size); \
static_assert(alignof(type) == alignment, "The align requirement of " #type \
" should be equal to " #alignment)
The standard new operator is overrided to print the amount of memory allocated on each call.
bool is_new_print_enabled = false;
void* operator new(size_t size) {
if (is_new_print_enabled) {
cout << "Allocating " << size << " bytes\n";
}
return malloc(size);
}
The standard delete operator is overrided to show when it is called.
// void operator delete(void* memory) {
// if (is_new_print_enabled) {
// cout << "Deallocating memory from " << memory << "\n";
// }
// free(memory);
// }
Empty class/struct — size: 1, align: 1
Empty class/struct
must require ONE byte of memory to make
then distinguishable.
struct EmptyStruct {};
CHECK_SIZE_AND_ALIGNMENT(EmptyStruct, 1, 1);
static_assert(sizeof(EmptyStruct) == 1,
"The size of an empty struct should be equal to 1");
class EmptyClass {};
CHECK_SIZE_AND_ALIGNMENT(EmptyClass, 1, 1);
static_assert(sizeof(EmptyStruct) == 1,
"The size of an empty class should be equal to 1");
EmptyClass ea, eb;
static_assert(&ea != &eb,
"The address of two empty class should be distinguishable");
class EmptyClassWithDefaultConstructor {
public:
EmptyClassWithDefaultConstructor() = default;
};
CHECK_SIZE_AND_ALIGNMENT(EmptyClassWithDefaultConstructor, 1, 1);
class EmptyClassWithDefaultConstructorAndDestructor {
public:
EmptyClassWithDefaultConstructorAndDestructor() = default;
~EmptyClassWithDefaultConstructorAndDestructor() = default;
};
CHECK_SIZE_AND_ALIGNMENT(EmptyClassWithDefaultConstructorAndDestructor, 1, 1);
class EmptyClassWithDefaultConstructorAndVirtualDestructor {
public:
EmptyClassWithDefaultConstructorAndVirtualDestructor() = default;
virtual ~EmptyClassWithDefaultConstructorAndVirtualDestructor() = default;
};
CHECK_SIZE_AND_ALIGNMENT(EmptyClassWithDefaultConstructorAndVirtualDestructor,
8, 8);
If there are at least one virtual function the 8 bytes of the virtual table will be prepended into the class.
Union — many attributes on same memory location
typedef union {
char a;
int b;
} a_union_t;
CHECK_SIZE_AND_ALIGNMENT(a_union_t, 4, 4);
cout << "Offset of a_union_t::a " << offsetof(a_union_t, a) << endl; // 0
cout << "Offset of a_union_t::b " << offsetof(a_union_t, b) << endl; // 0
Default member size alignment
typedef struct {
char a;
int b;
} b_struct_t;
CHECK_SIZE_AND_ALIGNMENT(b_struct_t, 8, 4);
cout << "Offset of b_struct_t::a " << offsetof(b_struct_t, a) << endl; // 0
cout << "Offset of b_struct_t::b " << offsetof(b_struct_t, b) << endl; // 4
class c_class {
public:
char a;
int b;
int f1() { return 1; }
int f2() { return 2; }
int f3() { return 3; }
int f4() { return 4; }
};
CHECK_SIZE_AND_ALIGNMENT(c_class, 8, 4);
cout << "Offset of c_class::a " << offsetof(c_class, a) << endl; // 0
cout << "Offset of c_class::b " << offsetof(c_class, b) << endl; // 4
Functions does not changes the class size.
class d_class {
public:
char a;
int b;
virtual ~d_class(){};
};
CHECK_SIZE_AND_ALIGNMENT(d_class, 16, 8);
// warning: offset of on non-standard-layout type 'd_class'
// [-Winvalid-offsetof]
// cout << "Offset of d_class::a " << offsetof(d_class, a) << endl; // ?
// cout << "Offset of d_class::b " << offsetof(d_class, b) << endl; // ?
//
// *** Dumping AST Record Layout
// 0 | class d_class
// 0 | (d_class vtable pointer)
// 8 | char a
// 12 | int b
// | [sizeof=16, dsize=16, align=8,
// | nvsize=16, nvalign=8]
The virtual
keyword include a virtual table attribute.
class e_class {
public:
char a;
int b;
~e_class(){};
};
CHECK_SIZE_AND_ALIGNMENT(e_class, 8, 4);
cout << "Offset of e_class::a " << offsetof(e_class, a) << endl; // ?
cout << "Offset of e_class::b " << offsetof(e_class, b) << endl; // ?
//
// *** Dumping AST Record Layout
// 0 | class e_class
// 0 | char a
// 4 | int b
// | [sizeof=8, dsize=8, align=4,
// | nvsize=8, nvalign=4]
class f_class {
public:
char a; // 4
int b; // 4
float c; // 4
~f_class(){};
};
CHECK_SIZE_AND_ALIGNMENT(f_class, 12, 4);
is_new_print_enabled = true;
{
auto f_class_obj_1 = new f_class; // Allocating 12 bytes
auto f_class_obj_2 = make_unique<f_class>(); // Allocating 12 bytes
auto f_class_obj_3 = make_shared<f_class>(); // Allocating 32 bytes
cout << "Offset of f_class::a " << offsetof(f_class, a) << endl; // 0
cout << "Offset of f_class::b " << offsetof(f_class, b) << endl; // 4
cout << "Offset of f_class::c " << offsetof(f_class, c) << endl; // 8
delete f_class_obj_1;
}
is_new_print_enabled = false;
class g_class {
public:
char a;
int b;
float c;
double d;
char e;
char f;
~g_class(){};
};
CHECK_SIZE_AND_ALIGNMENT(g_class, 32, 8);
cout << "Offset of g_class::a " << offsetof(g_class, a) << endl; // 0
cout << "Offset of g_class::b " << offsetof(g_class, b) << endl; // 4
cout << "Offset of g_class::c " << offsetof(g_class, c) << endl; // 8
cout << "Offset of g_class::d " << offsetof(g_class, d) << endl; // 16
cout << "Offset of g_class::e " << offsetof(g_class, e) << endl; // 24
cout << "Offset of g_class::f " << offsetof(g_class, f) << endl; // 25
Is possible to save memory by reorder the attributes on class.
class g2_class {
public:
char a;
char e;
char f;
int b;
float c;
double d;
~g2_class(){};
};
CHECK_SIZE_AND_ALIGNMENT(g2_class, 24, 8);
cout << "Offset of g2_class::a " << offsetof(g2_class, a) << endl; // 0
cout << "Offset of g2_class::b " << offsetof(g2_class, b) << endl; // 4
cout << "Offset of g2_class::c " << offsetof(g2_class, c) << endl; // 8
cout << "Offset of g2_class::d " << offsetof(g2_class, d) << endl; // 16
cout << "Offset of g2_class::e " << offsetof(g2_class, e) << endl; // 1
cout << "Offset of g2_class::f " << offsetof(g2_class, f) << endl; // 2
Force alignment
struct float4_4_t {
float data[4];
};
CHECK_SIZE_AND_ALIGNMENT(float4_4_t, 16, 4);
float4_4_t f4;
for (int i = 0; i < 4; ++i) {
cout << "Offset float4_4_t[" << i
<< "] = " << long(&f4.data[i]) - long(&f4) << endl;
}
// Offset float4_4_t[0] = 0
// Offset float4_4_t[1] = 4
// Offset float4_4_t[2] = 8
// Offset float4_4_t[3] = 12
// Every object of type float4_32_t will be aligned to 32-byte boundary.
// Might be useful for SIMD instructions.
struct alignas(32) float4_32_t {
float data[4];
};
CHECK_SIZE_AND_ALIGNMENT(float4_32_t, 32, 32);
float4_32_t f32;
for (int i = 0; i < 4; ++i) {
cout << "Offset float4_32_t[" << i
<< "] = " << long(&f32.data[i]) - long(&f32) << endl;
}
// Offset float4_32_t[0] = 0
// Offset float4_32_t[1] = 4
// Offset float4_32_t[2] = 8
// Offset float4_32_t[3] = 12
Memory Allocation
According to the [GNU
the address of a block returned by malloc or realloc in GNU systems is always a multiple of eight (or sixteen on 64-bit systems). The default memory address alignment of array is determined by the alignment requirement of the element.
It is possible to use custom data alignment for allocated static
memory and dynamic memory. alignas(T)
can be used to specify the
byte alignment of an static array and aligned_alloc can be used
to specify the byte alignment of a buffer on dynamic memory.
struct S1 {
unsigned char buf1[sizeof(int) / sizeof(char)];
};
CHECK_SIZE_AND_ALIGNMENT(S1, 4, 1);
struct S2 {
alignas(int) unsigned char buf2[sizeof(int) / sizeof(char)];
};
CHECK_SIZE_AND_ALIGNMENT(S2, 4, 4);
Virtual table
For every class that contains virtual functions, the compiler constructs a virtual table, a.k.a vtable. The vtable contains an entry for each virtual function accessible by the class and stores a pointer to its definition. Only the most specific function definition callable by the class is stored in the vtable. Entries in the vtable can point to either functions declared in the class itself, or virtual functions inherited from a base class.
class B {
public:
B() { puts("This is B's constructor"); }
virtual ~B() { puts("This is B's destructor"); }
virtual void bar() { puts("This is B's implementation of bar"); }
virtual void qux() { puts("This is B's implementation of qux"); }
};
class C : public B {
public:
C() { puts("This is C's constructor"); }
virtual ~C() { puts("This is C's destructor"); }
void bar() override { puts("This is C's implementation of bar"); }
};
B* b = new C();
b->bar();
delete b;
// This is B's constructor
// This is C's constructor
// This is C's implementation of bar
// This is C's destructor
// This is B's destructor
Note that the vpointer is just another class member added by the compiler and increases the size of every object that has a vtable by sizeof(vpointer).
Hopefully you have grasped how dynamic function dispatch can be implemented by using vtables: when a call to a virtual function on an object is performed, the vpointer of the object is used to find the corresponding vtable of the class. Next, the function name is used as index to the vtable to find the correct (most specific) routine to be executed.
Virtual destructor
By now it should also be clear why it is always a good idea to make destructors of base classes virtual. Since derived classes are often handled via base class references, declaring a non-virtual destructor will be dispatched statically, obfuscating the destructor of the derived class.
Possible output
sizeof(EmptyStruct) = 1 | 1
sizeof(EmptyClass) = 1 | 1
sizeof(EmptyClassWithDefaultConstructor) = 1 | 1
sizeof(EmptyClassWithDefaultConstructorAndDestructor) = 1 | 1
sizeof(EmptyClassWithDefaultConstructorAndVirtualDestructor) = 8 | 8
sizeof(a_union_t) = 4 | 4
Offset of a_union_t::a 0
Offset of a_union_t::b 0
sizeof(b_struct_t) = 8 | 4
Offset of b_struct_t::a 0
Offset of b_struct_t::b 4
sizeof(c_class) = 8 | 4
Offset of c_class::a 0
Offset of c_class::b 4
sizeof(d_class) = 16 | 8
sizeof(e_class) = 8 | 4
Offset of e_class::a 0
Offset of e_class::b 4
sizeof(f_class) = 12 | 4
Allocating 12 bytes
Allocating 12 bytes
Allocating 32 bytes
Offset of f_class::a 0
Offset of f_class::b 4
Offset of f_class::c 8
sizeof(g_class) = 32 | 8
Offset of g_class::a 0
Offset of g_class::b 4
Offset of g_class::c 8
Offset of g_class::d 16
Offset of g_class::e 24
Offset of g_class::f 25
sizeof(g2_class) = 24 | 8
Offset of g2_class::a 0
Offset of g2_class::b 4
Offset of g2_class::c 8
Offset of g2_class::d 16
Offset of g2_class::e 1
Offset of g2_class::f 2
sizeof(float4_4_t) = 16 | 4
Offset float4_4_t[0] = 0
Offset float4_4_t[1] = 4
Offset float4_4_t[2] = 8
Offset float4_4_t[3] = 12
sizeof(float4_32_t) = 32 | 32
Offset float4_32_t[0] = 0
Offset float4_32_t[1] = 4
Offset float4_32_t[2] = 8
Offset float4_32_t[3] = 12
sizeof(S1) = 4 | 1
sizeof(S2) = 4 | 4
This is B's constructor
This is C's constructor
This is C's implementation of bar
This is C's destructor
This is B's destructor