Chapter 1 gave you the procedural floor — types, functions, and the machine mapping. But built-in types (int, double, pointers) are deliberately low-level: they reflect the hardware, not the problem. Real programs need higher-level abstractions — types that name the concepts of the domain and carry their own invariants. This chapter introduces the four mechanisms C++ provides for that: structs (data grouping), classes (data + operations with access control), enumerations (named constant sets), and unions (shared-memory packing). Every lesson in this course builds on the last: here you learn the vocabulary (“class”, “constructor”, “access specifier”) you will use throughout.
Why user-defined types matter now: the standard library
(vector, string, map) is built from exactly these
mechanisms. Understanding how struct and class work at the
language level makes the library's design decisions legible instead of magical.
C++'s built-in types (int, double, pointers) and their operations
are rich but deliberately low-level. They directly and efficiently reflect the capabilities of
conventional computer hardware. They do not provide high-level facilities to write
advanced applications conveniently.
C++ augments the built-in types with abstraction mechanisms — facilities for building user-defined types. These are the backbone of the standard library and the primary subject of this course. A user-defined type is often preferred over a built-in type because it is easier to use, less error-prone, and typically as efficient as direct use of the underlying built-in representation.
Two names for user-defined types in C++: class and
enumeration. A struct is a class with its members public by
default; an enum is a type whose values are a finite set of named constants. The
rest of this chapter walks through all four.
built-in types → low-level, hardware-nativeabstraction mechanisms → user-defined types → struct, class, enum, union
The first step in building a new type is organizing the elements it needs into a data
structure. The C++ keyword for that is struct.
Consider a simple vector-of-doubles with a pointer and a size:
struct Vector {
double* elem; // pointer to elements
int sz; // number of elements
};
A variable of type Vector is declared like any other type:
Vector v;
But that is not useful yet. v.elem is an uninitialized pointer
— it doesn't point anywhere. To make the vector functional we must give it memory to hold
its elements.
void vector_init(Vector& v, int s)
// initialize a Vector
{
v.elem = new double[s]; // allocate s doubles on the free store
v.sz = s;
}
The & in Vector& means we pass v by non-const
reference — vector_init() can modify the caller's vector. The
new operator allocates memory from an area called the
free store (also known as dynamic memory or the heap). Objects allocated on
the free store live until they are explicitly destroyed with delete.
double read_and_sum(int s) // read s doubles from cin and return their sum
{
Vector v;
vector_init(v, s); // allocate s elements for v
for (int i = 0; i != s; ++i)
std::cin >> v.elem[i]; // read into elements
double sum = 0;
for (int i = 0; i != s; ++i)
sum += v.elem[i]; // compute the sum of the elements
return sum;
}
Access to struct members through a name (or a reference) uses the dot operator:
.. Access through a pointer uses the arrow operator: ->.
void f(Vector v, Vector& rv, Vector* pv)
{
int i1 = v.sz; // access through name
int i2 = rv.sz; // access through reference
int i3 = pv->sz; // access through pointer
}
The free-store pattern. The struct itself is a fixed-size
“handle” — a pointer (to the heap) plus a size. The data it refers to can grow
or shrink; the handle stays the same size. This is the foundational technique for handling
variable amounts of data in C++. The standard library's vector works exactly
this way (Ch. 12).
Keeping data separate from the operations on it has an obvious advantage: you can use the data in arbitrary ways. But it also means users can see and corrupt the representation directly, bypassing every invariant the designer intended. A tighter connection between representation and operations is needed for a type to behave like a “real” type.
The language mechanism is the class. A class has a set of members — data, functions, or type definitions — grouped under access specifiers. The interface is the set of public members; users interact with the class only through it. The implementation (private members) is invisible and inaccessible; we can change it later without breaking user code.
class Vector {
public:
Vector(int s) : elem{new double[s]}, sz{s} { }
// construct a Vector of s elements
double& operator[](int i) { return elem[i]; }
// element access: subscripting
int size() { return sz; }
// number of elements
private:
double* elem; // pointer to the elements
int sz; // the number of elements
};
What changed from the struct version.
Vector(int s) guarantees every Vector is initialized: no uninitialized
pointers, no missing sz. The colon after the parameter list is the
member initializer list:
elem{new double[s]}, sz{s} initializes elem first, then
sz.
public members form the interface;
private members (representation: elem, sz) are
accessible only through that interface. Users can't mess with the pointer directly.
operator[] — returns a double& (a
reference), allowing both read and write through the subscript syntax:
v[i] = 3.14;.
Vector v(6); // a Vector with 6 elements
v[0] = 1.0; // write through subscript
double x = v[3]; // read through subscript
for (int i = 0; i != v.size(); ++i)
std::cin >> v[i]; // cleaner than v.elem[i]
The read_and_sum() example from §2.2 simplifies: users no longer need to know
that the representation is a pointer to heap memory and a size. They just call
v.size() and v[i]. The representation is entirely encapsulated.
The constructor
Vector(int s) : elem{new double[s]}, sz{s} { } says:
s doubles on the free store and make
elem point at that memory.
sz to s.
The member initializer list (: elem{...}, sz{...}) is the preferred way to
initialize members, especially for const members, references, and members of classes without
default constructors. It is more efficient than assigning inside the constructor body because
member initialization happens once (in-place) rather than twice (default-construct then
assign).
A struct is simply a class with its members public by default.
You can define constructors and other member functions for structs just as you would for
classes. The choice between struct and class is a convention — use
class when you want encapsulation; use struct for simple data
aggregates with public access.
Enumerations are user-defined types whose values are a finite, named set. They make code more readable and less error-prone than raw integer constants.
enum class (C++11)enum class Color { red, blue, green };
enum class Traffic_light { green, yellow, red };
Color col = Color::red;
Traffic_light light = Traffic_light::red;
Scoping matters: Color::red and Traffic_light::red
are distinct values that happen to share the name. They cannot be mixed or compared
accidentally.
Color x1 = red; // error: which red?
Color y2 = Traffic_light::red; // error: Traffic_light::red is not a Color
Color z3 = Color::red; // OK
auto x4 = Color::red; // OK: Color::red is a Color
Enum class values also do not implicitly convert to integers — preventing a whole class of accidental integer comparisons:
int i = Color::red; // error: Color::red is not an int!
Color c = 2; // error: 2 is not a Color
Color x = Color{5}; // OK but verbose — explicit underlying value
Color y {6}; // also OK
int x = int(Color::red); // explicit conversion to underlying type (int)
Custom operations on enum class: define a member or free function to make the type practically usable:
Traffic_light& operator++(Traffic_light& t) // prefix increment: ++
{
switch (t) {
case Traffic_light::green: return t = Traffic_light::yellow;
case Traffic_light::yellow: return t = Traffic_light::red;
case Traffic_light::red: return t = Traffic_light::green;
}
}
auto signal = Traffic_light::red;
Traffic_light next = ++signal; // next becomes Traffic_light::green
If qualifying every enumerator is verbose, use a using enum declaration inside a
limited scope:
Traffic_light& operator++(Traffic_light& t)
{
using enum Traffic_light; // bring enumerators into scope here
switch (t) {
case green: return t = yellow;
case yellow: return t = red;
case red: return t = green;
}
}
C++ inherits plain enum from C and keeps it for compatibility. The enumerators
are in the enclosing scope and implicitly convert to int:
enum Color { red, green, blue };
int col = green; // OK: green is 1
Plain enumerations have been in C++ since C++98 and are common in existing code. Prefer
enum class in new code — the type safety prevents bugs that plain enums make too
easy.
A union is a struct in which all members are allocated at the same memory address, so the union occupies only as much space as its largest member. At any given time a union can hold a value for only one member.
Motivating example: a symbol-table entry that holds a name and a value, where
the value is either a pointer (Node*) or an integer:
// Tagged union approach (manual tagging)
enum class Type { ptr, num };
struct Entry {
std::string name;
Type t; // type tag
Node* p; // use if t == Type::ptr
int i; // use if t == Type::num
};
void f(Entry* pe) {
if (pe->t == Type::num)
std::cout << pe->i;
}
p and i are never used simultaneously, so memory is wasted unless we
pack them into a union:
union Value { Node* p; int i; };
// Value::p and Value::i share the same memory address
The language does not track which member is active — the programmer must do that by pairing the union with a type tag. This tagged union pattern is common and useful, but the correspondence between the tag and the union member it describes is error-prone to maintain by hand.
std::variant
Standard library variant (§15.4.1) eliminates most direct uses of unions. It
stores a value of one of a set of alternative types, with the active type tracked
automatically:
#include <variant>
struct Entry {
std::string name;
std::variant<Node*, int> v; // v holds either a Node* or an int
};
void f(Entry* pe) {
if (std::holds_alternative<int>(pe->v))
std::cout << std::get<int>(pe->v);
}
For many uses variant is simpler and safer than a naked union. Use it unless you
have a specific reason to manage the raw union manually.
The takeaway: naked unions are low-level space optimization. Prefer
enum class for type-safe sets of constants, and variant over
manual tagged unions. The standard library already has the safe versions; building them
yourself is error-prone and unnecessary unless you need the absolute control raw memory
gives.
| Mechanism | When to use | Type safety? | Overhead |
|---|---|---|---|
struct |
Simple data aggregates; public access by convention | No stronger than its members | None (zero-cost abstraction) |
class |
Encapsulated types with invariants; private + public separation | Enforced by compiler (access control) | None |
enum class |
Named constants that form a closed set; need scoping and type safety | Strong (no implicit int conversion) | Typically int-sized; zero overhead |
plain enum |
Existing C code; interoperating with APIs that use unscoped enums | Weak (implicit int conversion, enumerators leak) | None |
| union | Memory optimization when only one member is active; low-level systems code | None (programmer must track active member manually) | Saves sizeof(largest member) × count |
std::variant |
Tagged union with automatic type tracking; preferred over raw union | Type-safe (visit/handles_alternative/check) | Size of largest alternative + discriminator |
Stroustrup closes the chapter with a subset of the C++ Core Guidelines. All 10 items, with the section where each is introduced:
| # | Guideline | § |
|---|---|---|
| 1 | Prefer well-defined user-defined types over built-in types when the built-in types are too low-level | 2.1 |
| 2 |
Organize related data into structures (structs or classes)
|
2.2 |
| 3 |
Represent the distinction between an interface and an implementation using a
class
|
2.3 |
| 4 |
A struct is simply a class with its members public by default
|
2.3 |
| 5 | Define constructors to guarantee and simplify initialization of classes | 2.3 |
| 6 | Use enumerations to represent sets of named constants | 2.4 |
| 7 | Prefer enum class over plain enums to minimize surprises |
2.4 |
| 8 | Define operations on enumerations for safe and simple use | 2.4 |
| 9 | Avoid naked unions; wrap them in a class together with a type field | 2.5 |
| 10 | Prefer std::variant over naked unions |
2.5 |
Which statement about struct and class in C++ is true?
Given enum class Color { red, green, blue }; which code compiles?
Memory is wasted in a union when:
What does the colon after a constructor parameter list do?
Why is std::variant typically preferred over a raw union?
a) built-in ⇒ types in the language core (eg char, int, double)
b) user-defined ⇒ struct, class, enum ; no overhead vs built-ins
struct ⇒ class with public members by default ; data grouping
class ⇒ private by default ; separates interface from implementation
. member ⇒ direct access (eg v.sz) ;
-> member ⇒ access via pointer (eg p->sz)
constructor ⇒ same name as class ; no return type ; guarantees initialization
member initializer list ⇒ :sz{s}, elem{new double[s]} ; runs before
body
operator[] ⇒ index access ; returns double&
→ writable (eg v[i] = x)
enum class ⇒ scoped enumerators ; no implicit int → type-safe
plain enum ⇒ unscoped ; enumerators leak ; implicit int conversion
using enum ⇒ brings enumerators into scope (eg
using enum Color; red)
operator++ ⇒ advance enumerator (eg
Traffic_light green → yellow → red)
union ⇒ one active member at a time ; programmer tracks it
tag field ⇒ records which member is active
naked union × → wrap in class with type field
std::variant ⇒ type-safe tagged union ; tracks active member automatically
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 2: "User-Defined Types." Addison-Wesley.
Reference:
Chapter 1–2 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
classes,
enumerations, and
variant.
Questions? Ask your agent — your teacher — about anything unclear: a quiz answer, a code example, or how the struct-to-class progression maps to the abstractions you use in your own code. Follow-ups are expected, not optional.