C++ Module 5 – Arrays, vectors and aggregate types
C++ structs, aggregates and designated initialisers
Group values in a C++ struct, fill it with braces or C++20 designated initialisers, and get all six comparisons from one defaulted operator<=>.
What you will learn
- Define structs whose members have sensible default values
- Write aggregate initialisation with braces and with designated initialisers
- Derive all six comparison operators from one defaulted operator<=>
- Choose between passing a struct by value and by const reference
Before you start
What you’ll learn · Before you start
What you will learn
- Define structs whose members have sensible default values
- Write aggregate initialisation with braces and with designated initialisers
- Derive all six comparison operators from one defaulted operator<=>
- Choose between passing a struct by value and by const reference
Before you start
On this page
A struct gives a group of related values one name and one type. struct Student { std::string name; int roll; double marks; }; declares a type with three members: every Student object holds its own name, roll number and
marks, and you reach them with a dot, as in asha.marks. A simple struct like this one, with public data members and
no constructors, is an aggregate, and C++ can initialise it straight from a list in braces:
Student asha{"Asha", 12, 91.5}; gives the members their values in the order they are declared. Since C++20 you can
also name the members you set, with designated initialisers: Student asha{.name = "Asha", .marks = 91.5};.
This lesson builds structs with sensible defaults, shows the rules that make a type an aggregate, and runs every example with Apple clang 21 and GCC 16, which treat some mistakes with designated initialisers differently. It ends with the comparison operators that the compiler can write for you and with how to pass structs to functions.
A struct groups named values
// A struct gives related values one name: here, the record of one student.
#include <print>
#include <string>
struct Student {
std::string name;
int roll = 0; // default member initialisers: used whenever
double marks = 0.0; // the braces give no value for the member
};
int main() {
Student asha{"Asha", 12, 91.5}; // values in declaration order
Student dev{"Dev", 7}; // marks keeps its default
Student blank{}; // every member gets its default
dev.marks = 72.5; // members are read and written with a dot
std::println("{} (roll {}): {}", asha.name, asha.roll, asha.marks);
std::println("{} (roll {}): {}", dev.name, dev.roll, dev.marks);
std::println("blank: name \"{}\", roll {}, marks {}", blank.name, blank.roll, blank.marks);
} from session import run
# student.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra student.cpp -o student && ./student
g++-16 -std=c++23 -Wall -Wextra student.cpp -o student-gcc && ./student-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra student.cpp -o student && ./student Asha (roll 12): 91.5 Dev (roll 7): 72.5 blank: name "", roll 0, marks 0 $ g++-16 -std=c++23 -Wall -Wextra student.cpp -o student-gcc && ./student-gcc Asha (roll 12): 91.5 Dev (roll 7): 72.5 blank: name "", roll 0, marks 0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 student_session.py
Each line of main creates a Student in a different way:
{"Asha", 12, 91.5}gives every member a value, in declaration order:name, thenroll, thenmarks.{"Dev", 7}gives two. The third member,marks, takes its default member initialiser, the= 0.0written next to it in the struct.{}gives none, so every member takes its default;name, which has no default member initialiser, becomes an empty string.
A struct is a type like int or std::string: you can keep structs in a std::vector, pass them to functions,
return them and put one struct inside another. Its definition ends with a semicolon after the closing brace, which is
easy to forget if you know languages where a type definition needs none.
The keywords struct and class both define a class type, and the only difference is a default. The members and base
classes of a struct are public unless you write otherwise; those of a class are private. The C++ Core Guidelines
(rule C.2) turn that into a convention: use struct when the members can each take any value independently, and
class when a rule ties them together, such as a date whose day must fit its month, which a constructor and private
members then protect.
What makes a type an aggregate
Initialising member by member from braces works only for aggregates. In C++20 and C++23 a class is an aggregate when it has:
- no user-declared constructors, not even one declared
= default, and no inherited ones; - no private or protected non-static data members;
- no virtual functions;
- no virtual, private or protected base classes.
Arrays are aggregates too. Member functions, static members, default member initialisers and public base classes are
all allowed. The trait std::is_aggregate_v, from <type_traits>, checks a type at compile time:
// Which of these types are aggregates? std::is_aggregate_v answers at compile time.
#include <array>
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
struct Point { int x; int y; }; // only public members
struct Settings { int width = 80; bool colour = true; }; // default member initialisers
struct Counter { int value = 0; void bump() { ++value; } }; // a member function
struct Labelled : Point { std::string label; }; // a public base class
struct Defaulted { Defaulted() = default; int x = 0; }; // a constructor, even a defaulted one
struct Hidden { int get() const { return secret; } private: int secret = 0; }; // a private member
struct Shape { virtual double area() const { return 0; } }; // a virtual function
int main() {
std::cout << std::boolalpha
<< "Point " << std::is_aggregate_v<Point> << '\n'
<< "Settings " << std::is_aggregate_v<Settings> << '\n'
<< "Counter " << std::is_aggregate_v<Counter> << '\n'
<< "Labelled " << std::is_aggregate_v<Labelled> << '\n'
<< "int[3] " << std::is_aggregate_v<int[3]> << '\n'
<< "std::array " << std::is_aggregate_v<std::array<int, 3>> << '\n'
<< "Defaulted " << std::is_aggregate_v<Defaulted> << '\n'
<< "Hidden " << std::is_aggregate_v<Hidden> << '\n'
<< "Shape " << std::is_aggregate_v<Shape> << '\n'
<< "std::pair " << std::is_aggregate_v<std::pair<int, int>> << '\n'
<< "std::string " << std::is_aggregate_v<std::string> << '\n';
} from session import run
# The same answers from both compilers in C++23, then the one row that C++20 changed, built as C++17.
run("""
clang++ -std=c++23 -Wall -Wextra is_aggregate.cpp -o is_aggregate && ./is_aggregate
g++-16 -std=c++23 -Wall -Wextra is_aggregate.cpp -o is_aggregate-gcc && ./is_aggregate-gcc
clang++ -std=c++17 is_aggregate.cpp -o is_aggregate17 && ./is_aggregate17 | grep Defaulted
g++-16 -std=c++17 is_aggregate.cpp -o is_aggregate17-gcc && ./is_aggregate17-gcc | grep Defaulted
""") Output
$ clang++ -std=c++23 -Wall -Wextra is_aggregate.cpp -o is_aggregate && ./is_aggregate Point true Settings true Counter true Labelled true int[3] true std::array true Defaulted false Hidden false Shape false std::pair false std::string false $ g++-16 -std=c++23 -Wall -Wextra is_aggregate.cpp -o is_aggregate-gcc && ./is_aggregate-gcc Point true Settings true Counter true Labelled true int[3] true std::array true Defaulted false Hidden false Shape false std::pair false std::string false $ clang++ -std=c++17 is_aggregate.cpp -o is_aggregate17 && ./is_aggregate17 | grep Defaulted Defaulted true $ g++-16 -std=c++17 is_aggregate.cpp -o is_aggregate17-gcc && ./is_aggregate17-gcc | grep Defaulted Defaulted true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 is_aggregate_session.py
std::array is an aggregate (a struct with one public array member), which is why std::array<int, 3> a{1, 2, 3};
works without any constructor. std::pair and std::string are not aggregates: they have constructors, and braces
call one of those instead. A struct with a public base class, such as Labelled, takes the base’s values first, in
their own braces: Labelled corner{{1, 2}, "corner"};.
Changed in C++20
The last two commands build the same file as C++17, where Defaulted was still an aggregate, because a constructor
declared = default did not count. C++20 changed the rule with paper P1008R1: in C++17 even
struct NoInit { NoInit() = delete; int x; }; could be created with NoInit n{};, because aggregate initialisation
never calls a constructor. Both compilers accept that line as C++17 and reject it as C++20.
Default member initialisers
A default member initialiser, such as int roll = 0;, keeps a member’s default right where the member is declared.
Every way of creating the struct uses it unless it gives the member another value, and since C++14 a struct with
default member initialisers is still an aggregate.
A member that the braces leave out and that has no default member initialiser is value-initialised: numbers become
zero, bool becomes false, pointers become null, and class types such as std::string are created empty. Leaving a
member out without meaning to can hide a forgotten value, so with -Wextra both compilers warn when a brace list stops
before a member that has no default:
// Members left out of the braces, with and without default member initialisers.
struct Raw { int a; int b; int c; }; // no defaults
struct WithDefaults { int a = 0; int b = 0; int c = 0; };
int main() {
Raw positional{1, 2}; // c becomes 0, but was it forgotten?
Raw named{.a = 1}; // b and c become 0
WithDefaults d{1, 2}; // c keeps its default on purpose
return positional.c + named.b + d.c;
} from session import run
# -Wextra asks both compilers to report members that a brace list leaves out.
run("""
clang++ -std=c++23 -Wall -Wextra -fsyntax-only missing_fields.cpp
g++-16 -std=c++23 -Wall -Wextra -fsyntax-only missing_fields.cpp
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsyntax-only missing_fields.cpp
missing_fields.cpp:6:24: warning: missing field 'c' initializer [-Wmissing-field-initializers]
6 | Raw positional{1, 2}; // c becomes 0, but was it forgotten?
| ^
1 warning generated.
$ g++-16 -std=c++23 -Wall -Wextra -fsyntax-only missing_fields.cpp
missing_fields.cpp: In function 'int main()':
missing_fields.cpp:6:24: warning: missing initializer for member 'Raw::c' [-Wmissing-field-initializers]
6 | Raw positional{1, 2}; // c becomes 0, but was it forgotten?
| ^
missing_fields.cpp:7:21: warning: missing initializer for member 'Raw::b' [-Wmissing-field-initializers]
7 | Raw named{.a = 1}; // b and c become 0
| ^
missing_fields.cpp:7:21: warning: missing initializer for member 'Raw::c' [-Wmissing-field-initializers]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_fields_session.py
Both compilers report the positional list that stops before c. GCC also reports the designated list that names only
a; Clang 21 does not. Neither says anything about WithDefaults d{1, 2}, because a member with a default member
initialiser was left out on purpose. That is a good reason to give every member a default.
Leaving out the braces altogether is worse. Inside a function, Raw r; default-initialises the struct, which leaves
members without a default member initialiser uninitialised, and reading one of them is undefined behaviour in C++23.
C++26 turns such a read into erroneous behaviour, which tools are encouraged to report (paper P2795R5), but the value
is still not one you chose. Write Raw r{};, or give every member a default.
Designated initialisers name the members
C++20’s designated initialisers write the name of each member you set, with a dot in front of it:
// Designated initialisers name the members they set; the others keep their defaults.
#include <print>
#include <string>
struct Window {
std::string title = "Untitled";
int width = 80;
int height = 24;
bool resizable = true;
};
void open(const Window& w) {
std::println("{}: {} x {}, resizable: {}", w.title, w.width, w.height, w.resizable);
}
int main() {
Window report{.title = "Report", .width = 120};
open(report);
open({.height = 40, .resizable = false}); // reads like a call with named arguments
open({}); // every member keeps its default
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra window.cpp -o window && ./window
g++-16 -std=c++23 -Wall -Wextra window.cpp -o window-gcc && ./window-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra window.cpp -o window && ./window Report: 120 x 24, resizable: true Untitled: 80 x 40, resizable: false Untitled: 80 x 24, resizable: true $ g++-16 -std=c++23 -Wall -Wextra window.cpp -o window-gcc && ./window-gcc Report: 120 x 24, resizable: true Untitled: 80 x 40, resizable: false Untitled: 80 x 24, resizable: true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 window_session.py
How designated initialisers and default member initialisers fill one struct
Text description of the diagram
The diagram shows where the four members of report get their values in Window report{.title = "Report", .width = 120};.
- The braces name two members, so title becomes "Report" and width becomes 120.
- The braces do not name height or resizable, so these take the default member initialisers written in struct Window: height = 24 and resizable = true.
- The object report then holds title = "Report", width = 120, height = 24 and resizable = true, initialised in the order the members are declared in the struct. That order is why the designators in the braces have to follow it.
Every member that the braces do not name takes its default member initialiser, so report keeps the default height
and stays resizable. The second call to open shows the most useful pattern: a function that takes a struct of
options can be called with designated initialisers, which reads like a call with named arguments, and you can add
options to the struct later without touching a single call.
C++’s designated initialisers are stricter than C’s:
- the designators must follow the order in which the members are declared, although you may skip members;
- one pair of braces is either all designated or all positional, never a mixture;
- a designator names a direct member only, so write
.size = {.w = 3}instead of C’s.size.w = 3; - arrays cannot be initialised with C’s
[2] = valuedesignators.
When the two compilers disagree
For each broken rule the standard only requires a diagnostic, and a compiler may make that a warning and accept the code anyway. Clang does exactly that for three of these files, treating them as extensions borrowed from C, while GCC lets a different one through. Each file breaks one rule:
C++ (wrong_order.cpp) · wrong_order.cpp
// Designators out of declaration order: height is declared after width.
struct Window { int width = 80; int height = 24; bool resizable = true; };
int main() {
Window w{.height = 40, .width = 100};
return w.width == 100 ? 0 : 1;
} C++ (mixed.cpp) · mixed.cpp
// A positional value and a designated one in the same braces.
struct Window { int width = 80; int height = 24; bool resizable = true; };
int main() {
Window w{100, .resizable = false};
return w.width == 100 ? 0 : 1;
} C++ (nested.cpp) · nested.cpp
// A designator that reaches into a member of a member, as C allows.
struct Size { int w = 0; int h = 0; };
struct Window { Size size; bool resizable = true; };
int main() {
Window w{.size.w = 3};
return w.size.w == 3 ? 0 : 1;
} C++ (narrowing.cpp) · narrowing.cpp
// A double squeezed into an int member: braces forbid narrowing conversions.
struct Point { int x = 0; int y = 0; };
int main() {
double width = 2.5;
Point p{width, 1};
return p.y == 1 ? 0 : 1;
} from session import run
# The four files break rules of the standard. First the full report on wrong_order.cpp, then the first
# message for each of the others, then the verdict with -pedantic-errors.
run("""
clang++ -std=c++23 -fsyntax-only wrong_order.cpp
g++-16 -std=c++23 -fsyntax-only wrong_order.cpp
for f in mixed nested narrowing; do clang++ -std=c++23 -fsyntax-only $f.cpp 2>&1 | grep -m1 -E "warning:|error:"; done
for f in mixed nested narrowing; do g++-16 -std=c++23 -fsyntax-only $f.cpp 2>&1 | grep -m1 -E "warning:|error:"; done
for f in wrong_order mixed nested narrowing; do clang++ -std=c++23 -pedantic-errors -fsyntax-only $f.cpp 2>/dev/null || echo "clang++ rejects $f.cpp"; done
for f in wrong_order mixed nested narrowing; do g++-16 -std=c++23 -pedantic-errors -fsyntax-only $f.cpp 2>/dev/null || echo "g++-16 rejects $f.cpp"; done
""") Output
$ clang++ -std=c++23 -fsyntax-only wrong_order.cpp
wrong_order.cpp:5:37: warning: ISO C++ requires field designators to be specified in declaration order; field 'height' will be initialized after field 'width' [-Wreorder-init-list]
5 | Window w{.height = 40, .width = 100};
| ~~~~~~~~~^~~
wrong_order.cpp:5:24: note: previous initialization for field 'height' is here
5 | Window w{.height = 40, .width = 100};
| ^~
1 warning generated.
$ g++-16 -std=c++23 -fsyntax-only wrong_order.cpp
wrong_order.cpp: In function 'int main()':
wrong_order.cpp:5:40: error: designator order for field 'Window::width' does not match declaration order in 'Window'
5 | Window w{.height = 40, .width = 100};
| ^
[exit status 1]
$ for f in mixed nested narrowing; do clang++ -std=c++23 -fsyntax-only $f.cpp 2>&1 | grep -m1 -E "warning:|error:"; done
mixed.cpp:5:19: warning: mixture of designated and non-designated initializers in the same initializer list is a C99 extension [-Wc99-designator]
nested.cpp:6:14: warning: nested designators are a C99 extension [-Wc99-designator]
narrowing.cpp:6:13: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
$ for f in mixed nested narrowing; do g++-16 -std=c++23 -fsyntax-only $f.cpp 2>&1 | grep -m1 -E "warning:|error:"; done
mixed.cpp:5:19: error: either all initializer clauses should be designated or none of them should be
nested.cpp:6:14: error: expected primary-expression before '.' token
narrowing.cpp:6:13: warning: narrowing conversion of 'width' from 'double' to 'int' [-Wnarrowing]
$ for f in wrong_order mixed nested narrowing; do clang++ -std=c++23 -pedantic-errors -fsyntax-only $f.cpp 2>/dev/null || echo "clang++ rejects $f.cpp"; done
clang++ rejects wrong_order.cpp
clang++ rejects mixed.cpp
clang++ rejects nested.cpp
clang++ rejects narrowing.cpp
$ for f in wrong_order mixed nested narrowing; do g++-16 -std=c++23 -pedantic-errors -fsyntax-only $f.cpp 2>/dev/null || echo "g++-16 rejects $f.cpp"; done
g++-16 rejects wrong_order.cpp
g++-16 rejects mixed.cpp
g++-16 rejects nested.cpp
g++-16 rejects narrowing.cpp
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 designators_session.py
| Criterion | Apple clang 21 | GCC 16 |
|---|---|---|
| Designators out of declaration order | Warns, then compiles | Rejects |
| Designated and positional values mixed | Warns, then compiles | Rejects |
| A nested designator such as .size.w | Warns, then compiles | Rejects |
| A double variable narrowed to an int member | Rejects | Warns, then compiles |
| All four files, with pedantic errors turned on | Rejects all four | Rejects all four |
Clang’s three warnings belong to -Wreorder-init-list and -Wc99-designator, and GCC’s to -Wnarrowing, as the
session shows. Read the first report closely: Clang initialises height after width, in declaration order,
whatever order the braces suggest, which is why the standard asks for the order to match. GCC’s manual explains its
own exception: a narrowing conversion from a constant such as 2.5 is an error, but from a variable it is only a
warning.
Because each compiler lets a different mistake through, code that builds on your machine can fail on a teammate’s.
Add -pedantic-errors, which turns every diagnostic the standard requires into an error, to the builds of every
compiler you support. With it, both compilers reject all four files, as the last two commands show.
Comparisons with = default
C++20 can write a struct’s comparison operators for you. Declare the three-way comparison operator, <=>, and default
it:
// A defaulted operator<=> compares the members in declaration order: total first, then name.
#include <algorithm>
#include <compare>
#include <functional>
#include <print>
#include <string>
#include <vector>
struct Result {
int total = 0;
std::string name;
auto operator<=>(const Result&) const = default; // also declares a defaulted ==
};
int main() {
std::vector<Result> results{{72, "Kabir"}, {91, "Asha"}, {72, "Dev"}, {85, "Meera"}};
std::ranges::sort(results); // smallest first, with <
for (const Result& r : results) std::println("{} {}", r.total, r.name);
std::ranges::sort(results, std::greater{}); // largest first, with >
std::println("top: {} {}", results.front().total, results.front().name);
std::println("{}", Result{72, "Dev"} == Result{72, "Dev"});
std::println("{}", Result{72, "Dev"} < Result{72, "Kabir"});
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra rank_results.cpp -o rank_results && ./rank_results
g++-16 -std=c++23 -Wall -Wextra rank_results.cpp -o rank_results-gcc && ./rank_results-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra rank_results.cpp -o rank_results && ./rank_results 72 Dev 72 Kabir 85 Meera 91 Asha top: 91 Asha true true $ g++-16 -std=c++23 -Wall -Wextra rank_results.cpp -o rank_results-gcc && ./rank_results-gcc 72 Dev 72 Kabir 85 Meera 91 Asha top: 91 Asha true true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rank_results_session.py
The defaulted <=> compares two Results member by member, in declaration order: total first and, only when the
totals are equal, name. That is why Dev comes before Kabir, since both scored 72. From <=> the compiler derives
<, <=, > and >=. Because the struct does not declare == itself, defaulting <=> also declares a defaulted
==, which compares every member for equality and gives you != as well. One line therefore provides all six
comparisons, which is what std::ranges::sort (with <) and std::greater{} (with >) rely on.
A few details decide whether the defaulted operators do what you want:
- To sort by name first, declare
namefirst, or sort with a comparison of your own; the next lesson builds one withstd::tie. bool operator==(const Result&) const = default;on its own gives==and!=, but no ordering.- Here
<=>returnsstd::strong_ordering. Adoublemember makes itstd::partial_orderinginstead, because NaN, the “not a number” value, is unordered with every number. - Every member must support the comparison: if one member’s type has no
<=>, a defaultedauto operator<=>is deleted, and using<on the struct fails to compile with both compilers. - Include
<compare>, the header that defines the ordering types.
Passing and returning structs
How a function should take a struct depends on its size. Rule F.16 of the C++ Core Guidelines says to pass types
that are cheap to copy, about two or three machine words, by value, and everything else by reference to const.
sizeof shows what the sizes are with each standard library:
// The size of a struct in bytes, as sizeof reports it, padding included.
#include <print>
#include <string>
struct Point { int x = 0; int y = 0; };
struct Student { std::string name; int roll = 0; double marks = 0.0; };
int main() {
std::println("pointer {:>2} bytes", sizeof(void*));
std::println("Point {:>2} bytes", sizeof(Point));
std::println("std::string {:>2} bytes", sizeof(std::string));
std::println("Student {:>2} bytes", sizeof(Student));
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra sizes.cpp -o sizes && ./sizes
g++-16 -std=c++23 -Wall -Wextra sizes.cpp -o sizes-gcc && ./sizes-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra sizes.cpp -o sizes && ./sizes pointer 8 bytes Point 8 bytes std::string 24 bytes Student 40 bytes $ g++-16 -std=c++23 -Wall -Wextra sizes.cpp -o sizes-gcc && ./sizes-gcc pointer 8 bytes Point 8 bytes std::string 32 bytes Student 48 bytes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sizes_session.py
A Point is 8 bytes, a single machine word here, so a function such as double length(Point p) takes it by value. A
Student is 40 bytes with Apple clang’s libc++ and 48 with GCC’s libstdc++, because the two libraries lay out
std::string differently, in 24 and 32 bytes. Copying a Student also copies its name, which needs a memory
allocation once the name is too long to fit inside the string object, so pass it as const Student&. Four of its
bytes, between roll and marks, are padding: they keep the double at an address that is a multiple of 8. A
later lesson on memory layout shows how the order of members changes the padding.
Returning a struct by value is cheap. When a function returns a temporary, as in return Summary{runs, wickets};,
C++17 guarantees that the object is built directly where the caller stores it, with no copy at all, so return a
struct rather than filling in reference parameters.
Your turn: an innings summary
A cricket innings is a list of deliveries. In this exercise’s simplified scoring, each delivery has the runs scored
from it, may be an extra (a wide or a no-ball, which adds one more run and is not one of the six balls of an
over), and may take a wicket. innings.h declares the structs and three functions for you to complete:
summariseadds up the runs (one more for each extra), the wickets and the legal balls.overs_textwrites the legal balls in cricket’s notation: 20 balls are 3 overs and 2 balls,"3.2".run_rategives the runs per six legal balls, and 0 before the first legal ball.
C header · innings.h
// Starter code for "Your turn": complete summarise, overs_text and run_rate.
#pragma once
#include <string>
#include <vector>
// One delivery, with the simplified scoring of this exercise.
struct Ball {
int runs = 0; // runs scored from this delivery
bool extra = false; // a wide or a no-ball: one more run, and it is not one of the six balls of an over
bool wicket = false; // a batter is out
};
struct InningsSummary {
int runs = 0;
int wickets = 0;
int legal_balls = 0; // deliveries that count towards an over
bool operator==(const InningsSummary&) const = default;
};
// Adds up the deliveries of an innings.
inline InningsSummary summarise(const std::vector<Ball>& balls) {
InningsSummary total;
// TODO: count the runs (one more for every extra), the wickets and the legal balls.
(void)balls;
return total;
}
// Overs in cricket's notation: 20 legal balls are 3 overs and 2 balls, written "3.2".
inline std::string overs_text(const InningsSummary& s) {
// TODO
(void)s;
return "";
}
// Runs per over of six legal balls; 0 when no legal ball has been bowled.
inline double run_rate(const InningsSummary& s) {
// TODO
(void)s;
return 0.0;
} C++ · innings_tests.cpp
// Sample tests for innings.h. Build: clang++ -std=c++23 -Wall -Wextra innings_tests.cpp -o innings_tests
#include <cmath>
#include <print>
#include <vector>
#include "innings.h"
int checks = 0;
int failures = 0;
void check(bool ok, const char* what) {
++checks;
if (!ok) {
++failures;
std::println("FAIL {}", what);
}
}
int main() {
std::vector<Ball> over{{.runs = 1}, {.runs = 4}, {}, {.extra = true}, {.runs = 6}, {.wicket = true}, {.runs = 2}};
InningsSummary first = summarise(over);
check(first == InningsSummary{.runs = 14, .wickets = 1, .legal_balls = 6}, "an over with a wide: 14 runs, 1 wicket, 6 legal balls");
check(overs_text(first) == "1.0", "6 legal balls are 1.0 overs");
check(std::abs(run_rate(first) - 14.0) < 1e-9, "14 runs in one over: a run rate of 14");
InningsSummary twenty{.runs = 30, .wickets = 2, .legal_balls = 20};
check(overs_text(twenty) == "3.2", "20 legal balls are 3.2 overs");
check(std::abs(run_rate(twenty) - 9.0) < 1e-9, "30 runs from 20 balls: a run rate of 9, not 30 / 3.2");
check(summarise({}) == InningsSummary{}, "an empty innings adds up to zero");
check(overs_text(InningsSummary{}) == "0.0", "no balls are 0.0 overs");
check(run_rate(InningsSummary{}) == 0.0, "no balls: a run rate of 0");
std::println("{} of {} checks passed", checks - failures, checks);
return failures == 0 ? 0 : 1;
} The tests build each Ball with designated initialisers, such as {.runs = 4} and {.extra = true}, and compare
whole summaries with the defaulted ==. Before you change anything, the starter code fails most of them:
from session import run
# The sample tests run against the starter code, before any TODO is done.
run("""
clang++ -std=c++23 -Wall -Wextra innings_tests.cpp -o innings_tests && ./innings_tests
g++-16 -std=c++23 -Wall -Wextra innings_tests.cpp -o innings_tests-gcc && ./innings_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra innings_tests.cpp -o innings_tests && ./innings_tests FAIL an over with a wide: 14 runs, 1 wicket, 6 legal balls FAIL 6 legal balls are 1.0 overs FAIL 14 runs in one over: a run rate of 14 FAIL 20 legal balls are 3.2 overs FAIL 30 runs from 20 balls: a run rate of 9, not 30 / 3.2 FAIL no balls are 0.0 overs 2 of 8 checks passed [exit status 1] $ g++-16 -std=c++23 -Wall -Wextra innings_tests.cpp -o innings_tests-gcc && ./innings_tests-gcc FAIL an over with a wide: 14 runs, 1 wicket, 6 legal balls FAIL 6 legal balls are 1.0 overs FAIL 14 runs in one over: a run rate of 14 FAIL 20 legal balls are 3.2 overs FAIL 30 runs from 20 balls: a run rate of 9, not 30 / 3.2 FAIL no balls are 0.0 overs 2 of 8 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 innings_session.py
Edit innings.h until both compilers print 8 of 8 checks passed and the program exits with status 0. Watch the
run rate: “3.2 overs” means 20 balls, which is 3.33 overs as a number, so 30 runs make a rate of 9 and not 30 ÷ 3.2.
Then add a std::string bowler member with a default to Ball and build the tests again: they compile unchanged,
because designated initialisers name only the members they set.
Interview questions
Fresher · What is the difference between a struct and a class in C++?
Only the default access. The members and base classes of a struct are public unless you write otherwise, and those
of a class are private. Everything else is the same: both can have constructors, member functions, virtual
functions and base classes. The difference that matters in practice is the convention. A struct usually holds plain
values that can each change independently, so it is often an aggregate with public members that you initialise with
braces or designated initialisers. A class keeps an invariant, a rule between its members such as “the day fits the
month”, and protects it with private members and a constructor that checks it. Saying that a struct “cannot have
member functions” is the common wrong answer: that is true in C, not in C++.
Key takeaways
- A struct groups named members into one type. Its members and base classes are public by default, which is its only
difference from a
class. - A class with no user-declared constructors, only public data members, no virtual functions and no virtual or
non-public base classes is an aggregate, filled member by member from braces. Since C++20 even a constructor
declared
= defaultdisqualifies it. - Give members default member initialisers: braces that leave a member out then use the default, and
-Wextrareports only the members left out that have no default. - Designated initialisers (C++20) name members in declaration order and never mix with positional values. Build with
-pedantic-errors, so that Clang and GCC both reject the mistakes they treat differently by default. auto operator<=>(const T&) const = default;gives all six comparisons, member by member in declaration order.- Pass structs of two or three words by value and larger ones by
const&, and return structs by value.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- P0329R4: Designated Initialization Wording (ISO/IEC JTC1/SC22/WG21)
- P1008R1: Prohibit aggregates with user-declared constructors (ISO/IEC JTC1/SC22/WG21)
- P0515R3: Consistent comparison (ISO/IEC JTC1/SC22/WG21)
- P1185R2: <=> != == (ISO/IEC JTC1/SC22/WG21)
- P2795R5: Erroneous behaviour for uninitialized reads (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: F.16, pass cheaply-copied types by value (Standard C++ Foundation)
- C++ Core Guidelines: C.2, use struct if the data members can vary independently (Standard C++ Foundation)
- Diagnostic flags in Clang (LLVM Project)
- Options to Request or Suppress Warnings (Using the GNU Compiler Collection) (GNU Project)
- Options Controlling C++ Dialect (Using the GNU Compiler Collection) (GNU Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress