Object-Oriented Programming with C++


Detailed C++ notes on classes, object lifetime, references, resource management, overloading, inheritance, polymorphism, streams and the standard library.
The C++ scope focuses on object-oriented programming and resource management. Concepts established after C++11 are treated together with the foundational progression through classes, object lifetime, inheritance, polymorphism, and the standard library.
Unit 1: Moving from C to C++
1.1. Relationship Between C and C++
C++ grew historically from C and retains many low-level facilities, but it is a separate language with its own object model, type rules, standard library and design idioms. Valid C is not automatically good C++, and some C constructs do not have identical semantics in C++.
A minimal program is:
#include <iostream>
int main()
{
std::cout << "hello\n";
return 0;
}The standard library is organized in namespaces, primarily std. A global using namespace std; may shorten teaching examples, but production headers should avoid exporting such broad namespace directives to their users.
1.2. Initialization
C++ supports several initialization forms:
int a = 10;
int b(10);
int c{10};Brace initialization is useful because it rejects many narrowing conversions:
int x{3.5}; // ill-formedThe exact choice should reflect the desired constructor and conversion semantics rather than a stylistic rule applied blindly.
1.3. auto
auto asks the compiler to deduce a variable's type from its initializer:
auto count = 10;
auto ratio = 0.5;It reduces redundant type spelling, especially with iterators and template-heavy types. It does not make C++ dynamically typed; the deduced type is still a compile-time type.
1.4. const And constexpr
const prevents modification through that name or access path where the type system applies it. constexpr expresses that an entity can participate in constant evaluation when its requirements are satisfied.
constexpr int buffer_size = 4096;The two concepts overlap but are not identical: const is primarily about mutability, while constexpr is about constant-expression semantics.
1.5. nullptr
C++11 introduced nullptr as a dedicated null pointer literal:
int *p = nullptr;It avoids overload ambiguities caused by integer literal 0 or macro-style NULL values.
1.6. Streams, Strings and Vectors
C++ standard I/O uses streams:
std::cout << value << '\n';
std::cin >> value;std::string owns a dynamically sized character sequence and is generally preferable to manual C-string management for ordinary text processing:
std::string name = "Ali";std::vector<T> is the standard contiguous dynamic sequence container:
std::vector<int> values{1, 2, 3};
values.push_back(4);It manages storage automatically and keeps ownership with the container.
1.7. Fundamental Differences Between C and C++
C and C++ share syntax and low-level capabilities, but they encourage different abstractions. C commonly models state through structures, functions and explicit ownership conventions. C++ adds deterministic object lifetime, constructors/destructors, references, overloading, templates, exceptions, generic containers and compile-time abstraction mechanisms.
A C program converted mechanically to C++ does not automatically become idiomatic C++. Conversely, C++ abstractions should not hide performance or ownership properties that are important to a systems-level design. The useful common principle is to make lifetime, representation, control flow and error behavior explicit enough that the code remains predictable.
1.8. Reference Types
A reference is an alias bound to an object:
int value = 10;
int& ref = value;References are useful for mandatory non-owning access, while pointers naturally express nullable or reseatable relationships. The semantic distinction is more important than the syntax.
Unit 2: Object-Oriented Programming
Before defining a class boundary, identify the object invariants and the operations allowed to change them. The public interface should not expose intermediate states that violate those invariants. If inheritance is proposed, test whether the subtype truly preserves the base type's behavioral contract; eliminating duplicated code is insufficient justification. The design decision is thereby grounded in behavior rather than a class diagram alone.
2.1. Objects and Classes
A class defines state and operations that maintain the invariants of objects of that type:
class Counter {
private:
int value_;
public:
explicit Counter(int value) : value_(value) {}
void increment() { ++value_; }
int value() const { return value_; }
};Object-oriented design is more than a mechanism for grouping fields and functions. The useful abstraction is one in which operations preserve a coherent model and hide representation details that callers should not depend on.
2.2. Abstraction and Encapsulation
Abstraction exposes the properties relevant to users of a type. Encapsulation controls access to representation and implementation detail. C++ provides public, private and protected access control.
A struct and a class have almost the same language capabilities; their main default differences are access and inheritance visibility. struct defaults to public members, while class defaults to private members.
2.3. Constructors
A constructor establishes a valid object state:
class Point {
int x_;
int y_;
public:
Point(int x, int y) : x_(x), y_(y) {}
};Members are initialized in declaration order, not in the textual order of the initializer list. This matters when one member depends on another.
A default constructor can be compiler-generated or explicitly requested:
Point() = default;A single-argument constructor can unintentionally define an implicit conversion. explicit suppresses such conversion where it is not part of the intended interface:
explicit Size(std::size_t value);2.4. Destructors and RAII
A destructor runs when an object's lifetime ends:
~Resource();C++'s central resource-management idiom is RAII: acquire a resource in object construction and release it in destruction. This associates file handles, locks, memory and other resources with lexical object lifetime, making cleanup work across normal return paths and exception unwinding.
2.5. Copying and Assignment
Copy construction creates a new object from an existing one. Copy assignment replaces the state of an already existing object. For resource-owning types, these operations must have well-defined ownership semantics.
If a type can be represented safely by standard value members such as std::string, std::vector and smart pointers, the Rule of Zero is usually preferable: let those members implement ownership so the user-defined class does not need custom copy/move/destructor logic.
2.6. Move Semantics
C++11 move semantics allow resources to be transferred from an expiring object rather than copied:
T(T&& other);
T& operator=(T&& other);A moved-from object must remain valid according to the guarantees of its type, although its value may be unspecified. Move operations are performance and ownership tools, not permission to access destroyed state.
Unit 3: References, Object Pointers and Memory Management
Object resource management begins by deciding lifetime and whether ownership is exclusive or shared. Placing acquisition and release within the same responsibility boundary makes cleanup during failures more predictable. Choose move or copy semantics by asking whether the resource may be duplicated and what valid state must remain after the operation. Test reference validity and exception safety as well as address correctness.
3.1. Passing by Value and by Reference
Passing a small value type by value is often simplest:
int square(int x);A read-only large object can be passed by const reference:
void process(const std::string& text);A non-const reference can make mutation explicit:
void normalize(Vector& v);For APIs, the parameter form should communicate ownership and mutation expectations.
3.2. this
Inside a non-static member function, this is a pointer to the current object. It is useful when distinguishing members from parameters or returning the current object:
return *this;3.3. new And delete
Raw dynamic allocation exists:
T* p = new T;
delete p;
T* a = new T[n];
delete[] a;but direct ownership with raw new/delete is usually avoidable in modern C++. Containers and RAII wrappers make ownership clearer and exception-safe.
3.3.1. Allocation, object construction, and alignment
A new T{...} expression obtains suitable storage, initializes an object, and returns a typed pointer. By contrast, ::operator new(sizeof(T)) obtains raw storage only: a T object has not yet been constructed there. If a constructor throws, the new expression calls the matching deallocation function when applicable. Code that manages raw storage directly must establish its own equivalent cleanup protocol.
#include <cstddef>
#include <memory>
#include <new>
struct Sample {
int value;
explicit Sample(const int v) : value(v) {}
};
int main() {
alignas(Sample) std::byte storage[sizeof(Sample)];
Sample* object = ::new (static_cast<void*>(storage)) Sample{42};
const int result = object->value;
std::destroy_at(object);
return result == 42 ? 0 : 1;
}Placement new in this example performs no allocation; it begins an object's lifetime within suitably aligned storage. delete object would be incorrect because that storage was not acquired by the ordinary allocation function. Manual construction is mainly useful when implementing low-level containers or memory pools. Application code normally expresses ownership through std::vector, std::optional, and RAII types. Storage reuse must preserve separate invariants for lifetime, pointer validity, and alignment.
3.3.2. Allocation failure and resource limits
The throwing form of the standard operator new normally signals failure by throwing std::bad_alloc. new (std::nothrow) T can return a null pointer when allocation fails, but does not generally suppress exceptions thrown by the object's constructor. A handler installed with std::set_new_handler must make memory available, throw, or terminate; returning without changing the failure condition can trigger repeated allocation attempts.
An allocation-failure policy requires more than adding catch (std::bad_alloc). Previously published pointers, partially initialized structures, and the resource cost of retries must also be checked. Under memory pressure, the desired behavior is bounded work, preserved object invariants, and failure isolation between independent requests.
3.4. Smart Pointers
std::unique_ptr<T> represents exclusive ownership:
auto p = std::make_unique<Object>();It is movable but not copyable.
std::shared_ptr<T> represents shared ownership through reference counting. It should be used only when ownership is genuinely shared; otherwise it obscures lifetime and adds synchronization/reference-counting overhead. Cycles involving shared_ptr require std::weak_ptr or a different ownership design.
3.4.1. Control blocks, weak ownership, and allocation choices
std::shared_ptr maintains ownership through a control block associated with the managed object. It tracks strong owners, the bookkeeping required by weak observers, and potentially a custom deleter and allocator. The managed object is destroyed after its last strong owner disappears; control-block storage may remain while weak observers exist. In concurrent code, use_count() does not guarantee that the object will remain alive at a later instant.
std::make_shared<T> can commonly place the managed object and control block within a single allocation, improving allocation count and locality. However, outstanding weak_ptr objects may keep this combined allocation in place after the object is destroyed. For large objects this retention deserves measurement. Choosing between direct construction and make_shared is not merely a syntactic preference.
#include <memory>
struct Node {
int id;
explicit Node(const int value) : id(value) {}
};
int main() {
std::weak_ptr<Node> observer;
{
auto owner = std::make_shared<Node>(7);
observer = owner;
if (const auto live = observer.lock()) {
if (live->id != 7) return 1;
}
}
return observer.expired() ? 0 : 1;
}Failure of weak_ptr::lock() is an ordinary lifecycle outcome. shared_from_this() must not be called before an object is managed by a suitable shared_ptr; inheriting from std::enable_shared_from_this alone does not establish shared ownership. Prefer unique_ptr when a single owner accurately describes the resource.
3.5. Arrays of Objects
Prefer containers when the number of objects is dynamic:
std::vector<Object> objects;This preserves deterministic destruction and avoids manual array allocation.
Unit 4: Function and Operator Overloading
4.1. Function Overloading
Functions can share a name when their parameter lists distinguish them:
void print(int value);
void print(double value);Overload resolution uses compile-time type information and conversion ranking. Return type alone cannot distinguish overloads.
4.2. Default Arguments
void connect(int timeout_ms = 1000);Default arguments are substituted at the call site according to declarations visible there. Changing a default can therefore affect callers differently from changing an implementation body.
4.3. Constructor Overloading and Delegating Constructors
A class can provide multiple construction forms. C++11 also allows one constructor to delegate to another:
Widget() : Widget(0) {}Delegation reduces duplicated initialization logic.
4.4. Operator Overloading
User-defined types can overload many operators:
Vector operator+(const Vector& a, const Vector& b);An overloaded operator should preserve the ordinary semantic expectation of that operator. Surprising side effects make generic code difficult to reason about.
Comparison operators should define a coherent relation. C++20's three-way comparison can synthesize related comparisons for appropriate types, but older explicit operators remain valid and common.
Prefix and postfix increment are distinct signatures; postfix conventionally takes an unused int parameter:
Counter& operator++();
Counter operator++(int);Unit 5: Inheritance and Polymorphism
5.1. Inheritance
Inheritance defines a derived class in terms of a base-class relationship:
class Derived : public Base {
...
};Public inheritance should model an substitutable "is-a" relation, not simply code reuse. Composition is often more appropriate when one object merely contains or uses another.
Protected and private inheritance change accessibility and conversion relationships and are less commonly appropriate for domain modeling.
5.2. Construction and Destruction Order
Base subobjects are constructed before derived-class members and the derived constructor body. Destruction occurs in the reverse order. Virtual bases have additional ordering rules defined by the language.
5.3. Multiple Inheritance and the Diamond
C++ supports multiple direct base classes. A diamond can introduce duplicated base subobjects:
A
/ \
B C
\ /
DVirtual inheritance can make B and C share one A subobject when that is the intended model. Multiple inheritance is powerful but increases layout and lifecycle complexity.
5.4. Virtual Functions and Dynamic Dispatch
A virtual function enables runtime dispatch through a base reference or pointer:
class Base {
public:
virtual void run();
};A derived override should use override so mismatches are diagnosed:
void run() override;A polymorphic base intended for deletion through a base pointer normally needs a virtual destructor:
virtual ~Base() = default;A pure virtual function defines an abstract interface:
virtual void run() = 0;Dynamic polymorphism uses runtime dispatch. Templates can provide static polymorphism, where behavior is selected at compile time without a virtual call. The two mechanisms solve different design problems.
5.5. Boundaries Among Inheritance, Polymorphism, and Object Lifetime
Inheritance in C++ is not merely a mechanism for reducing duplicated code; when used well, it represents an “is-a” relationship. A class should not derive from another class only because they share a few fields. If their lifecycles or reasons for change differ, composition often gives a clearer design.
Runtime polymorphism requires virtual dispatch through a base-class interface. Copying a derived object by value into a base-class object, however, causes object slicing and discards the derived portion. Polymorphic objects are therefore commonly used through references or smart pointers that express an appropriate ownership model.
A polymorphic base class that may be used to delete derived objects needs a virtual destructor:
struct Base {
virtual ~Base() = default;
virtual void run() = 0;
};The purpose is not syntactic style but correct destruction of resources owned by the complete derived object. RAII generalizes this lifetime discipline: resource acquisition is tied to construction and release to destruction, so normal return, exceptions, and early exits follow the same cleanup rule.
Copy and move semantics must also be considered together with ownership. If a class does not directly manage a resource, the Rule of Zero is often the safest design. If resource management truly belongs to the class, copy, move, and destruction behaviour must form a coherent policy.
C++ object-oriented reasoning is therefore clearer when three questions are kept together rather than memorizing isolated keywords: What is the dynamic type of the object, who owns its resources, and when does its lifetime end?
Unit 6: C++ I/O and Files
6.1. Streams
C++ models I/O as streams. std::cout, std::cerr and std::cin are standard stream objects. Stream state records whether formatted extraction or I/O has failed.
int value;
if (std::cin >> value) {
...
}Checking stream state is preferable to assuming input succeeded.
6.2. Formatting
The <iomanip> facilities can control width, precision, base and other formatting properties. Some manipulators modify persistent stream state, so reusable code should not assume formatting is unchanged by previous operations.
6.3. File Streams
std::ofstream out("data.txt");
std::ifstream in("data.txt");The stream object closes its file during destruction, demonstrating RAII. Open state and I/O errors should still be tested.
std::getline reads a complete line into std::string:
std::string line;
while (std::getline(in, line)) {
...
}6.4. Binary and Random Access
Binary mode is requested with std::ios::binary. read and write transfer byte sequences. As with C, writing the raw memory image of an arbitrary object does not define a portable serialization format.
seekg/seekp and tellg/tellp support positioning where the stream permits it.
Unit 7: The C++ Standard Library
7.1. STL Model
The Standard Template Library style separates:
- containers that own or organize data,
- iterators that identify positions/ranges,
- algorithms that operate on ranges,
- function objects and callable values that customize operations.
This separation allows one algorithm to work with many container types when their iterator requirements are satisfied.
7.2. Sequence Containers
std::vector provides contiguous dynamic storage and amortized constant-time insertion at the end. Reallocation can invalidate pointers, references and iterators into the old storage.
std::array<T, N> is a fixed-size array wrapper with ordinary container semantics.
std::list is a doubly linked list. It provides stable node addresses and constant-time insertion/erasure at known positions but has poor locality and no constant-time random access.
std::deque supports efficient insertion/removal at both ends and random access, without requiring one contiguous allocation.
7.3. Container Adaptors
std::stack, std::queue and std::priority_queue expose restricted interfaces over underlying containers. priority_queue provides access to the highest-priority element according to its comparator rather than sorted iteration over all elements.
7.4. Associative and Unordered Containers
Ordered associative containers such as std::map and std::set maintain key order and usually provide logarithmic search/insert/erase complexity.
Unordered containers such as std::unordered_map use hashing and provide average constant-time lookup under normal hash-distribution assumptions, with different worst-case behavior and iteration semantics.
7.5. Iterators
Iterators generalize traversal. Categories encode supported operations, from single-pass input iteration through random-access and contiguous iteration. An algorithm's complexity and valid operations depend on the iterator category it requires.
7.5.1. Invalidation and container invariants
An iterator denotes a position; its validity after a structural mutation depends on the container and the operation. When a std::vector reallocates, all iterators, references, and pointers into its elements become invalid. Without reallocation, push_back preserves references and iterators to existing elements, but invalidates the old end(). Inserting into a std::list does not invalidate iterators to existing elements; an iterator to an erased element cannot be reused. Rehashing an std::unordered_map can invalidate iterators while references and pointers to elements that were not erased follow separate validity rules.
#include <algorithm>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3, 4};
values.erase(std::remove_if(values.begin(), values.end(),
[](const int value) { return value % 2 == 0; }), values.end());
return values.size() == 2 && values[1] == 3 ? 0 : 1;
}Rather than continue a loop using invalidated iterators, use the new boundary returned by the applicable algorithm. C++20 std::erase_if expresses the same intent more directly for supported containers. Complexity analysis should account for reallocation, locality, and iterator stability as well as asymptotic operation counts.
7.6. Algorithms
Common algorithms include:
find
sort
count
search
transformFor example:
std::sort(values.begin(), values.end());requires a random-access range. Algorithms usually do not own the elements; they operate through iterators.
7.7. Lambdas
A lambda defines an unnamed callable:
auto pred = [](int x) { return x > 0; };Its capture list determines which surrounding objects or values become part of the closure. Capturing by reference requires lifetime discipline.
7.8. Ranges
C++20 ranges add range-aware algorithms, views and pipelines. They reduce explicit iterator pairing and support lazy composition, but views frequently reference other objects and therefore introduce lifetime considerations of their own.
7.8.1. View lifetime, borrowed ranges, and dangling data
A range is an abstraction for iteration; a view typically represents a lightweight, often lazy computation that need not own the underlying data. A view cannot be assumed to extend that data's lifetime. In particular, std::span and std::string_view refer to externally owned storage. Once its owner is destroyed or reallocates, the view object may remain syntactically usable even though dereferencing it is invalid.
std::ranges::borrowed_range is a type-level property concerning iterator usability after a temporary range object ends; it does not generally extend the lifetime of arbitrary temporary data. With lazy adaptors, references captured from the surrounding scope must remain valid when evaluation occurs. A zero-copy design used across asynchronous boundaries therefore needs an explicit lifetime or ownership protocol.
Unit 8: Modern Resource and Type Safety
8.1. Raw Pointers Do Not Imply Ownership
A raw pointer is best treated as an address/non-owning access mechanism unless an API explicitly documents ownership transfer. Ownership should preferably be visible in values, containers or smart-pointer types.
8.2. std::span
std::span<T> represents a non-owning contiguous sequence with a pointer and extent:
void process(std::span<const int> values);It makes length part of the view and is safer than a naked pointer without a corresponding count. It does not extend the lifetime of the referenced storage.
8.3. std::string_view
std::string_view is a non-owning view of character data. It avoids allocation and copying for read-only string slices, but it can dangle if the original string/storage is destroyed or reallocated.
8.4. Exceptions
Exceptions transfer control from a failure point to a matching handler while unwinding automatic objects:
try {
...
} catch (const std::exception& e) {
...
}RAII is what makes this manageable: resources stored in automatic objects are released as destructors run during unwinding.
8.5. noexcept
noexcept states that a function is not expected to let exceptions escape. Violating an unconditional noexcept results in termination. It is therefore a semantic contract, not a performance decoration.
Move operations that are noexcept can also enable stronger behavior in standard containers during reallocation.
8.6. Undefined Behavior
C++ contains operations whose behavior is not defined by the language, including many out-of-bounds accesses, use-after-lifetime errors, invalid shifts, certain signed overflows and data races. Optimization assumes that a well-formed executing program does not perform undefined behavior, so the result can be much more severe than a local wrong value.
8.6.1. Undefined, unspecified, and implementation-defined behavior
Undefined behavior is a condition for which the C++ standard imposes no requirements. Its consequences are not limited to unexpected output: compilers may optimize valid programs under the assumption that undefined behavior never occurs. With unspecified behavior, the standard allows a set of alternatives without requiring the implementation to document its choice. With implementation-defined behavior, the implementation is expected to document its choice. These categories have different implications for portability and correctness.
For example, accessing outside an array's bounds can cause undefined behavior. Different outcomes between optimization levels do not imply randomness; the program has left the contract defined by the language. Compiler warnings, static analysis, and sanitizers provide useful evidence, but a single clean execution does not establish the absence of undefined behavior.
8.7. Warnings and Sanitizers
Compiler warnings should be treated as engineering feedback, not cosmetic output. High warning levels, static analysis and sanitizers such as AddressSanitizer and UndefinedBehaviorSanitizer can expose defects that ordinary tests may not make visible.
8.8. Safe Default Assumptions
Useful defaults for both C and C++ include:
- initialize objects before reading them,
- make ownership explicit,
- preserve bounds with data,
- avoid unchecked arithmetic when sizes come from external input,
- do not rely on unspecified object representation,
- prefer standard-library resource owners to manual allocation,
- keep interfaces const-correct,
- treat compiler diagnostics and runtime instrumentation as part of verification.
8.9. Designing Value Semantics, RAII, and Ownership Together
Reliable modern C++ design is not centered solely on classes and inheritance. It makes object lifetime and ownership visible through types. Resource Acquisition Is Initialization (RAII) binds resource cleanup to object lifetime so that normal returns and exception unwinding follow the same release rule.
8.10. Rule of Zero and Rule of Five
When a class stores resources through RAII types such as std::vector, std::string, and std::unique_ptr, it often needs no hand-written destructor, copy/move constructor, or assignment operators. This is the Rule of Zero.
If a class must directly manage a special resource, copy and move behavior must be designed as one contract. Is copying deep, prohibited, or reference-like? What state remains after a move? These decisions are connected rather than independent boilerplate.
8.11. unique_ptr, shared_ptr, And Raw Pointers
std::unique_ptr<T> expresses unique ownership; std::shared_ptr<T> expresses reference-counted shared ownership. shared_ptr is not a universal safe default: it adds reference-counting/ownership-block costs and can create ownership cycles. std::weak_ptr can represent a non-owning relationship into a shared ownership graph.
Raw pointers remain valid tools in modern C++, especially for nullable non-owning observation. The core problem is not the syntax T*; it is an unclear ownership contract.
8.12. Copy and Move Semantics
A move usually transfers ownership of an expensive resource. It does not inherently perform some magical byte-level optimization. A moved-from object remains valid but its value is constrained by the relevant type contract. std::move itself does not move anything; it changes the value category so that a move overload can be selected.
Containers can use information such as a noexcept move constructor when preserving strong exception guarantees during reallocation. noexcept can therefore affect generic-container behavior rather than being a cosmetic annotation.
A move constructor's noexcept specification can affect whether containers relocate elements by moving or copying. When a vector grows, existing elements must be transferred to new storage; if moving may throw and copying is available, a copy may be selected to preserve strong exception guarantees. Declaring every move unconditionally noexcept is unsafe, however: an exception escaping such a function terminates the program. Move safety must be designed together with ownership transfer and the valid state of an object after it has been moved from.
8.13. Polymorphism and Destruction
If an object can be deleted through a base-class pointer, the base needs an appropriate virtual destructor. Otherwise destruction through the base can produce undefined behavior. Conversely, adding a virtual destructor to every class by habit changes semantics and representation without justification; the requirement comes from the polymorphic ownership model.
8.14. Composition and Inheritance
Public inheritance is stronger than a code-reuse mechanism: it expresses an is-a/substitutability relationship. Choosing inheritance only to reuse implementation can produce fragile hierarchies. Composition often states ownership and collaboration more directly.
8.15. Generic Programming and Concepts
Templates allow algorithms and data structures to be parameterized by types and values. C++20 concepts make template requirements explicit and improve diagnostics by constraining candidate types closer to the interface contract.
This illustrates a central C++ design goal: build higher-level abstractions without hiding their essential lifetime and performance costs.
8.16. Ownership, RAII, and Exception Safety
Modern C++ design is fundamentally about ownership. RAII ties resources such as memory, files, locks, and sockets to object lifetime so cleanup follows the same rule on normal and exceptional exits.
std::unique_ptr expresses single ownership. std::shared_ptr expresses shared ownership, but should not be the default because ownership cycles and unclear lifetime can result. Non-owning observations should remain distinguishable from owners.
Exception-safe operations should at least preserve valid invariants when they fail. Stronger designs leave observable state unchanged on failure. Move semantics, swap-based updates, and narrowly scoped classes make these guarantees easier to implement.
8.16.1. Basic and strong guarantees in transactional object updates
An exception-safety contract states what remains true when an operation fails. The basic guarantee preserves invariants and prevents resource leaks, although the object's value may change. The strong guarantee leaves the observable state as it was before the operation if an exception is thrown. The no-throw guarantee means the operation does not emit exceptions. These promises form increasingly restrictive requirements, not interchangeable descriptions.
#include <string>
#include <utility>
class Configuration {
std::string value_;
public:
void replace(std::string next) noexcept {
value_.swap(next);
}
const std::string& value() const noexcept { return value_; }
};If constructing the by-value next argument fails before entering replace, the existing value_ is unchanged. In this example, the std::string swap in the body does not throw; the previous value is released when the local argument is destroyed. The update separates preparation from commit. For operations spanning several objects or external systems, a single swap may not establish the strong guarantee for every observable side effect.
An incorrect noexcept declaration can invoke std::terminate rather than allow the caller to handle an exception. Nonthrowing destruction and cleanup of only those members that were successfully constructed are essential to robust resource management.
8.17. Verifiable Ownership in C++ Design
A readable interface is not sufficient when ownership, lifetime, and exception behavior are unclear. If there is one owner, std::unique_ptr may express that directly; value semantics may remove ownership machinery entirely; std::shared_ptr should reflect genuine shared lifetime rather than convenience.
Verification can combine unit tests, sanitizers, static analysis, different optimization levels, and tests that exercise copy/move paths. Benchmarks should record compiler version, flags, and data size.
For RAII and exception safety, the central question is which invariants remain valid after failure. Once that contract is explicit, design quality can be judged by observable behavior rather than style alone.
8.18. Move Semantics, Exception Safety, and Valid Object States
Moving a resource-owning C++ object is not equivalent to copying its representation byte for byte. Move construction and move assignment establish an ownership transfer that must avoid double release. A moved-from object must remain destructible and valid for operations defined by its type, but it need not preserve its former value. Post-move guarantees must be read from the relevant API contract rather than assumed universally.
Standard containers may move or copy elements while reallocating storage. A noexcept move operation can affect how a container preserves the strong exception guarantee in certain operations, but merely adding noexcept does not make a flawed move implementation correct. When a raw owning pointer is transferred without leaving the source in a safe state, both destructors may release the same allocation. RAII and std::unique_ptr are therefore preferable defaults for exclusive ownership.
Exception guarantees should be distinguished explicitly: the basic guarantee preserves invariants and avoids leaks; the strong guarantee leaves externally observable state unchanged if an operation fails; a no-throw guarantee excludes exceptions from the operation. Constructing a new state first and committing it only after successful preparation can help provide the strong guarantee. Tests should inject allocation and copying failures to verify the guarantee offered by a particular API.
8.18.1. Conditional moving and exception safety during reallocation
std::move does not itself move data: it casts an expression to an appropriate value category so that overload resolution can select a move operation. The selected constructor can still be a copy constructor. std::move_if_noexcept may prefer copying when moving could throw and copying is available, helping callers preserve a strong exception guarantee. This is particularly relevant when std::vector grows: failure halfway through moving several elements can make rollback difficult.
#include <type_traits>
#include <utility>
struct Data {
Data(const Data&);
Data(Data&&) noexcept;
};
static_assert(std::is_nothrow_move_constructible_v<Data>);A moved-from object must remain valid and destructible, but its value is known only to the extent promised by that type's contract. Move cost, exception guarantees, and copyability must be assessed together. noexcept is a verifiable statement about actual behavior, not a general-purpose speed switch.
Unit 9: Templates, Concepts, and Generic Programming
Generic programming in C++ is more than making one function accept several types. The goal is to preserve type information at compile time, express reusable algorithms, and reject invalid type combinations as early as possible.
9.1. Function and Class Templates
A function template lets the compiler determine a parameter type during compilation:
template <class T>
T maxValue(const T& a, const T& b) {
return b < a ? a : b;
}The compiler instantiates concrete forms for the types that are required. This can provide zero-overhead abstraction because runtime type dispatch is unnecessary, but many instantiations can increase binary size and compile time.
Class templates apply the same model to data structures. std::vector<T>, std::optional<T>, and smart pointers are common examples.
9.2. Constraints and concept
Syntactic validity alone is not enough; an algorithm usually expects semantic operations from its type parameters. Since C++20, concept and requires make template constraints explicit:
template <class T>
concept Addable = requires(T a, T b) {
a + b;
};
template <Addable T>
T add(const T& a, const T& b) {
return a + b;
}A concept is not a runtime interface or an inheritance hierarchy. It is a compile-time requirement and participates in overload resolution.
9.3. constexpr, consteval, And compile-time Computation
constexpr allows an expression or function to be evaluated at compile time when used in a constant-expression context; it does not mean every call must execute during compilation. A consteval function requires compile-time evaluation.
Compile-time computation can remove runtime work, but it can also increase compilation cost, diagnostic complexity, and code size. "Compile time" is therefore not automatically synonymous with "faster overall."
9.3.1. constinit and the static initialization order
Constant initialization and dynamic initialization are distinct phases for objects with static storage duration. Dynamic initialization across translation units can create ordering dependencies. C++20 constinit requires constant initialization for a variable with static or thread storage duration, allowing the compiler to reject some hidden dependencies. It does not mean that the variable is immutable and is not interchangeable with const or constexpr.
constinit int accepted = 0;
constexpr int limit = 128;
int main() {
accepted += 1;
return accepted <= limit ? 0 : 1;
}Where mutable global state is necessary, initialization order, concurrent access, and shutdown dependencies require separate review. Function-local static initialization can narrow cross-translation-unit order problems; thread-safe initialization does not, by itself, make later concurrent mutations of the object safe.
9.4. Forwarding References and std::forward
In a deduced template context, a parameter of the form T&& can be a forwarding reference. std::forward preserves whether the original argument was an lvalue or rvalue when passing it to another function.
This is useful in generic factories and wrappers because it can avoid unnecessary copies. It also makes lifetime and overload analysis more subtle. Ownership transfer and preservation of value category are separate concepts.
9.4.1. Value categories, reference collapsing, and temporary lifetimes
C++ expressions are classified using lvalue, xvalue, and prvalue; glvalue encompasses the first two. A named variable of type T&& is nevertheless an lvalue expression. The declared ability to bind to an rvalue and the value category of the expression used at a call site must not be confused.
For a forwarding reference, T can be deduced as U& from an lvalue argument and as U from an rvalue argument. Reference collapsing gives & whenever an & participates, and && only when both references are &&. Thus std::forward<T>(value) preserves the caller's original category even though the named parameter value is an lvalue expression.
#include <type_traits>
#include <utility>
int main() {
int n = 4;
static_assert(std::is_lvalue_reference_v<decltype((n))>);
static_assert(std::is_rvalue_reference_v<decltype(std::move(n))>);
return 0;
}A reference bound to a temporary does not always extend the temporary's lifetime. Returning a reference to a local or expired temporary produces a dangling reference. Neither std::forward nor std::move extends object lifetime: both affect expression categories, not storage duration.
9.5. Template Visibility and Binary Boundaries
A template definition normally must be visible where instantiation occurs, which is why templates are commonly defined in headers. Explicit instantiation can move code generation for selected types into a separate translation unit.
Generic interfaces also have different ABI consequences from ordinary virtual interfaces. Public template types, compile dependencies, and binary compatibility must be considered together when a library boundary is designed.
9.6. Standard Status and Draft Features
As of September 2026, the published C++ International Standard is ISO/IEC 14882:2024. The next revision is in ISO's Draft International Standard (DIS) stage. A feature visible in the draft should therefore not be described as part of the published standard until that status is verified.
9.6.1. Guaranteed copy elision versus NRVO since C++17
Since C++17, certain prvalue initialization and return contexts need not materialize an intermediate temporary; the result is constructed directly in its destination. This language guarantee differs from named return value optimization (NRVO) when returning a named local variable. NRVO is still an optimization permitted rather than universally required by the language.
#include <memory>
struct Resource {
std::unique_ptr<int> value = std::make_unique<int>(5);
Resource() = default;
Resource(const Resource&) = delete;
Resource(Resource&&) = delete;
};
Resource create() {
return Resource{};
}
int main() {
Resource resource = create();
return *resource.value == 5 ? 0 : 1;
}This example is valid since C++17 despite both copy and move constructors being deleted. By contrast, Resource result; return result; cannot rely on NRVO as a mandatory guarantee. Writing return std::move(result) can also prevent NRVO. The cost of returning values should be assessed using the guarantees of the selected language standard and the code emitted by the compiler.
Unit 10: Concurrency and the C++ Memory Model
C++ concurrency is not only a thread-creation API. Correctness depends on object lifetime, data races, visibility, synchronization, and shutdown behavior.
10.1. Concurrency versus Parallelism
Concurrency means multiple activities can make progress over time. Parallelism means they execute simultaneously on multiple execution resources. A single-core system can run concurrent software without true parallel execution.
The distinction matters for performance. More threads do not automatically increase throughput; contention, cache effects, and scheduler overhead can dominate.
10.2. Data Races and Undefined Behavior
If two threads access the same memory location concurrently, at least one access writes, and there is no suitable synchronization between them, the program has a data race. In the C++ memory model, a data race generally results in undefined behavior.
volatile does not solve this problem. It can constrain certain compiler optimizations around observable accesses, but it does not provide inter-thread atomicity or establish a happens-before relation.
10.3. Mutexes and RAII
A mutex protects invariants over shared state. Binding lock ownership to object lifetime prevents forgotten unlock operations during exceptions or early returns:
std::mutex m;
void update() {
std::lock_guard<std::mutex> lock(m);
// protected state
}When multiple mutexes are acquired together, lock ordering and deadlock risk must be considered. std::scoped_lock is designed to manage several mutexes as one scope-bound operation.
10.4. Condition Variables
A condition variable allows a thread to wait for a state transition. Because wakeups may be spurious and the state can change before the waiter resumes, the predicate must be checked again:
cv.wait(lock, [&] { return ready; });A notification is not a durable message by itself. The shared state and the condition variable form one protocol.
10.5. Atomics and Memory Ordering
std::atomic<T> provides race-free atomic operations for supported types. The default ordering is sequential consistency (memory_order_seq_cst), which provides the strongest and usually simplest mental model.
Acquire/release and relaxed orderings offer weaker guarantees and can reduce synchronization cost in carefully designed low-level code. Weakening ordering without a demonstrated need can turn a performance experiment into a correctness bug.
10.6. The happens-before Relation
It is not enough for one thread to have physically executed a write "earlier." Reliable visibility requires a happens-before relation defined by the language.
Mutex unlock/lock pairs, suitable release/acquire atomic operations, and thread-lifecycle operations can establish that relation. Hardware cache coherence alone does not prove that a C++ program is data-race free.
10.7. Cancellation, Shutdown, and Lifetime
Long-lived concurrency requires a shutdown protocol as well as startup logic. std::jthread provides scope-bound joining and integrates with std::stop_token for cooperative cancellation.
A reliable shutdown design answers: who requests termination, how blocked threads wake, what happens to queued work, in what order shared objects are destroyed, and how joining remains guaranteed after errors.
10.8. Performance: Contention, False Sharing, and Determinism
Contention on one mutex limits scalability. Independent atomic variables can still interact when they share a cache line, producing false sharing and unnecessary cache-coherence traffic.
Performance analysis should therefore include queue length, lock wait time, cache misses, context switches, and tail latency in addition to CPU utilization. In low-latency systems, p95/p99 and worst-case behavior can matter as much as average throughput.
10.9. Object Lifetime, Ownership, and Concurrency
Correct multicore programming is more than the absence of a data race. In C++ concurrent code, observable correctness such as linearizability, the progress properties provided by mutexes or atomics, contention cost, and acquire/release visibility under the C++ memory model are not interchangeable; a correct data structure has to justify each dimension separately. Linearizability provides a strong correctness model, while lock-free and wait-free progress classes show why latency cannot be explained merely by counting mutexes.
- Design shared-object lifetime together with thread safety.
- Prove that invariants survive changes in lock granularity.
- Evaluate concurrent containers by progress and tail latency as well as throughput.
10.9.1. future, promise, and ownership of task results
std::promise<T> represents the producer side of a shared state containing either a result or an exception; std::future<T> is a one-time consumer interface. future::get() may block, consumes the result, and rethrows an exception stored by the producer. std::packaged_task associates a callable task with a result observable through a future.
#include <future>
int compute(const int value) {
return value * value;
}
int main() {
auto result = std::async(std::launch::async, compute, 12);
return result.get() == 144 ? 0 : 1;
}Explicit std::launch::async requests asynchronous execution; default launch-policy selection may also permit deferred execution. In certain cases, destruction of a future produced by async can wait for an unfinished task. Future lifetime and task completion therefore belong in the latency budget. A future is neither an unbounded task queue nor an automatic backpressure mechanism; admission and capacity policies must be designed separately.
10.9.2. Coroutine frames and suspended-operation lifetimes
A C++20 coroutine is a suspendable function transformation, not a thread. Some local variables and execution state reside in a coroutine frame; co_await follows the awaitable object's protocol to manage suspension. Suspension does not launch a thread. The executor or event loop responsible for resumption depends on the library's contract.
The coroutine handle must have a lifecycle consistent with the frame and externally owned resources. Resuming after the frame has been destroyed risks invalid access; never destroying an abandoned frame can leak resources. Cancellation must release sockets, buffers, and synchronization resources according to the coroutine type's promise_type, suspension, and finalization behavior. A view into a zero-copy audio or video buffer becomes unsafe if its producer reuses that buffer while the consumer coroutine is suspended.
References
- Brendan Gregg. Systems Performance: Enterprise and the Cloud. Pearson Education, 2014.
- ISO/IEC. ISO/IEC 14882:2014 Information Technology — Programming Languages — C++. International Organization for Standardization, 2014. https://www.iso.org/standard/64029.html
- ISO/IEC. ISO/IEC 14882:2024 Programming Languages — C++. International Organization for Standardization, 2024. https://www.iso.org/standard/83626.html
- ISO/IEC. ISO/IEC DIS 14882 Programming Languages — C++. Draft International Standard, 2026. https://www.iso.org/standard/91179.html
- Maurice Herlihy; Nir Shavit. The Art of Multiprocessor Programming. Morgan Kaufmann, 2008.
- Meyers, S. Effective Modern C++. O'Reilly Media, 2014.
- Stroustrup, B. A Tour of C++. Addison-Wesley, 2013.
- Stroustrup, B. The C++ Programming Language. Addison-Wesley, 2013.