What This Chapter Added

Up to this chapter, every type you declared came out of a fixed catalogue: int, double, char, bool, and the standard library types built on top of them. Those are the fundamental types, and the language hands them to you already defined. This chapter gave you the other half of the type system, the half where you write the type yourself.

Everything below is reviewed against one running example: the route board at a climbing gym. A route sits on a wall of some angle, its hardest hold has some shape, someone set it, and it takes some number of moves. Not one of those facts fits an int honestly, which is exactly the situation program-defined types exist for.

Two Families of Program-Defined Type

When the built-in types cannot express what you mean, you define your own. Such a type is a program-defined type, and the standard's broader term for the same idea is user-defined type. They come in two families, and the whole chapter divides along that line.

Family Keywords What one object holds Reach for it when
Enumerated types enum, enum class exactly one name from a fixed list the value is a choice from a short, closed set
Class types struct, class, union several values at once, one per member the concept is several facts that travel together

Both families obey the same two rules. The compiler needs the full definition before you use the type, since a forward declaration does not tell it how large an object is or what is inside one. And the definition itself, called a type definition, is exempt from the one-definition rule: you may repeat it in every translation unit that needs it, which is why type definitions live happily in headers. Repeat it twice inside a single file and you still have an error.

Fundamental Versus Program-Defined
The difference is authorship, not capability. Fundamental types are built into the language and always available. Program-defined types are ones you write, and they must be defined before use. Once defined, yours is a real type: it has a name, the compiler type-checks it, and it appears by name in diagnostics.

Enumerations: The Same Idea Behind Two Keywords

An enumeration (also called an enumerated type or an enum) is a compound type whose every possible value is a symbolic constant that you named. Each of those constants is an enumerator. Both keywords produce a distinct type, meaning the compiler can tell it apart from every other type. This is the line that separates an enumeration from a type alias: an alias is only a second spelling for a type that already exists, so the compiler cannot tell the alias from the original and cannot reject a nonsense value passed through it. An enumeration is a genuinely new type, so it can.

Where the two keywords part company:

Question enum (unscoped) enum class (scoped)
Where do the enumerator names land? in the scope enclosing the definition, alongside the type only inside the enumeration's own scope
Must you qualify an enumerator? no, though WallAngle::slab also works yes, always HoldShape::crimp
Does it convert to an integral type on its own? yes, implicitly no, never
How do you get the number out? it is already there static_cast to the underlying type
Risk of clashing with a nearby name real, since the names are out in the open none, the type name walls them off
Distinct type? yes yes

The name is the whole story: an unscoped enumeration does not create a scope region for its enumerators, so they end up next to the type. It does still supply a named scope region you may use for qualification, which is why WallAngle::slab compiles even though the bare slab does too.

#include <iostream>

enum WallAngle // unscoped: the enumerator names land out here, beside the type
{
    slab,
    vertical,
    overhang,
    roof,
};

enum class HoldShape // scoped: the enumerator names stay inside HoldShape
{
    crimp,
    jug,
    sloper,
    pinch,
};

int main()
{
    WallAngle angle{ overhang };         // no qualification needed
    HoldShape shape{ HoldShape::pinch }; // qualification is mandatory

    std::cout << "angle as a number: " << angle << '\n';
    std::cout << "shape as a number: " << static_cast<int>(shape) << '\n';

    return 0;
}

Output:

angle as a number: 2
shape as a number: 3

Notice the asymmetry in those two print statements. angle reached std::cout on its own because an unscoped enumeration converts to its integral value without being asked. shape needed a cast, because a scoped enumeration refuses that conversion. The refusal is the feature: it is also what stops a HoldShape from being quietly compared against, added to, or assigned from a plain number.

Best Practice
Reach for enum class first. Drop to an unscoped enum only when you have a specific reason to want the implicit conversion, such as an enumeration whose values index into an array.

Giving an Enumeration a Readable Face

Because an enumerator is a name attached to a number, printing one prints the number, which is useless on a route board. Two chapter techniques fix that together. A lookup function turns the enumerator into text, and an overloaded operator<< wires that function into the stream so callers never have to invoke it by hand.

#include <iostream>
#include <string_view>

enum class HoldShape
{
    crimp,
    jug,
    sloper,
    pinch,
};

constexpr std::string_view describe(HoldShape shape)
{
    switch (shape)
    {
    case HoldShape::crimp:  return "crimp";
    case HoldShape::jug:    return "jug";
    case HoldShape::sloper: return "sloper";
    case HoldShape::pinch:  return "pinch";
    }

    return "off-list";
}

std::ostream& operator<<(std::ostream& out, HoldShape shape)
{
    return out << describe(shape);
}

int main()
{
    std::cout << "start hold: " << HoldShape::jug << '\n';
    std::cout << "crux hold: " << HoldShape::sloper << '\n';

    return 0;
}

Output:

start hold: jug
crux hold: sloper

Three details in that overload are worth carrying forward. The first parameter is std::ostream&, not std::cout, so the same function serves any stream. The return type is that same reference, which is what makes << chain. And the function is an ordinary free function, not a member, because the left operand of << is the stream rather than your type. The matching input operator, operator>>, takes std::istream& and a non-const reference to the object it fills.

Structs: Where a Member's Value Comes From

A struct (short for structure) is a program-defined type that bundles several variables into one. Those variables are its data members (or member variables).

A struct that holds nothing but data members is an aggregate. So is an array. That word matters because aggregates get their own initialization form, aggregate initialization: you supply an initializer list, a brace-enclosed run of comma-separated values, and the compiler performs memberwise initialization, walking the members in declaration order and matching them to your values one for one. A struct stops being an aggregate the moment it gains a user-declared constructor, a private or protected data member, or a virtual function, and then this whole form stops applying to it.

Four different mechanisms can put a value into a member, and they cooperate rather than compete:

Mechanism What it looks like What decides the value
Positional aggregate initialization Route r{ "Wren", 14, 5 } position in the list, matched against declaration order
Designated initializer (C++20) Route r{ .setter = "Otis" } the member you named
Default member initializer int moves{ 8 }; inside the definition the value written in the type definition
Value initialization Route r{} the default member initializer, or zero-like if there is none

A default member initializer is a value written beside a member in the type definition itself. The practice of writing them is called non-static member initialization, and their effect is to catch every member you leave out of an initializer list.

#include <iostream>
#include <string>

struct Route
{
    std::string setter{};  // no default value, so an empty string
    int moves{ 8 };        // default member initializer
    int stars{ 3 };        // default member initializer
};

void printRoute(const Route& route)
{
    std::cout << route.setter << " | " << route.moves << " moves | " << route.stars << " stars" << '\n';
}

int main()
{
    Route positional{ "Wren", 14, 5 };            // memberwise, in declaration order
    Route partial{ "Otis" };                      // the rest fall back to their defaults
    Route designated{ .setter = "Marisol", .stars = 4 }; // C++20, moves keeps its default
    Route blank{};                                // every member takes its default

    printRoute(positional);
    printRoute(partial);
    printRoute(designated);
    printRoute(blank);

    return 0;
}

Output:

Wren | 14 moves | 5 stars
Otis | 8 moves | 3 stars
Marisol | 8 moves | 4 stars
 | 8 moves | 3 stars

Designated initializers buy readability at a call site full of bare numbers, but they do not buy freedom of order. The designators must still run in declaration order. The following program is broken on purpose to show what happens when they do not:

#include <iostream>
#include <string>

struct Route
{
    std::string setter{};
    int moves{ 8 };
    int stars{ 3 };
};

int main()
{
    Route jumbled{ .stars = 4, .setter = "Marisol" }; // designators run backwards

    std::cout << jumbled.setter << '\n';

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:13:52: error: designator order for field 'Route::setter' does not match declaration order in 'Route'
   13 |     Route jumbled{ .stars = 4, .setter = "Marisol" }; // designators run backwards
      |                                                    ^
Warning
Reordering the members in a struct definition silently rewrites the meaning of every positional initializer list already written for it, and turns every designated initializer list into the error above. The error is the lucky case, because the compiler tells you. Prefer adding new members at the end.

Reaching a Member Through Whatever You Are Holding

Selecting a member takes one of two operators, and which one you need depends purely on what you are holding.

You are holding Operator Written as
an object member selection operator, operator. topOut.setter
a reference to an object member selection operator, operator. sameRoute.setter
a pointer to an object member selection from pointer operator, operator-> boardEntry->setter
#include <iostream>
#include <string>

struct Route
{
    std::string setter{};
    int moves{};
};

int main()
{
    Route topOut{ "Wren", 14 };
    Route& sameRoute{ topOut };   // a reference to it
    Route* boardEntry{ &topOut }; // a pointer to it

    std::cout << topOut.setter << '\n';       // object: dot
    std::cout << sameRoute.moves << '\n';     // reference: still dot
    std::cout << boardEntry->setter << '\n';  // pointer: arrow
    std::cout << (*boardEntry).moves << '\n'; // what the arrow stands for

    return 0;
}

Output:

Wren
14
Wren
14

A reference behaves as the object itself, so it takes the dot. A pointer does not, so boardEntry.setter would not compile. The arrow is exactly the shorthand shown on the last line, dereference then select, and it is worth preferring over the parenthesised form because the parentheses are load-bearing. Drop them and *boardEntry.setter parses as *(boardEntry.setter), which asks a pointer for a member it does not have.

Crossing a Function Boundary

Structs are cheap to pass in the sense that one argument replaces a handful, but the copy is not free, so the chapter's rule of thumb splits by what the type costs to copy.

Parameter type Pass it Reason
int, char, bool, double by value a copy is one register
an enumeration object by value it holds an integral value, so copying costs the same
std::string_view by value it owns nothing and is cheap to copy
std::string by const reference copying it allocates
any struct by const reference copying every member adds up, and members get added later

The last row deserves the emphasis. A struct with one int in it today would be fine by value today, but passing it by const reference costs almost nothing and spares you from revisiting every call site the week a second member appears.

Returning is the other direction and follows the opposite habit: return a struct by value. Returning a reference or pointer to a local object hands the caller a dangling one, and the compiler is very good at making the by-value return cheap.

Size, Padding, and Layout

The size of a struct is not reliably the sum of the sizes of its members. Processors read memory fastest when a value sits on an address that is a multiple of its own size, so for performance reasons the compiler inserts gaps between members to keep them aligned. Those gaps are padding.

Padding is a consequence of declaration order, which means the same four members can produce two different sizes:

#include <iostream>

struct Interleaved
{
    char panel{};
    int moves{};
    char grade{};
    int holds{};
};

struct Grouped
{
    int moves{};
    int holds{};
    char panel{};
    char grade{};
};

int main()
{
    std::cout << "sum of the member sizes: " << 2 * sizeof(int) + 2 * sizeof(char) << '\n';
    std::cout << "sizeof(Interleaved): " << sizeof(Interleaved) << '\n';
    std::cout << "sizeof(Grouped): " << sizeof(Grouped) << '\n';

    return 0;
}

Output:

sum of the member sizes: 10
sizeof(Interleaved): 16
sizeof(Grouped): 12

Ten bytes of members, and neither struct is ten bytes. Interleaved pays three bytes of padding after each char so that the int following it starts on a four-byte boundary. Grouped puts both int members first and lets the two char members share the tail, so it wastes two bytes instead of six. Exact numbers vary by platform, so measure with sizeof rather than counting members.

Perspective
Declaring members largest first is a reasonable default, but it is a micro-optimisation. Group members by what they mean while you are writing ordinary code, and only reorder for size when a profiler or a very large array of the struct says it matters.

One Definition, Many Types

A class template is not a type. It is a definition the compiler uses to manufacture types on demand: write it once with the varying type left as a parameter, and the compiler instantiates a separate class type for each set of arguments you actually use.

Three features work together here:

Feature Since What it does for you
Class template C++98 one definition generates a class type per set of template arguments
Class template argument deduction (CTAD) C++17 the compiler deduces the template arguments from the initializer, so you can omit them
Alias template C++11 names a class template with some arguments already filled in
#include <iostream>
#include <string_view>

template <typename TScore>
struct GradeBand
{
    TScore easiest{};
    TScore hardest{};
};

template <typename TLabel, typename TScore>
struct Placard
{
    TLabel heading{};
    GradeBand<TScore> band{};
};

// an alias template: Placard with the label type already pinned down
template <typename TScore>
using WallPlacard = Placard<std::string_view, TScore>;

template <typename TScore>
void printBand(const GradeBand<TScore>& band)
{
    std::cout << band.easiest << " up to " << band.hardest << '\n';
}

int main()
{
    GradeBand<int> spelledOut{ 3, 7 }; // template argument written by hand
    GradeBand deduced{ 4, 9 };         // CTAD deduces GradeBand<int>
    GradeBand fractional{ 5.5, 8.25 }; // CTAD deduces GradeBand<double>

    printBand(spelledOut);
    printBand(deduced);
    printBand(fractional);

    WallPlacard<int> board{ "roof circuit", { 6, 9 } };
    std::cout << board.heading << ": ";
    printBand(board.band);

    return 0;
}

Output:

3 up to 7
4 up to 9
5.5 up to 8.25
roof circuit: 6 up to 9

GradeBand<int> and GradeBand<double> are two unrelated types that happen to share a definition. CTAD read { 4, 9 } and concluded int, and read { 5.5, 8.25 } and concluded double, which is the same service the standard library gives you when std::pair p{ 1, 2.5 } compiles without angle brackets. WallPlacard is not a new template but a shorter name for an old one with std::string_view already supplied.

Warning
CTAD deduces from an initializer, so it needs one. GradeBand empty{}; and GradeBand empty; give the compiler nothing to work from and will not compile. Name the argument explicitly when there is no initializer to read.

Summary

The chapter is one idea applied twice: the language lets you add types to it, and the two shapes those types come in are a closed list of names and a bundle of related values.

Need Tool Key detail
a value from a short fixed set enum class no implicit conversion to a number, enumerators are scoped inside the type
the same, but you want the number enum converts implicitly, enumerator names sit in the enclosing scope
readable output for either a lookup function plus operator<< free function, takes and returns std::ostream&
several facts as one object struct data members initialize memberwise, in declaration order
sensible values when you omit some default member initializers written in the type definition, used by anything you leave out
clarity at a busy call site designated initializers C++20, and they must follow declaration order
a member, given a pointer operator-> given an object or reference, use operator. instead
a struct parameter const reference return by value, never by reference to a local
the same structure over many types a class template CTAD infers the arguments, an alias template shortens the name

The one number never to assume is sizeof. Padding makes a struct at least as large as the sum of its members and often larger, and reordering the members changes the answer.

Key Terminology

  • Program-defined type: a type you write yourself rather than one built into the language
  • User-defined type: another name for a program-defined type
  • Type definition: the definition of a program-defined type, exempt from the one-definition rule
  • Enumeration (enumerated type, enum): a compound type whose every value is a named symbolic constant
  • Enumerator: one named value belonging to an enumeration
  • Distinct type: a type the compiler can tell apart from all others, which a type alias is not
  • Unscoped enumeration: an enum whose enumerators land in the enclosing scope and convert to integers implicitly
  • Scoped enumeration: an enum class whose enumerators stay inside the type and refuse implicit conversion
  • Named scope region: the scope an unscoped enumeration supplies for optional qualification of its enumerators
  • Struct (structure): a program-defined type bundling several variables into one
  • Data member (member variable): one of the variables that makes up a struct or class
  • Member selection operator (.): selects a member of an object or of a reference to one
  • Member selection from pointer operator (->): dereferences a pointer and selects a member in one step
  • Aggregate: a type that can hold several data members, which in C++ means arrays and structs with data members only
  • Aggregate initialization: initializing an aggregate directly from a braced list of values
  • Initializer list: the comma-separated list of values supplied in that form
  • Memberwise initialization: matching those values to members one at a time, in declaration order
  • Designated initializer: a C++20 initializer that names the member it applies to, and must follow declaration order
  • Non-static member initialization: writing default values for members in the type definition
  • Default member initializer: the value so written, used whenever that member is not explicitly initialized
  • Padding: gaps the compiler inserts between members to keep them aligned, making a struct larger than its members total
  • Class template: a definition from which the compiler instantiates a class type per set of template arguments
  • Class template argument deduction (CTAD): the C++17 feature that deduces those arguments from an initializer
  • Alias template: a templated name for a class template with some arguments already supplied

Looking Forward

Every struct you wrote in this chapter was a passive container: it held data, and the functions that operated on it lived outside it and took it as a parameter. The next step is to move those functions inside the type, so an object can act on its own data instead of waiting to be handed to something that will.

That step turns a struct into a class, and it brings a set of questions a plain aggregate never had to answer. Which members may outsiders touch, and which are internal bookkeeping? How does an object guarantee it is in a valid state from the moment it is created, rather than trusting whoever writes the initializer list? What happens when one is copied, or destroyed? Access specifiers, constructors, and destructors are the answers, and they build directly on the aggregate, initialization, and member-selection rules you have just reviewed. Nothing here gets replaced. It gets extended.