Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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<=>.

  • Beginner
  • 22 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 22 minutes

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

student.cpp C++ · student.cpp
// 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);
}
Built and run with both compilers Python · student_session.py
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, then roll, then marks.
  • {"Dev", 7} gives two. The third member, marks, takes its default member initialiser, the = 0.0 written 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.

JSON to Code Generator Paste a JSON sample to get matching C++ structs with public members, aggregates you can fill with braces.

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:

is_aggregate.cpp C++ · is_aggregate.cpp
// 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';
}
Aggregate or not, according to both compilers Python · is_aggregate_session.py
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:

missing_fields.cpp C++ · missing_fields.cpp
// 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;
}
Members left out, as -Wextra reports them Python · missing_fields_session.py
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:

window.cpp C++ · window.cpp
// 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
}
Three windows from one struct Python · window_session.py
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

The braces set title and width; height and resizable take the defaults in struct Window; report holds all four in order.In the braces.title = "Report".width = 120In struct Windowheight = 24resizable = truereport, in declaration ordertitle = "Report"width = 120height = 24resizable = truenamedmembersevery othermember

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};.

  1. The braces name two members, so title becomes "Report" and width becomes 120.
  2. The braces do not name height or resizable, so these take the default member initialisers written in struct Window: height = 24 and resizable = true.
  3. 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] = value designators.

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:

Four files that break a 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;
}
Warnings, errors and -pedantic-errors Python · designators_session.py
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

What each compiler does by default in C++23 mode
Criterion Apple clang 21GCC 16
Designators out of declaration order Warns, then compilesRejects
Designated and positional values mixed Warns, then compilesRejects
A nested designator such as .size.w Warns, then compilesRejects
A double variable narrowed to an int member RejectsWarns, then compiles
All four files, with pedantic errors turned on Rejects all fourRejects 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:

rank_results.cpp C++ · rank_results.cpp
// 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"});
}
Sorted by the defaulted comparison Python · rank_results_session.py
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 name first, or sort with a comparison of your own; the next lesson builds one with std::tie.
  • bool operator==(const Result&) const = default; on its own gives == and !=, but no ordering.
  • Here <=> returns std::strong_ordering. A double member makes it std::partial_ordering instead, 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 defaulted auto 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:

sizes.cpp C++ · sizes.cpp
// 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));
}
Sizes on 64-bit arm64, with libc++ and with libstdc++ Python · sizes_session.py
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:

  1. summarise adds up the runs (one more for each extra), the wickets and the legal balls.
  2. overs_text writes the legal balls in cricket’s notation: 20 balls are 3 overs and 2 balls, "3.2".
  3. run_rate gives the runs per six legal balls, and 0 before the first legal ball.
The starter code and its sample tests

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:

The sample tests against the starter code Python · innings_session.py
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 = default disqualifies it.
  • Give members default member initialisers: braces that leave a member out then use the default, and -Wextra reports 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.

  1. Question 1 of 6 Which of these structs are aggregates in C++23?

    Choose every answer that is right.

    Show the answer to question 1

    Answer:

    • struct B { int x; void reset() { x = 0; } };
    • struct A { int x = 1; int y = 2; };

    Default member initialisers and member functions are allowed in an aggregate. A user-declared constructor, even one declared = default (since C++20), a virtual function and a private data member each stop a type from being an aggregate.

  2. Question 2 of 6 width is declared before height in Window. What do the two compilers do with this line, built with -std=c++23 and no other option?

    Read the code, then choose one answer.

    Window w{.height = 40, .width = 100};
    Show the answer to question 2

    Answer: GCC 16 rejects it with an error; Clang compiles it with a warning

    Designators must follow the declaration order. GCC reports an error, while Clang warns with -Wreorder-init-list and initialises width first anyway. With -pedantic-errors both compilers reject it.

  3. Question 3 of 6 Given struct Score { int runs = 0; int balls; };, what is s.balls after Score s{12};?

    Choose one answer.

    Show the answer to question 3

    Answer: 0, because a member left out of the braces is value-initialised

    The 12 goes to runs, the first member. balls has no default member initialiser, so it is value-initialised to 0. With -Wextra both compilers warn that the brace list left it out.

  4. Question 4 of 6 A struct declares only auto operator<=>(const Tally&) const = default;. Which comparisons can you use on it?

    Choose one answer.

    Show the answer to question 4

    Answer: All six, == and != included

    The ordering operators are derived from <=>, and because the struct declares no == of its own, defaulting <=> also declares a defaulted ==, from which != follows.

  5. Question 5 of 6 Following rule F.16 of the C++ Core Guidelines, how should double area(...) take struct Rect { double w; double h; };, which is 16 bytes?

    Choose one answer.

    Show the answer to question 5

    Answer: By value, Rect r

    Two doubles are two machine words on a 64-bit computer, cheap to copy, so pass them by value. A struct holding a std::string or a std::vector is better passed as a reference to const.

  6. Question 6 of 6 An innings has 30 runs from 20 legal balls, written 3.2 overs. What is its run rate, in runs per over?

    Type a number.

    Show the answer to question 6

    Answer: 9 runs per over (anything from 8.99 to 9.01 counts)

    20 balls are 20 ÷ 6 = 3.33 overs as a number, so the rate is 30 × 6 ÷ 20 = 9. Dividing 30 by 3.2 treats the ball count after the dot as a decimal fraction and gives the wrong 9.375.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.