C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data
Aggregate Initialization and Structure Layout
Chapter 9.7 used brace initialization from its fourth section onward and never stated its rules, and it measured sizeof twice while warning both times that the result could not be generalised. This chapter settles both, and they belong together because they are two views of the same gap: the difference between the members you wrote down and the bytes the object actually occupies. The initialization half is short and mostly reassuring — values are matched to members positionally, anything you leave out is zeroed rather than left as garbage, and supplying too many is an error. The layout half is the one with a result worth remembering. A struct's members are stored in the order you declare them, and they are not simply laid end to end: each type is only allowed to begin at certain addresses, so the compiler inserts unused bytes to make that work out. The consequence is that sizeof a struct can exceed the sum of its members, by an amount that depends on the order you happened to write them in. Measured here, four members totalling ten bytes produced a sixteen-byte type in one order and a twelve-byte type in another, which is a quarter smaller for no change in content.
Intermediate17 min readC++Part IFundamentalsStructs
Part I · Module 9 · Chapter 9.8 · Aggregate Initialization and Structure Layout
1. Two Deferred Things, One Gap
Chapter 9.7 left two things explicitly open:
The brace initializer's rules. It wrote Field f{8, 4} and Field f{} and reported what they produced without saying why.
sizeof. It measured 8 and 12 and said twice that you should not generalise from them.
Both are about the same gap:
The members you declare and the bytes the object occupies are not the same list.
§2 to §4 are the first half; §5 onward is the second, and it is where the surprise lives.
2. What an Aggregate Is
struct Field { int offset; int width; }; // an aggregateThe brace form works because Field is an aggregate — informally, a type that is just its members, with no constructor of its own deciding how it is built.
The precise definition involves conditions that only matter once classes exist, and the one that matters now is: a type with no user-provided constructor. Chapter 12.2 is where constructors arrive, and at that point a type stops being an aggregate and the brace form starts calling the constructor instead. Everything in this chapter applies to the plain grouping types of Chapter 9.7.
Arrays are aggregates too, which is why Chapter 9.1 §2's int counts[8]{} worked — the rules below are the same rules.
3. Initialization, Member by Member
Field f1{8, 4}; // offset = 8, width = 4
Field f2{8}; // offset = 8, width = 0
Field f3{}; // offset = 0, width = 0
Field f4; // INDETERMINATE -- Chapter 3.8Measured on struct P { int a; int b; };:
P p1{1,2} : a=1 b=2
P p2{} : a=0 b=0
P p3{5} : a=5 b=0 <- b was filled inFour rules, and the third is the one worth knowing:
Values are matched to members positionally, in declaration order. {8, 4} sets the first member to 8.
Too many values is an error, not a silent truncation.
Too few is fine, and the rest are value-initialized — zeroed for the arithmetic types. p3.b is 0, measured, not garbage. That is a real guarantee and it is the difference between Field f{8} and Field f;.
No braces at all means indeterminate. Field f4; as a local leaves both members unspecified and reading them is undefined behaviour — Chapter 3.8. This is the one form to avoid.
T x{};zeroes everything.T x;zeroes nothing. Two characters.
4. Positional Is a Real Cost
Because matching is positional, two members of the same type will accept each other's values silently:
struct Field { int offset; int width; };
Field f{4, 8}; // did you mean offset 4 width 8, or the other way round?Nothing can catch that. Chapter 9.7 §9's advice stands — declare members in an order a reader would guess — and two further habits help:
Use distinct types where you can. Chapter 9.9's enumerations make one member unmistakable; a Width type distinct from an Offset type would make both. That is a real technique and a heavier one than this module needs.
Name the members at the call site, if your standard allows it. C++20 added designated initializers:
Field f{.offset = 4, .width = 8}; // C++20 -- not available hereThis curriculum teaches C++17, so the form is named rather than used. You will meet it, it is exactly the fix for this problem, and if your project targets C++20 it is what you should write.
5. Why sizeof Is Not the Sum
Now the second half. The members are stored in declaration order — that much is guaranteed — but not necessarily adjacently.
The reason is a rule about where objects may begin:
Each type has an alignment: a number its address must be a multiple of. An object may only begin at an address satisfying its type's alignment.
On this platform a std::uint32_t has alignment 4, so it may only start at an address divisible by 4. A std::uint8_t has alignment 1 and may start anywhere.
Two consequences, and together they are the whole of §6's measurement:
The compiler inserts unused bytes before a member whose natural position would violate its alignment. Those bytes are padding, they hold nothing, and you cannot see them in the declaration.
And it pads the end of the struct, so that the whole type's size is a multiple of its own alignment. That is needed because an array of the type must put every element at a correctly aligned address — Chapter 9.1 §3 measured that array elements are exactly sizeof apart, so sizeof has to be a multiple of the alignment or the second element would be misaligned.
There is a keyword for asking for stronger alignment than a type needs, alignas, which belongs to the hardware-facing work of Part II and is named here only so you recognise it.
6. Measured: Ten Bytes of Members, Sixteen Bytes of Struct
#include <cstddef>
#include <cstdint>
#include <iostream>
struct Poor { // declared in a careless order
std::uint8_t flag; // 1 byte
std::uint32_t count; // 4 bytes
std::uint8_t mode; // 1 byte
std::uint32_t total; // 4 bytes
};
struct Better { // the same four members, largest first
std::uint32_t count;
std::uint32_t total;
std::uint8_t flag;
std::uint8_t mode;
};
int main()
{
std::cout << "sum of member sizes = " << (1 + 4 + 1 + 4) << " bytes\n\n";
std::cout << "sizeof(Poor) = " << sizeof(Poor) << " alignof = " << alignof(Poor) << '\n';
std::cout << " flag at offset " << offsetof(Poor, flag)
<< " count at offset " << offsetof(Poor, count)
<< " mode at offset " << offsetof(Poor, mode)
<< " total at offset " << offsetof(Poor, total) << '\n';
std::cout << "sizeof(Better) = " << sizeof(Better) << " alignof = " << alignof(Better) << '\n';
std::cout << " count at offset " << offsetof(Better, count)
<< " total at offset " << offsetof(Better, total)
<< " flag at offset " << offsetof(Better, flag)
<< " mode at offset " << offsetof(Better, mode) << '\n';
return 0;
}Measured output, zero warnings:
sum of member sizes = 10 bytes
sizeof(Poor) = 16 alignof = 4
flag at offset 0 count at offset 4 mode at offset 8 total at offset 12
sizeof(Better) = 12 alignof = 4
count at offset 0 total at offset 4 flag at offset 8 mode at offset 9Ten bytes of members. Sixteen bytes of Poor. Twelve bytes of Better. Same four members, same four types, different declaration order.
Trace Poor using §5's rule:
| member | size | offset | why |
|---|---|---|---|
flag | 1 | 0 | first member, alignment 1 |
| padding | 3 | 1–3 | count needs a multiple of 4; offset 1 is not |
count | 4 | 4 | satisfied |
mode | 1 | 8 | alignment 1 |
| padding | 3 | 9–11 | total needs a multiple of 4 |
total | 4 | 12 | satisfied |
| 16 | already a multiple of alignof 4 |
Six wasted bytes, in two gaps, both caused by a 1-byte member sitting immediately before a 4-byte one.
Now Better:
| member | size | offset | why |
|---|---|---|---|
count | 4 | 0 | aligned |
total | 4 | 4 | aligned |
flag | 1 | 8 | alignment 1 |
mode | 1 | 9 | alignment 1 |
| padding | 2 | 10–11 | round 10 up to a multiple of alignof 4 |
| 12 |
Two wasted bytes, at the end only. The rule that produced six bytes of waste in one order produces two in the other.
offsetof is the tool that made this visible — it reports a member's byte offset within its type, and it is the right way to check a layout rather than inferring one.
7. The Bytes, Pictured
The two highlighted words are the waste. Nothing in the declaration of Poor suggests they exist.
8. The Rule That Follows
Declare members in decreasing order of size.
That is not a universal optimum and it is a good default: putting the widest members first means each one lands on an address its alignment already permits, and the only padding left is at the end.
Three qualifications, because the rule is easy to over-apply.
It is implementation-defined. The alignments above are this platform's. The standard requires members to be in declaration order and requires alignment to be respected; it does not fix the numbers. Chapter 1.3 §5's distinction applies — the 16 and the 12 are measurements, and the rule is what ports.
It can conflict with readability. If grouping related members matters more than four bytes, group them. Chapter 9.7 §9 asked for a guessable order and that is usually the more valuable property.
It matters at scale, or at a boundary. Six wasted bytes in one object is nothing. Six wasted bytes in an array of a million is six megabytes, and Chapter 9.3 §5 measured what touching more memory costs. And at a hardware or protocol boundary the layout is not yours to choose at all — Chapter 27.9 is where a specified byte layout becomes the requirement, and where bit-fields, a feature this curriculum does not otherwise cover, are one of the tools.
9. Two Things Padding Breaks
Worth knowing now, because both look like they should work.
You cannot compare two structs by comparing their bytes. The padding bytes hold nothing in particular, and two objects with identical member values may differ in them. Measured — two Poor objects whose padding was filled differently and whose four members were then set identically:
all four members equal? true
memcmp says equal? falseComparing member by member is the correct way, and Module 15's operator overloading is where you write that once rather than everywhere.
sizeof is not a wire format. Writing a struct to a file or a socket by copying sizeof(S) bytes writes the padding too, and the layout depends on the compiler and platform — so a reader built elsewhere may disagree. Chapter 27.9 is where turning a specification into bytes is the subject, and the short version is that you serialise member by member rather than copying the object.
10. What to Actually Do
Always write T x{}; rather than T x;. Omitted members are zeroed; omitted braces are not.
Order members so a brace initializer reads correctly — Chapter 9.7 §9's rule, which is about readers.
Then, where it is free, order by decreasing size to avoid internal padding.
Use offsetof, sizeof and alignof to check a layout. Do not deduce it.
Expect sizeof to exceed the member sum, and do not treat it as a bug.
Never compare two structs by their bytes, and never write one to a file or socket by copying sizeof bytes.
Treat the numbers as implementation-defined. The rule ports; 16 and 12 do not.
Reach for designated initializers if your project is C++20 — they remove §4's positional hazard entirely.
11. Mistakes Worth Making Once
Writing Field f; instead of Field f{};. The members are indeterminate — Chapter 3.8.
Expecting Field f{8} to leave the second member as garbage. Measured: it is 0.
Getting two same-typed members the wrong way round. Positional matching accepts it silently.
Assuming sizeof(S) is the sum of the members. Measured: 10 bytes of members, 16 bytes of struct.
Assuming two structs with the same members have the same size. Measured: 16 and 12.
Comparing structs with memcmp. The padding bytes are not specified.
Writing a struct to a socket by copying sizeof bytes. You send padding, and the layout is implementation-defined.
Reordering members for packing when readability mattered more. Six bytes is usually worth less than a guessable order.
12. Interview Relevance
"Why is sizeof a struct sometimes bigger than its members?" Alignment: each type may only begin at an address that is a multiple of its alignment, so the compiler inserts padding, and it pads the end so an array of the type stays aligned. The measurement to have ready is 10 bytes of members giving 16.
"Can you make it smaller without removing anything?" Yes — reorder, largest first. Measured 16 against 12 for the same four members. This is the follow-up that separates knowing the rule from having used it.
"Why does the compiler not reorder for you?" Because declaration order is guaranteed, and programs rely on the layout to match hardware registers, packets and C interfaces. A guarantee is also a constraint.
"How would you check a layout?" sizeof, alignof, offsetof. Reaching for the tools rather than reasoning about it is the right answer.
13. What You Should Be Able to Say Now
- The members you declare and the bytes the object occupies are not the same list — which is what both halves of this chapter are about.
- The brace form works because these types are aggregates — informally, types with no user-provided constructor (Chapter 12.2 changes that).
- Values match members positionally, in declaration order.
- Too many is an error; too few is fine and the rest are value-initialized — measured,
P p3{5}gaveb = 0. T x{}zeroes everything;T x;zeroes nothing.- Positional matching is a real cost: two same-typed members accept each other's values silently. C++20's designated initializers fix it; this curriculum is C++17.
- Members are stored in declaration order, which is guaranteed — but not necessarily adjacently.
- Each type has an alignment: an object may only begin at an address that is a multiple of it.
- So the compiler inserts padding before a misaligned member and at the end, so that
sizeofis a multiple ofalignofand array elements stay aligned. - Measured: 10 bytes of members gave
sizeof(Poor)= 16, with 3 bytes of padding afterflagand 3 aftermode. - Measured: the same four members, largest first, gave
sizeof(Better)= 12 — two trailing bytes only. 25% smaller. - Declare members in decreasing size where it is free; readability usually matters more than a few bytes.
- The numbers are implementation-defined; the rule ports. Use
sizeof,alignof,offsetofrather than deducing. - Padding breaks byte comparison and byte serialisation — compare member by member, and serialise member by member (Chapter 27.9).
14. Exercises
Exercise 1 — predict the initialization. For struct T { int a; int b; int c; }; give all three members after: (a) T t{1,2,3}; (b) T t{1}; (c) T t{}; (d) T t; as a local (e) T t{1,2,3,4};.
Exercise 2 — trace the padding. For struct A { char a; int b; }; give each member's offset, the padding, sizeof(A) and alignof(A), then check your answer against the rule in §5.
Exercise 3 — reorder it. struct B { char a; double b; char c; int d; }; — give the offsets and sizeof as written, then reorder for minimum size and give the new sizeof.
Exercise 4 — read the figure. From Figure 1, say how many of the sixteen bytes are padding, where each gap is, and which single declaration change removes both gaps.
Exercise 5 — why the trailing padding. Better's members end at offset 10 and sizeof is 12. Explain why the last two bytes exist, referring to what Chapter 9.1 §3 measured about array elements.
Exercise 6 — what breaks. For each, say whether it is safe and why: (a) memcmp(&x, &y, sizeof x) to compare two structs (b) comparing member by member (c) write(fd, &x, sizeof x) to send a struct (d) sizeof(S) to size an array of S.
Exercise 7 — whose layout is it? A struct must match a device's register map exactly. Say why the compiler's refusal to reorder members is essential here, what tool you would use to verify the layout, and what you would do if the required layout has no padding at all.
15. Summary
- The members you wrote and the bytes you get are not the same list.
- These types are aggregates — no user-provided constructor; Chapter 12.2 changes that.
- Positional matching, declaration order. Too many is an error; too few is zeroed — measured,
b = 0. T x{}zeroes;T x;does not.- Positional matching accepts swapped same-typed members silently; C++20's designated initializers are the fix.
- Members are in declaration order and not necessarily adjacent, because each type has an alignment.
- Padding is inserted before a misaligned member and at the end, so array elements stay aligned.
- Measured: 10 bytes of members → 16 as declared, 12 reordered largest-first.
- Declare by decreasing size where it is free; the numbers are implementation-defined and the rule ports.
- Padding breaks
memcmpand byte-wise serialisation — compare and serialise member by member.
The register this module has been building needs one more member and it has been left out twice. Chapter 9.7 §3 said an access mode is naturally a small set of named values and that representing it as an int throws that away; Chapter 9.8 §4 said distinct types are what stop two same-typed members being swapped. Both point at the same construct. The next chapter is the type for a fixed set of named values, and it comes in two forms that differ in exactly one respect — whether its values convert to integers behind your back. The measurement is three operations that one form permits silently and the other refuses outright.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
