C++ / CLASSES AND OBJECT LIFECYCLE
Structs, aggregates, and plain data bundles
Initialise plain data bundles with brace lists, predict what unlisted members get, and know what keeps a struct an aggregate.
What you will learn
- Brace-initialise a struct and track how elements map to members in declaration order
- Predict unlisted members: default member initialiser first, otherwise value-initialisation
- Spot what destroys aggregate status: any declared constructor, private data, virtuals
- Use designated initialisers (C++20) to name fields, still in declaration order
Understanding Structs, aggregates, and plain data bundles
`struct` and `class` differ in exactly one thing: members and base classes are public by default in a struct. The distinction that actually changes behaviour is whether the type is an aggregate — a class with no user-declared or inherited constructors, no private or protected non-static data members, no virtual functions and no virtual bases. When a type is an aggregate, `Sensor s{...}` calls no constructor at all: the compiler pairs the elements of the brace list with the non-static data members in declaration order and initialises each one directly.
That member-by-member rule explains everything else. Supplying fewer initialisers than there are members is legal: each remaining member uses its default member initialiser if it has one, otherwise it is value-initialised, which means zero for scalars and the default constructor for class members. So `Sensor c{}` gives `channel` the value 0 while `scale` still becomes 1.0. And because the list is checked element by element against each member's type, narrowing is diagnosed: passing 3.5 for an `int` member inside braces is a compile error, where an assignment would have quietly truncated it to 3.
Think of a plain data bundle as a named layout with no invariant to defend. Nothing needs validating on construction, so there is nothing for a constructor to do, and staying an aggregate buys real things: brace and designated initialisation, easy `constexpr` use, and — when every member is trivially copyable — a type you can memcpy or read straight out of a binary buffer. The price is that declaration order becomes part of the public interface, since every positional brace list depends on it, and that adding one convenience constructor takes aggregate initialisation away from every existing call site.
<iostream>
<string>
struct Sensor {
std::string id;
int channel;
double scale = 1.0; // default member initialiser; still an aggregate
bool active;
};
void report(const Sensor& s) {
std::cout << '[' << s.id << "] ch=" << s.channel
<< " scale=" << s.scale
<< " active=" << s.active << '\n';
}
int main() {
Sensor a{"temp-1", 3, 0.5, true}; // all four members listed
Sensor b{"hum-2", 7}; // scale keeps its default, active value-initialised
Sensor c{}; // nothing listed at all
report(a);
report(b);
report(c);
}
A struct is only special while it stays an aggregate: initialisation then means filling members one at a time in declaration order, with no constructor involved.
Worked examples
Naming fields with designated initialisers
Designated initialisers label and skip members, but must follow declaration order.
<iostream>
struct Window {
int width;
int height;
bool fullscreen;
const char* title;
};
int main() {
Window w{.width = 1280, .height = 720, .title = "editor"};
std::cout << w.width << 'x' << w.height
<< " fullscreen=" << w.fullscreen
<< " title=" << w.title << '\n';
}
Example explained
Line 1`.width` and `.height` name their members, so nobody has to count positions to read the call.
Line 2`fullscreen` is skipped and has no default member initialiser, so it is value-initialised to `false` and streams as 0.
Line 3Skipping a member is allowed, but reordering is not: `.title` before `.height` is ill-formed in C++20 even though C accepts it.
Line 4Compile with `-std=c++20`; on older standards the same aggregate still works positionally as `Window w{1280, 720, false, "editor"}`.
Nested aggregates and brace elision
Inner braces may be omitted, and the flat list is still consumed in declaration order.
<iostream>
struct Point { int x; int y; };
struct Rect { Point origin; Point size; };
void show(const char* tag, const Rect& r) {
std::cout << tag << ": origin " << r.origin.x << ',' << r.origin.y
<< " size " << r.size.x << ',' << r.size.y << '\n';
}
int main() {
Rect a{{1, 2}, {30, 40}};
Rect b{3, 4, 50, 60};
Rect c{{5, 6}};
show("a", a);
show("b", b);
show("c", c);
}
Example explained
Line 1`Rect a{{1, 2}, {30, 40}}` gives each `Point` its own brace list, which is the form worth preferring.
Line 2`Rect b{3, 4, 50, 60}` relies on brace elision: elements are fed to `origin.x`, `origin.y`, `size.x`, `size.y` in that order.
Line 3`Rect c{{5, 6}}` leaves `size` with no initialiser at all, so it is value-initialised and both coordinates read 0.
Line 4Because elision is purely positional, swapping the `origin` and `size` declarations would silently change what line `b` builds.
Returning two values without a constructor
`return {a, b};` aggregate-initialises the result, and a structured binding unpacks it.
<cstddef>
<iostream>
<string>
struct Split {
std::string head;
std::string tail;
};
Split split_at(const std::string& s, std::size_t pos) {
return {s.substr(0, pos), s.substr(pos)};
}
int main() {
auto [h, t] = split_at("aggregate", 3);
std::cout << h << '|' << t << '\n';
std::cout << split_at("aggregate", 0).tail << '\n';
}
Example explained
Line 1`return {s.substr(0, pos), s.substr(pos)};` needs no constructor: the braced list initialises `head` then `tail` in declaration order.
Line 2`auto [h, t]` binds to the two members by position, so renaming a member leaves this call site alone but reordering them swaps the results.
Line 3The second call shows the bundle is an ordinary value, so reading `.tail` off the temporary is fine.
Line 4The structured binding needs C++17; the struct and the `return {...}` are valid all the way back to C++11.
Important notes
Designated initialisers are C++20 and must follow declaration order; C's out-of-order designators, `[0] = ...` array designators and the `.field: value` GNU form are not valid C++.
In C++20 any user-declared constructor disqualifies an aggregate, including `Sensor() = default;` written inside the class, so leave the special members undeclared if you want `Sensor{...}` to keep working.
Common mistakes
Writing `Sensor s;` and expecting zeros: that default-initialises, so `channel` and `active` hold indeterminate values and reading them is undefined behaviour — only `Sensor s{};` zero-fills them.
Adding one convenience constructor: the struct stops being an aggregate, so existing `Sensor s{"hum-2", 7}` lines no longer match anything and fail to compile.
Reordering members to group them or improve packing: positional lists like `Rect{3, 4, 50, 60}` still compile but now put the numbers in different members, with no diagnostic.
Try it yourself
Change, predict, then run
Define `struct Colour { unsigned char r, g, b; unsigned char a = 255; };`, then create one value with all four channels given and one with only `r` and `g` given. Print all four channels of both with `static_cast<int>` and check your prediction for the unsupplied members.
Open the C++ workspaceCheck your understanding
A codebase uses `struct Vec { double x; double y; };` as `Vec v{1.0, 2.0};` in dozens of places. Someone adds `Vec(double s) : x(s), y(s) {}` for convenience. What happens to the existing `Vec v{1.0, 2.0};` lines?
- They keep working, because brace initialisation always writes the members directly.
- They compile, but only `x` is initialised and `y` is left indeterminate.
- They fail to compile: `Vec` is no longer an aggregate and has no two-argument constructor.
- They compile and call the new constructor once per member.
Show answer
Declaring any constructor makes `Vec` a non-aggregate, so `Vec v{1.0, 2.0}` becomes list-initialisation resolved against constructors; the only candidates are `Vec(double)` plus the implicit copy and move constructors, none of which take two arguments, so the line is ill-formed. Option 0 is tempting because braces look like they always fill members, but filling members one by one is a property of aggregates, not of the brace syntax.