This bounded anthology collects language and toolchain-facing rules that do not require a separate article. Each section states the boundary that governs the feature, because similar-looking C++ spellings can have different ownership, lookup, or lifetime effects.
Resource Ownership and Optional State #
Ownership is the responsibility to release a resource at the correct time. Model it with RAII: acquire in construction, release in destruction, and make the object’s invariant hold across exceptional control flow. Prefer the Rule of Zero when members such as std::unique_ptr or std::vector already implement the resource policy. A type that directly manages a raw resource must define a coherent destructor, copy policy, move policy, and failure policy.
std::make_unique<T>(args...) expresses sole ownership without exposing a standalone new; it has been available since C++14. std::make_shared<T>(args...) may combine the control block and object allocation. Combined allocation is not always appropriate: a weak_ptr can keep that allocation alive after the object is destroyed, and custom deletion or an existing raw pointer can require direct construction. When direct construction is necessary, establish the smart-pointer owner in the same full expression. Before C++17 tightened function-argument evaluation sequencing, separating allocation from ownership inside a larger call could also leak if another argument threw.
Choose the representation of an optional subobject from its ownership and identity:
- A direct member with a valid empty state has no indirection, but that state must be unambiguous.
std::optional<T>stores a possibly absentTin the containing object;emplacebegins the contained object’s lifetime without a separate allocation.std::unique_ptr<T>represents exclusive ownership of a distinct object and supports incomplete or polymorphic types, at the cost of indirection and a nullable handle.
Neither optional nor heap allocation lets an object outlive its owner. A reference, pointer, or std::reference_wrapper<T> is a non-owning handle; the referred object must outlive every use. std::ref and std::cref are useful when a value-oriented interface, such as a container or std::bind, must carry a reference.
Interfaces, Operators, and Object Model #
A symmetric binary operator should often be a nonmember so conversions can be considered for both operands. Declaring it as a friend grants narrowly scoped private access; friendship does not make it a member.
class Quantity {
public:
explicit Quantity(int value) : value_{value} {}
friend Quantity operator+(Quantity left, const Quantity& right) {
left.value_ += right.value_;
return left;
}
private:
int value_;
};A polymorphic class has a virtual function. The standard specifies virtual-dispatch behavior but not a vptr, a vtable, their layout, or their ABI. Many implementations use hidden pointers and tables of function pointers, but multiple inheritance and compiler options invalidate assumptions about that model. Application code must rely on virtual functions, not representation details.
An abstract class is complete once its class definition is complete, even though a pure virtual function prevents direct instantiation. An incomplete class has only been declared and cannot be used for sizeof or direct data members. In a non-virtual diamond, a derived object contains two base subobjects, so an unqualified inherited member can be ambiguous. A static data member belongs to its declaring class, however; it is not duplicated for each base subobject.
struct A {
int value;
static int count;
};
struct B : A {};
struct C : A {};
struct D : B, C {};
int A::count = 0;
D object;
// object.value is ambiguous: object contains two A base subobjects.
D::count = 1; // One static member; no A object is selected.
Expressions, References, and Parsing #
Every non-void expression is a prvalue, xvalue, or lvalue. Lvalues and xvalues are glvalues because they identify entities; prvalues and xvalues are rvalues. Named variables—including a variable declared T&&—are lvalue expressions. A scalar prvalue has no addressable identity for the built-in address-of operator.
References are not objects. A named reference is an lvalue expression of its referred-to type. Template substitution can form references to references; collapsing reduces T& &, T& &&, and T&& & to T&, while only T&& && remains T&&. These rules enable forwarding without creating nested reference objects.
const member functions constrain the implicit object parameter. A const overload can run on const and non-const objects; a non-const overload cannot run on a const object. Within a const member, non-mutable members cannot be modified through *this.
When text can be parsed as a declaration, C++ parses it as a declaration. This most-vexing parse does not construct an object:
Widget widget(Factory()); // May declare a function.
Widget object{Factory{}}; // Unambiguously constructs an object.
Integral Conversions and Shifts #
Integer promotions convert bool, character types, short, and other eligible types with rank below int to int when int represents all values; otherwise they promote to unsigned int. The usual arithmetic conversions then select a common type. Therefore, arithmetic on unsigned char values occurs as int arithmetic on common systems where int represents every unsigned char value.
unsigned char left = 42;
unsigned char right = 55;
int sum = left + right;For a shift, both operands undergo integral promotion. A negative count or a count at least as large as the width of the promoted left operand is undefined behavior. Use unsigned types and validate the count for portable bit manipulation. C++ has no Java-style >>>; right shift of an unsigned value shifts in zero bits. C++20 specifies signed right shift as arithmetic right shift, but bit-oriented code should still avoid signed operands when representation matters.
Callables, Lookup, and Polymorphism #
Each lambda expression has a distinct unnamed closure type; decltype can name a particular closure object’s type. Captures are stored by the closure object, so captured references do not extend lifetime. A generic lambda has a templated call operator and instantiates behavior for the argument types used.
An unqualified function call combines ordinary lookup with argument-dependent lookup (ADL). ADL considers namespaces and classes associated with its argument types, which supports customization patterns such as:
using std::swap;
swap(left, right); // Standard fallback plus an associated-namespace overload.
Qualified calls suppress ADL. Do not rely on ADL for fundamental types: an int does not associate a call with an unrelated namespace.
Function parameters declared with a function type adjust to pointers to function type. Function names also convert to function pointers in the applicable contexts.
void callback(void);
void invoke(void f(void));
void invoke_pointer(void (*f)(void));
void use(void) {
invoke(callback);
invoke_pointer(&callback);
}C++ adds overload sets, templates, and member-function pointers, so a function name can need a target type before one overload is selected.
Ad hoc polymorphism selects an overload from static argument types. Parametric polymorphism instantiates templates from compile-time requirements. Subtype polymorphism dispatches a virtual call through a base pointer or reference at runtime. Type erasure—such as std::function, a concept/model wrapper, or a base interface—replaces a concrete type in a boundary with a runtime interface. It can reduce header and ABI coupling, but may add indirection, allocation, wrapper storage, and runtime checks.
Lazy Initialization and Concurrency #
Function-local static initialization is thread-safe: if multiple threads enter the declaration concurrently, initialization completes once before any proceeds, and an exception permits a later retry. For lazily initializing a member shared among threads, protect the state transition with std::mutex, std::call_once, or an appropriate atomic protocol.
const Settings& settings() {
static const Settings value = load_settings();
return value;
}volatile neither makes accesses atomic nor creates a happens-before relationship. It does not replace a mutex, condition variable, or std::atomic, and it does not promise cache flushes or main-memory traffic. Use it only when a platform contract, such as memory-mapped I/O, requires observable volatile accesses.
Translation-Unit Dependencies #
Including a header creates a compile-time dependency when changes to declarations in that header require client recompilation. Exposing a concrete container or its iterator type in a public interface couples clients to that representation. A narrow abstract interface or a Pimpl boundary can preserve implementation freedom, but each adds an interface or indirection cost.
An automatic object constructed inside a function is constructed and destroyed once per call. Moving it outside a loop merely to avoid construction changes its lifetime and may retain state, resources, or synchronization beyond the intended scope.
Comments
Read or comment on GitHub (sign-in required to comment)