Appearance
C++ Classes, Invariants, and Special Member Functions
A useful C++ class makes invalid states difficult to create and gives resource ownership ordinary value behavior. Syntax is secondary to the operations the type promises.
Construction should establish an invariant
cpp
#include <stdexcept>
#include <string>
class Percentage {
public:
explicit Percentage(int value) : value_{value} {
if (value < 0 || value > 100) {
throw std::out_of_range{"percentage"};
}
}
int value() const noexcept { return value_; }
private:
int value_;
};
int main() {
Percentage complete{75};
return complete.value() == 75 ? 0 : 1;
}Member initialization happens before the constructor body and follows member declaration order, not initializer-list order. explicit prevents unintended single-argument conversions.
The compiler can generate special members
C++ recognizes default construction, destruction, copy construction/assignment, and move construction/assignment. For a class composed of well-behaved values, the rule of zero is ideal: declare none and let members manage themselves.
If a class directly owns a raw resource, it may need the rule of five. Often the better design is to place that resource in a dedicated RAII member such as unique_ptr, vector, or a small handle wrapper, returning the outer class to the rule of zero.
The older rule of three says that a class needing a custom destructor, copy constructor, or copy assignment operator probably needs all three. C++11 added move construction and move assignment, producing the rule of five. This history matters in maintained code because declaring a destructor or copy operation can prevent the compiler from implicitly generating move operations. Check all five operations when adding any one of them, or move the resource into a member that restores the rule of zero.
Use = default to request normal behavior explicitly and = delete to reject an operation:
cpp
class Socket {
public:
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
Socket(Socket&&) noexcept = default;
Socket& operator=(Socket&&) noexcept = default;
};Operators should preserve the type's meaning
Operator overloading lets a class participate in ordinary expression syntax. It cannot introduce a new operator, change precedence, or change the number of operands. Use it when the operation has the meaning readers already expect from the type.
cpp
#include <compare>
class Version {
public:
Version(int major, int minor) : major_{major}, minor_{minor} {}
bool operator==(const Version&) const = default;
auto operator<=>(const Version&) const = default;
private:
int major_;
int minor_;
};C++20's defaulted operator== and three-way comparison can derive memberwise equality and ordering. Memberwise order is appropriate only when it matches the domain. A semantic version with prerelease labels, for example, needs rules beyond comparing stored fields blindly.
Binary operators that treat both operands symmetrically are often non-member functions. Compound assignment commonly performs the mutation, while the non-mutating operator works on a copy:
cpp
Counter& operator+=(Counter& left, int amount);
Counter operator+(Counter left, int amount) {
left += amount;
return left;
}Avoid surprising overloads such as using operator+ for database insertion. Named functions are clearer when the domain does not already supply an operator meaning. Conversion operators and single-argument constructors should usually be explicit unless implicit conversion is safe, cheap, and unsurprising.
Destruction is deterministic
Automatic objects are destroyed when their scopes end, in reverse construction order. Member and base destruction also follows defined order. This occurs on normal return and during exception unwinding, making destructors the foundation of cleanup.
Destructors should not emit exceptions. Resource-release failures that matter need an explicit operation before destruction or another reporting channel.
Inheritance models substitutability, not code reuse alone
A polymorphic base needs a virtual destructor when deletion may occur through a base pointer:
cpp
class Renderer {
public:
virtual ~Renderer() = default;
virtual void render(const Report&) = 0;
};Use override on derived overrides so signature mistakes are diagnosed. Prefer composition when a type merely uses another service rather than satisfying its behavioral contract.
Passing a derived object by base value slices away the derived portion. Polymorphic APIs therefore use references or pointers with an explicit ownership model.
The one-definition rule spans translation units
Classes are normally defined in headers so every translation unit sees the same complete definition. Non-inline member functions can be defined in a .cpp file. Inline functions and templates may have equivalent definitions in multiple translation units under the one-definition rule.
Violations can be diagnosed at link time or remain subtle when definitions differ due to macros or generated configuration. Keep public headers deterministic and minimize conditional layout changes.
Optional prompts
Explain: Why is the rule of zero safer than manually implementing five operations?
Answer: Resource-managing members already implement correct copy/move/destruction policy. Composition lets generated outer operations preserve that policy without duplicated ownership code.
Debug: Deleting a derived object through a base pointer leaks derived resources. What declaration is missing?
Answer: The polymorphic base needs a virtual destructor so destruction dispatches through the complete object type.
Explain: Why can adding a destructor make a previously movable class copy instead?
Answer: A user-declared destructor suppresses implicit move generation. Existing copy operations may then be selected. Review or explicitly default/delete all special members, preferably by moving cleanup into an RAII member.