Structs and classes
Grouping data, and deciding what the outside world may touch.
By the end of this chapter you can
- Define a class with a clear public interface
- Explain the only difference between struct and class
- Choose what belongs in the interface and what does not
Everything so far has used types the language gave you. This part is about
making your own — types that behave as well as int does: they can be copied,
compared, passed around, and cleaned up, and they cannot easily be misused.
The starting point is grouping related data under one name.
An aggregate
#include <iostream>
#include <string>
struct Point {
double x;
double y;
};
int main() {
Point origin{}; // both members zero-initialised
Point p{3.0, 4.0}; // aggregate initialisation, in declaration order
Point named{.x = 1.0, .y = 2.0}; // designated initialisers (C++20)
std::cout << "p = (" << p.x << ", " << p.y << ")\n";
std::cout << "origin = (" << origin.x << ", " << origin.y << ")\n";
std::cout << "named.x = " << named.x << '\n';
p.x = 10.0; // members are public: anyone may write them
std::cout << "after p.x = 10: " << p.x << '\n';
}Point is an aggregate: no private members, no user-declared constructors, so
you can initialise it with braces in declaration order. Designated initialisers
(.x = 1.0) arrived in C++20 and are worth using — they survive someone
reordering the members, and they document what each value means at the call
site.
You can give members default values, which fixes that trap at the source:
#include <iostream>
struct Settings {
int retries = 3;
bool verbose = false;
double timeout = 1.5;
};
int main() {
Settings a; // uses every default
Settings b{5}; // retries = 5, the rest defaulted
Settings c{.verbose = true}; // only verbose overridden
std::cout << a.retries << ' ' << b.retries << ' ' << c.verbose << '\n';
std::cout << "c.timeout still " << c.timeout << '\n';
}The only difference between struct and class
struct members are public by default. class members are private by default.
That is the entire difference — everything else is identical.
class Hidden {
int value = 1; // private, because this is a class
};
int main() {
Hidden h;
return h.value; // error: 'int Hidden::value' is private
}The convention almost everyone follows: use struct when the type is a plain
bundle of data with no invariant to maintain, and class when it has an
interface to protect. The compiler does not care; readers do.
Why hide anything?
Because some combinations of member values are nonsense, and a type that can be put into a nonsensical state will eventually be put into one.
#include <iostream>
struct Fraction {
int numerator;
int denominator;
double value() const { return static_cast<double>(numerator) / denominator; }
};
int main() {
Fraction half{1, 2};
std::cout << "1/2 = " << half.value() << '\n';
half.denominator = 0; // nothing stops this
std::cout << "1/0 = " << half.value() << '\n';
}Dividing by zero in floating point gives inf rather than crashing, so the
program carries on with a nonsense value and fails somewhere far from the cause.
Make the invariant the type’s responsibility:
#include <iostream>
#include <numeric>
#include <stdexcept>
class Fraction {
public:
Fraction(int numerator, int denominator)
: numerator_(numerator), denominator_(denominator) {
if (denominator_ == 0) {
throw std::invalid_argument("a fraction cannot have a zero denominator");
}
normalise();
}
int numerator() const { return numerator_; }
int denominator() const { return denominator_; }
double value() const { return static_cast<double>(numerator_) / denominator_; }
private:
void normalise() {
if (denominator_ < 0) { // keep the sign on the numerator
numerator_ = -numerator_;
denominator_ = -denominator_;
}
const int divisor = std::gcd(numerator_, denominator_);
if (divisor > 1) {
numerator_ /= divisor;
denominator_ /= divisor;
}
}
int numerator_;
int denominator_;
};
int main() {
Fraction half{2, 4};
std::cout << half.numerator() << '/' << half.denominator()
<< " = " << half.value() << '\n';
Fraction negative{1, -2};
std::cout << negative.numerator() << '/' << negative.denominator() << '\n';
try {
Fraction bad{1, 0};
} catch (const std::invalid_argument& e) {
std::cout << "rejected: " << e.what() << '\n';
}
}Now there is no way to obtain a Fraction with a zero denominator, and every
Fraction is in lowest terms with the sign on the numerator. Those are the
class’s invariants: statements true of every instance, from construction
until destruction.
That is what private is for. Not secrecy — there is nothing secret about
denominator_ — but the guarantee that the only code able to break the
invariant is the small, reviewable amount inside the class.
Constructors
A constructor establishes the invariant. The member initialiser list — the part after the colon — initialises members directly, rather than default-constructing them and then assigning:
#include <iostream>
#include <string>
struct Loud {
Loud() { std::cout << " default-constructed\n"; }
Loud(const std::string&) { std::cout << " constructed from a string\n"; }
Loud& operator=(const std::string&) { std::cout << " assigned\n"; return *this; }
};
class Bad {
public:
Bad(const std::string& text) { member_ = text; } // constructs, then assigns
private:
Loud member_;
};
class Good {
public:
Good(const std::string& text) : member_(text) {} // constructs once
private:
Loud member_;
};
int main() {
std::cout << "Bad:\n"; Bad b{"x"};
std::cout << "Good:\n"; Good g{"x"};
}Bad does twice the work. For a Loud that is a wasted line of output; for a
std::string or a std::vector it is a wasted allocation. And some members —
references, const members, and types with no default constructor — cannot be
assigned after the fact, so the initialiser list is the only option.
explicit
A single-argument constructor defines an implicit conversion unless you say otherwise:
#include <iostream>
class Meters {
public:
Meters(double value) : value_(value) {} // not explicit
double value() const { return value_; }
private:
double value_;
};
void report(Meters distance) {
std::cout << "distance: " << distance.value() << " m\n";
}
int main() {
report(Meters{5.0});
report(5.0); // compiles — 5.0 is silently converted
report(true); // compiles — bool to double to Meters!
}report(true) is almost certainly a bug, and the language accepted it without a
word. Adding explicit requires callers to name the type:
class Meters {
public:
explicit Meters(double value) : value_(value) {}
double value() const { return value_; }
private:
double value_;
};
void report(Meters) {}
int main() {
report(Meters{5.0}); // fine
report(5.0); // error: no implicit conversion
}Make single-argument constructors explicit by default. Leave it off only when
the conversion is genuinely something you want at every call site — as with
std::string from a string literal.
What belongs in the interface
A useful test: would a caller notice if this changed? If not, it should be private.
- Data members are almost always private, because their names, types, and representation are implementation choices you will want to change.
- A getter is not automatically good design.
balance()on an account is meaningful; a getter and setter for every member is a struct with extra typing, and it gives you the maintenance cost of a class with none of the protection. - Prefer members that do something over members that expose state.
account.deposit(50)maintains the invariant;account.set_balance(x)hands it to the caller.