A C++ class defines representation, invariants, and the operations that establish and preserve them. Constructors establish an object’s invariant, member functions preserve it, and the destructor releases resources owned by that object. Design the ownership policy first, because it determines whether compiler-generated special members are correct.
Construction and Destruction #
A constructor has the class name and no return type. A default constructor has no parameters or has defaults for all its parameters. Copy and move constructors initialize a new object from an lvalue or rvalue of the same class; copy and move assignment update an existing object.
#include <string>
#include <utility>
class Connection {
public:
explicit Connection(std::string endpoint) : endpoint_(std::move(endpoint)) {}
private:
std::string endpoint_;
};Members do not initialize in the textual order of a constructor’s initializer list. Construction occurs in this order:
- Virtual bases, when constructing the most-derived object.
- Direct bases, in base-specifier order.
- Non-static data members, in declaration order.
- The constructor body.
Thus, write initializers in declaration order and do not initialize one member from another declared later:
struct Valid {
int first;
int second;
Valid(int value) : first(value), second(first + 1) {}
};Destruction reverses the completed construction order. Let automatic storage and RAII owners perform normal destruction; explicit destructor calls are for controlled storage management where lifetime and later storage reuse are both established.
Special Member Functions and the Rule of Zero #
The special member functions are the default constructor, copy constructor, move constructor, copy-assignment operator, move-assignment operator, and destructor. An implementation implicitly declares operations that the class has not user-declared, then defines them when needed. A declaration can be implicitly declared, explicitly defaulted, or explicitly deleted; a function with a user-written definition is user-provided.
The following suppression rules require deliberate review:
| Operation | Important Rule |
|---|---|
| Default constructor | A user-declared constructor or constructor template suppresses it. |
| Move constructor and move assignment | A user-declared copy constructor, copy assignment, move operation, or destructor suppresses both implicit move operations. |
| Copy constructor and copy assignment | They are implicitly declared when not user-declared, but can be defined as deleted because a base or member cannot perform the required operation. |
Reference members and unsuitable const members commonly delete implicit assignment. Declaring either move operation causes both implicitly declared copy operations to be defined as deleted. Inaccessible, deleted, or unusable operations in a base or member can delete other implicit special members. Compiler-generated functions can also be trivial when their bases and members meet the standard’s structural requirements; triviality is a type property, not an ownership policy.
Prefer the Rule of Zero: compose types with correct ownership behavior and declare no special members in a domain type.
#include <string>
#include <utility>
#include <vector>
class Document {
public:
explicit Document(std::string title) : title_(std::move(title)) {}
private:
std::string title_;
std::vector<std::string> paragraphs_;
};A custom resource owner needs an explicit coherent policy. A move-only owner deletes copying, releases exactly once, and should make moves noexcept when transfer cannot fail. Defining one of the old “Rule of Three” operations or one move operation should trigger a review of all copy, move, and destruction behavior, not an automatic implementation of five functions.
Inheritance and Virtual Dispatch #
Public inheritance expresses substitutability: code correct for Base must remain correct for a Derived. Use composition for implementation reuse that is not an “is-a” relationship. Private and protected inheritance change inherited accessibility but do not expose public substitutability; a base’s private members remain inaccessible by name to derived classes.
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle final : Shape {
double area() const override;
};A pure virtual function without a final overrider makes a class abstract. A pure virtual destructor still requires a definition because every derived destructor invokes it. Virtual dispatch chooses a final overrider for the object’s dynamic type, except during construction and destruction, when calls dispatch only within the constructor’s or destructor’s class hierarchy. Avoid virtual calls that depend on derived state from base constructors or destructors.
Use override on every intended override because it rejects signature mismatches. A polymorphic base that may be destroyed through a base pointer needs a public virtual destructor; otherwise prohibit that destruction route.
Operators and Literal Suffixes #
Overload an operator only when it preserves the operator’s established meaning. operator= and operator-> must be non-static members. operator[] and operator() were also required to be non-static before C++23; C++23 permits static overloads of those two operators. Prefix increment conventionally returns T&, and postfix increment accepts an unused int discriminator and conventionally returns the prior T value.
class Flag {
public:
explicit operator bool() const noexcept;
Flag& operator++(); // prefix
Flag operator++(int); // postfix
};Mark conversion operators explicit unless implicit conversion is an intentional part of the interface. Operators cannot alter precedence, arity, or short-circuit behavior, and no new operator token can be introduced.
User-defined literal operators are declared at namespace scope; member functions cannot define literal suffixes. Argument-dependent lookup does not find them. Their suffix must start with an underscore: suffixes without it are reserved for the standard library, and identifiers beginning with an underscore followed by an uppercase letter or another underscore are reserved.
#include <cstddef>
struct Bytes {
std::size_t value;
};
constexpr Bytes operator"" _bytes(unsigned long long value) {
return {static_cast<std::size_t>(value)};
}
constexpr Bytes cache = 64_bytes;Cooked integer and floating literal operators take unsigned long long and long double, respectively. String literal operators take the corresponding character pointer and std::size_t length; character literal forms use their applicable character type. Select only a parameter form permitted for the literal category, and keep the suffix’s unit and conversion rules explicit.
Comments
Read or comment on GitHub (sign-in required to comment)