Skip to content
Object_Oriented_Programing

Object-Oriented Programing

Warning These notes are written primarily for my own study needs. They may not be comprehensive or suitable for everyone, so feel free to browse selectively. Acknowledgments Part of these notes draws inspiration from the excellent Object-Oriented Programing notes written by NoughtQ. I am also grateful to Professor Yuchi Huo for his teaching and guidance in the course, which helped shape the understanding reflected in these notes.

Chapter 1 Fundamentals

#include <iostream>
using namespace std;

int main() {
int number;
cout << "Enter a decimal number: ";
cin >> number;
cout << "The number you entered is " << number << endl;
return 0;
}

1.1 Type System

C++ is a statically typed language: once a variable’s type is declared, it cannot be changed.
It is recommended to use explicit type conversion operators, such as static_castdynamic_cast``、reinterpret_castconst_cast

Type Alias with using

using Zeros = std::pair<double, double>;
using Solution = std::pair<bool, Zeros>;
Solution solveQuadratic(double a, double b, double c);

Type Deduction with auto

auto result = solveQuadratic(a, b, c);

Do not specify the type, let the compiler infer the specific type.But we need to explicitly specify the type of the return value.

std::optional

std::optional<T> is a template class that can wrap a value of type T that may or may not exist.

  • Contains a value: Holds a valid value of type T

  • Does not contain a value: Holds no value, in which case it is std::nullopt

  • has_value() (or operator bool()): Checks whether it contains a value

Namespace Divide variables or functions from different modules into multiple groups to avoid conflicts with the same name.
When using these built-in types, we must add the prefix `std::`

1.2 Structs

#include <utility>

struct zjuIDCard {
string name;
string type;
int idNumber;

void display() {
cout << "Name: " << name << "\nType: " << type
<< "\nID: " << idNumber << endl;
}
};

zjuIDCard myCard = {"Rinic", "Student", 3240100000};

// For structures with only two fields, we can use the built-in std::pair type in C++ as a replacement.
std::pair<std::string, int> dozen { "Eggs", 12 };
std::string item = dozen.first; // "Eggs"
int quantity = dozen.second; // 12

1.3 Initialization

#include <iostream>

std::tuple<std::string, std::string, std::string> getClassInfo() {
std::string className = "CS106L";
std::string buildingName = "Thornton 110";
std::string language = "C++";
return {className, buildingName, language};
}

int main()
{
// Direct Initialization
// Attempt to implicitly convert the initialization value to the specified type
int numOne = 12;
int numTwo(12);

// Uniform Initialization
// Type checking is performed and type conversion is not supported
int numThree{12};

// Structured Binding
auto [className, buildingName, language] = getClassInfo();
}

Tips: Try to delay the timing of variable definitions as much as possible.

1.4 Strings

Three methods of initialization, similar to the previous content.

string str = "Hello";
string str("Hello");
string str{"Hello"};

Read an entire line of string.
Before using getline, you should first use cin.get() to read the newline character (the function of this is to read a single character).

cin.get();
getline(cin, line_var);

Get length

int len = s.length();

Constructor:
Take the first len characters to construct a string.

string(const char *cp, int len);

From s2, construct a new string starting from position pos to the end.

string(const string& s2, int pos);

Construct a new string of length len from s2 starting at position pos to the end.

string(const string& s2, int pos, int len);

Get substring

substr(int pos, int len);

Change the string
Insert string s at position pos.

insert(size_t pos, const string& s);

Starting from position pos, delete len characters (npos defaults to deleting until the end).

erase(size_t pos = 0, size_t len = npos);

Append a string at the end.

append(const string& str);

Starting from pos, replace len characters with str.

replace(size_t pos, size_t len, const string& str);

Find the string
In the current string, starting from position pos, find the first occurrence of substring str.
If found, return the position (index); if not found, return npos

size_t find(const string& str, size_t pos = 0) const;

 

When writing C++ code, it is highly recommended to use nullptr to represent NULL when using pointers.

1.5 Reference

We can understand a reference as another name, or alias, created for an already existing memory object.

Pass by Reference

The modification of n by squareN directly acts on num.

#include <iostream>
#include <math.h>

void squareN(int& n)
{
n = std::pow(n, 2);
}

int main() {
int num = 2;
squareN(num);
std::cout << num << std::endl;
return 0;
}

Structured Binding and Reference

When using structured bindings in a for loop to unpack elements such as std::pair or std::tuple, it is important to consider: do we want to modify the elements in the original container?

The ‘&’ in the first line binds to a copy of the element, while the ‘&’ in the third line binds to the element itself.

void shift_correct(std::vector<std::pair<int, int>>& nums) 
{
for (auto& pair_ref : nums)
{
pair_ref.first++;
pair_ref.second++;
}
}

Left Value Reference

L-Value:

  • Refers to objects that persist after expression evaluation and have identifiable memory locations

  • Can appear on the left side of the assignment operator (=) (as the assignment target) or on the right side (as the source of the value)

R-Value:

  • Typically refers to temporary values or literal constants that do not persist after expression evaluation

  • Usually can only appear on the right side of the assignment operator

Rules for Lvalue References:

  • Must be initialized

  • Binding cannot be changed

  • The target of the reference must have a definite memory location

int squareN(int& num) {
return std::pow(num, 2);
}

int main()
{
auto fourAgain = squareN(2); // error!
}

Since a reference itself is not an object whose address can be obtained:

  1. Conceptually, there is no direct “reference to a reference” type; references “collapse.”
  1. There cannot be pointers to references.
  1. There cannot be arrays of references, but there can be “references to arrays.”

Right Value Reference

Rvalue references, denoted by T&&, can bind to rvalues (temporary objects) that are about to be destroyed, thereby allowing us to “steal” the resources of these temporary objects.

// Correct: The result of x * 2 is an rvalue (temporary expression result)
int&& rr1 = x * 2;

// The role of `std::move`: Unconditionally converts an lvalue to an rvalue reference type, commonly used to "steal" resources.
// Note: At this point, the state of x may become uncertain, and its original value should no longer be relied upon.
int&& rr2 = std::move(x);

// Once an rvalue reference is initialized and has a name (such as rr1, rr2), this named rvalue reference itself becomes an lvalue! Therefore, it can be assigned a value.
rr1 = 100;
int y = rr1 + 2;

1.6 Dynamically Allocated Memory

new:Allocate memory space for the runtime program and call one or more constructors on that memory

int *p = new int(2);
int *a = new int[10];

delete:First, call one or more destructors in the memory space, then release the memory.

delete p;
delete[] a;
int argc, char **argv `argc` = argument count, `argv` = argument vector;
`char **argv` means "pointer to a pointer to a character", and `int atoi(argv[1])` is used to convert a string to an integer.

Chapter 2 STL Containers

2.1 Containers

Vector

#include <iostream>
#include <vector>

int main()
{
// constructors
vector<int> v; // Create an empty vector
vector<float> v(n); // The vector contains n zeros
vector<string> v(n, k); // The vector contains n k's

// Get size
V.size(); // Current number of items in the container
V.empty(); // Whether it is empty, faster compared to .size()

// Element Access
V.at(index); // Boundary checks will be performed
V.front(); V.back(); // First item & Last item

// Add / Delete / Search
V.push_back(e); // Add elements at the end
V.pop_back(); // Remove the last element, but do not return that element
V.insert(pos, e); // where pos is the iterator variable
V.clear(); // Clear all elements within the vector
find(first, last, item); // where first and last are iterator variables, returning an iterator located between first and last; if not found, it returns last

while (cin >> i) v.push_back(i);
}

Lists

// Essentially, it is a doubly linked list.

x.push_back(item);
x.push_front(item);
x.pop_back();
x.pop_front();
x.erase(pos1, pos2); // Delete all elements within the interval [pos1, pos2)
x.count();
x.reverse();
x.resize(); // Capacity adjustment
Choose the appropriate sequential container Usually use vector;
If the program has many small elements and has high space requirements, do not use list;
If the program requires random access to elements, then use vector or deque;
If the program needs to insert elements in the middle of the container, then use list; if the program only needs to insert elements at the beginning and end, without needing to insert in the middle, then use deque.

Maps

Used to store elements consisting of keys and their mapped values, arranged in a specific order.

The elements stored in std::map<K, V> are of type std::pair<const K, V>.

std::map<std::string, int> m 
{
{ "Chris", 2 },
{ "Keith", 14 },
{ "Nick", 51 },
{ "Sean", 35 },
};

// Insert Item
m.insert({k, v});
m.insert(p); // p is of type std::pair<const K, V>.

// Update Value
m[k] = v;

// Get Value
auto v = m[k]; // Accessing a non-existent key will create that key with a default initialized value.
auto v = m.at(k); // Accessing a non-existent key will result in an error.

// Remove Item
m.erase(k);

// Check Item
m.count(k);
m.contain(k); // The return value is of type bool
m.empty();

// Clear Items
m.clear();

// Loop traversal
std::map<std::string, int> map;
for (auto kv : map)
{
// kv is a std::pair<const std::string, int>
std::string key = kv.first;
int value = kv.second;
}
// Or use structured binding
for (const auto& [key, value] : map)
{
// key has type const std::string&
// value has type const int&
}

map requires that keys must support the operator <

Think of a set as a map without values; additionally, a set performs deduplication.

2.2 Iterators

auto it = c.begin();
auto that = c.end(); // Iterator pointing to the position after the last element in the container
++it; // Forward iterator
--it; // Used by bidirectional iterator
auto elem = *it; // Dereferencing an iterator
if (it == c.end()) // Equality comparison

// for loop
for (const auto& elem : c)
{
// loop statements
}

auto elem = *it; // Read element
*it = elem; // Write element

++it and it++ (where it is an iterator)
The former is more efficient, because it returns a reference to the same object (updated in place), while the latter returns a copy of the old value.

It is not difficult to see that the interface of an iterator also exists in pointers—we can think of iterators as a special type of pointer that operates exclusively on containers.


Chapter 3 Classes and Objects

3.1 Declaration and Definition

A basic class should include:

  • Constructor
  • Private member functions / variables
  • Public member functions (user-facing interface)
  • Destructor

The keyword this refers to a pointer to the object itself, and is frequently used in the implementation of member functions.

special member functions (SMF): code is automatically generated when they are called

  • Default constructor: T()
  • Destructor: ~T()
  • Copy constructor: T(const T&)
  • Copy assignment operator: T& operator=(const T&)
  • Move constructor: T(T&&)
  • Move assignment operator: T& operator=(T&&)

If you want all parameters of a function (including the hidden this in member functions) to support type conversion, then the function must be a non-member function.

Header Files and Source Files

.h header files are used to define interfaces: Typically include: function prototypes, class declarations, type definitions, macros, constants, external variables.

  • #include "xx.h": First searches the current directory, then looks in system directories
  • #include <xx.h> / #include <xx>: Directly searches system directories
// To prevent the header file content from being included multiple times

#ifndef HEADER_FLAG
#define HEADER_FLAG
// Type declaration here...
#endif // HEADER_FLAG

.cpp source files generally implement functions or classes

  • The compiler looks at only one .cpp file at a time and compiles it into a .obj file
  • The linker links all .obj files into an executable file

Access Control

Access specifiers provided by C++:

  • public: Can be accessed by any object
  • protected: Can only be accessed within the class and its derived classes
  • private: Can only be accessed within the class

Classes default to private, while structs default to public

class zjuID 
{
private:
string name;
int idNumber;
public:
void setName(string n);
void setIdNumber(int id);
void display();
};

We should declare all member variables as private

Initialization and Clean-Up

constructor (ctor): The name is the same as the class name, and there is no need to specify the return type.

// These two types of constructors can coexist simultaneously.
// `explicit` indicates that this constructor does not allow implicit type conversion

// Type 1: Parameterized Constructor
// impose additional restrictions
explicit zjuID::zjuID(std::string name, int idNumber)
{
this->name = name;
if (idNumber > 0) this->idNumber = idNumber;
}

// member initialization list
explicit zjuID::zjuID(std::string n, int id): name{n}, idNumber{id} {}

// Type 2: Default constructor (no parameters)
explicit zjuID::zjuID()
{
name = "Rinic";
idNumber = 1989;
}

destructor (dtor): Function name = ~ + class name, no parameters required, and similarly no return type

  • The order of destruction is the reverse of the order of construction.

Getters and Setters

// getter: Functions for reading member variables
std::string zjuID::getName()
{
return this->name;
}

// setter: Function to modify member variables
void zjuID::setIdNumber(int idNumber)
{
if (idNumber >= 0) this->idNumber = idNumber;
}

3.2 Friends

Friend declaration within a class allows external classes or functions to access the private or protected members of that class.

// Friend function
class Box
{
private:
int length;
public:
Box(int l) : length(l) {}
friend void printLength(const Box& b);
};

void printLength(const Box& b)
{
std::cout << "Length is: " << b.length << std::endl;
// correct
}

// Friend class
class Car
{
private:
int speed;
public:
Car() : speed(200) {}
friend class Engine;
};

class Engine
{
public:
void boost(Car& c)
{
c.speed += 100;
// correct
}
};

3.3 Scope and Lifetime

Local Objects

fields: Variables defined inside a class but outside of functions.

parameters: Variables in the declaration of a function or method, used to receive values passed from outside.

local variables: Variables defined inside a function or code block, used for temporary calculations or intermediate states.

Global Objects

The constructor of global objects needs to be called before entering the main() function, and the destructor is called when main() exits or exit() is invoked.

Static

non-local static object:

  • Defined in global or namespace scope
  • Declared as static variables within a class or file scope

Static

  • Basic meaning: static storage (persistent), restricted access (hidden)
  • Usage scenarios
    • Global variables: scope limited to the current file
    • Local variables: lifetime spans the entire program runtime
    • Member variables/functions: belong to the class itself, declared inside the class, defined outside the class.It does not belong to any object, so the this pointer cannot be used on it.
      class CostEstimate 
      {
      private:
      static const double cost;
      // ...
      };
      const double CostEstimate::cost = 1.35;

3.4 Constants (Cannot be modified)

Modify Variables

  • The compiler attempts to avoid creating memory space for const variables, but if the extern keyword is used in front, it forces the compiler to allocate memory space.
// Solution for initializing class member variables in the constructor
class HasArray
{
enum { size = 100 }; // 1. anonymous enumeration
static const int size = 100; // 2. use static keyword
int array[size];
};

Modify Pointers

  • const int* p1 / int const* p1: The value pointed to by p1 cannot be modified, but p1 itself can be modified.
  • int* const p2: p2 itself cannot be modified, but the value it points to can be modified.

Modify Function Parameters

  • void foo(const int& x): Prevents the function from modifying the passed parameter.

Modify Return Values

  • const std::string& getStr(): If returning a pointer or reference, const can be added to prevent unintended modifications.

Modify member functions

class A 
{
public:
int getVal() const
{
return val;
}
private:
int val;
};
  • At this point, the member function cannot modify member variables and can only call other const member functions.
  • This const modifies the this pointer.
  • Function overloading: You can create two versions of the same member function—one decorated with const and one without. First, define a const version of the function, and then have the non-const version call this function.
    template <typename T>
    const T& Vector<T>::findElement(const T& value) const
    {
    for (size_t i = 0; i < logical_size; i++)
    {
    if (elems[i] == value) return elems[i];
    }
    throw std::out_of_range("Element not found");
    }

    template <typename T>
    T& Vector<T>::findElement(const T& value)
    {
    return const_cast<T&>(static_cast<const Vector<T>&>(*this).findElement(value));
    }

3.5 Composition

A class uses an object of another class as its member variable, organizing and reusing its code, thereby “possessing” the functionality of the other class.

Method 1: Value Composition / Embedded Objects

class A 
{
int x;
public:
A(int val) : x(val) {}
};

class B
{
A a1, a2;
public:
B() : a1(10), a2(20) {} // Initialize sub-objects using list initialization
};

Method 2: Reference / Pointer Combination

3.6 Inheritance

Create a new class (called a derived class or subclass) based on an existing class (called a base class or superclass). The derived class automatically inherits the attributes and methods of the base class.

// Base class
class Animal
{
public:
void speak()
{
cout << "Animal makes a sound" << endl;
}
};

// Derived class
class Dog : public Animal
{
public:
void bark()
{
cout << "Dog barks" << endl;
}
};

int main() {
Dog d;

d.speak(); // Inherited from Animal
d.bark(); // User-defined functions

return 0;
}

The content that can be inherited includes:

  • Member variables
  • public or protected member functions
  • Static members: Their scope remains within the class.

Content that cannot be directly inherited includes:

  • Constructors
  • Destructors
  • Assignment operators

Assuming class B is a derived class of class A, then:

Inheritance Type public protected private
public A Public in B Protected in B Hidden
private A Private in B Private in B Hidden
protected A Protected in B Protected in B Hidden

3.7 Polymorphism

Its core idea is “one interface, multiple implementations.”

Subclassing and Subtyping

public inheritance: establishes an “is-a” relationship and allows implicit type conversion from derived class to base class

Liskov Substitution Principle (LSP): derived classes must also be behaviorally compatible with the base class

  • A derived class cannot strengthen the preconditions of a base class method (i.e., impose stricter input requirements)

  • A derived class cannot weaken the postconditions of a base class method (i.e., provide fewer guarantees on output)

  • A derived class must maintain the invariants of the base class

Upcasting

Convert a pointer or reference of a derived class to a pointer or reference of a base class.

class Animal {};
class Dog : public Animal {};

Dog myDog;
Animal* pAnimal = &myDog; // Upcasting
Animal& rAnimal = myDog; // Upcasting

Only virtual allows override.

Dynamic Binding (Late Binding / Runtime Binding)

The mechanism that determines which version of a member function to call based on the actual object type pointed to by a pointer or reference during program execution.

The conditions for this to occur are:

  • The call must be made through a pointer or reference to a base class.
  • The called member function must be declared as virtual in the base class.

static binding: During compilation, the specific function to be called has already been determined based on the declared type of the pointer or reference.


Virtual functions allow subclasses to override the behavior of the base class and determine which version of the function to call at runtime based on the actual type of the object.

class Shape {
public:
virtual void Draw() { cout << "Drawing a generic shape" << endl; }
virtual ~Shape() {}
};

If a derived class object is deleted through a base class pointer, and the base class destructor is not virtual, then only the base class destructor will be called, resulting in a “partially destroyed” object.

Therefore, if a class is intended to be used as a base class and derived class objects may later be deleted through a base class pointer, its destructor must be declared as virtual.

class Base {
public:
virtual ~Base() = default;
};

Never call virtual functions in constructors or destructors.
Never redefine inherited default parameter values.

Alternatives / Enhancements for Virtual Functions

  1. Non-Virtual Interface (NVI) method: It is a template method
  2. Strategy pattern: The core idea is to decouple the algorithm (strategy) from the context that uses it and make it interchangeable.

The purpose of overriding is to provide a function in a subclass that has exactly the same signature as a virtual function in the base class, in order to implement its own specific behavior.

  • Function signature: The name, parameter list, and const modifier must be strictly consistent
  • The base class function must be virtual, and it is recommended to also write virtual when overriding in a derived class
class Base {
public:
virtual void display(int x) const;
};

class Derived : public Base {
public:
void display(int x) const override;
// void display(int x) override; // Compilation error! const mismatch
// void display(double x) const override; // Compilation error! Parameter type mismatch
};

Covariant return types: If a base class virtual function returns Base* or Base&, a derived class can override it to return Derived* or Derived& (where Derived inherits from Base).

class Document {
public:
virtual Document* clone() const = 0;
virtual ~Document() = default;
};
class TextDocument : public Document {
public:
TextDocument* clone() const override { return new TextDocument(*this); }
};

The access level of a derived class’s overridden function cannot be more restrictive than that of the base class.

name hiding:

class Base {
public:
virtual void func(int);
void func(double); // overloading
};
class Derived : public Base {
public:
// Only rewrite the int version
void func(int) override;
// If there is no next line, Base::func(double) will be hidden
using Base::func;// Bring Base's func(double) into Derived's scope
};

Abstract Classes

A class that contains at least one pure virtual function

Pure virtual function: Adding = 0 at the end of the declaration indicates that the function has no implementation and must be provided by a derived class

// Shape is an abstract class
class Shape {
public:
// Pure virtual function: defines an interface, forcing subclasses to implement it
virtual double calculateArea() const = 0;

// Abstract classes can have ordinary member functions and data members
void setColor(const std::string& c) { color = c; }

// Even abstract classes need virtual destructors if they might be inherited!
virtual ~Shape() = default;
private:
std::string color;
};

Its function is to provide a unified interface specification, forcing subclasses to implement it.

3.8 Inheritance of Interface and Implementation

Inheritance can be divided into interface inheritance and implementation inheritance.

Type What You Inherit Can It Be Changed? Essential Meaning
Pure Virtual Function Only the “rule” Must implement yourself Only defines the interface
Virtual Function Rule + default implementation Can be changed Has default behavior
Non-Virtual Function Rule + enforced implementation Cannot be changed No variation allowed

Multiple inheritance refers to a mechanism where a class can inherit from multiple base classes simultaneously.

  • Implementing multiple interfaces: A class can satisfy multiple different contracts at the same time. When encountering members with the same name inherited from different base classes, we need to manually specify which base class’s member to use.
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};

class Serializable {
public:
virtual void serialize(std::ostream& os) const = 0;
virtual ~Serializable() = default;
};

// Button implements both the Clickable and Serializable interfaces
class Button : public Clickable, public Serializable { /* ... implement interface ... */ };
  • Mixed inheritance: inheriting from a concrete base class while implementing one or more interfaces.

Diamond Problem: When a class inherits from the same indirect base class through different paths, multiple copies of the base class’s members exist in the derived class, leading to ambiguity in access.

The solution to this problem is to use virtual inheritance, i.e., applying the virtual keyword when the intermediate classes inherit from the common base class.

class Top { public: int value; };
// Using virtual inheritance
class Left : virtual public Top {};
class Right : virtual public Top {};
class Bottom : public Left, public Right {};

Bottom b;
b.value = 10;

Design Principles for Inheritance and Polymorphism

  1. Prefer composition over inheritance
  2. Use multiple inheritance with caution
  3. Beware of default parameters in virtual functions
  4. Do not call virtual functions in constructors/destructors

Chapter 4 Functions

4.1 Parameter Passing

Generally, functions use pass-by-value. In this case, function parameters are initialized with copies of the actual arguments, and these copies are generated through the object’s copy constructor, which makes pass-by-value relatively costly.

bool validateStudent(Student s);

We can avoid these copy constructors and destructors by using pass by reference-to-const, which passes the actual argument itself without creating a copy. Additionally, the const modifier indicates that the function cannot modify the parameter, so there is no need to worry about altering the actual argument.

bool validateStudent(const Student& s);

Passing by constant reference can also avoid the slicing problem: when a derived class object is passed by value to a base class object, the base class’s copy constructor is called, which causes the derived class-specific parts to be discarded, retaining only the base class components.

For built-in types (such as int, double, etc.) or small user-defined classes, passing by value may be more efficient. After all, the underlying implementation of references is pointers, and pointers themselves also occupy a significant amount of space.

4.2 Function Return

Functions have two ways to create new objects: on the stack or on the heap.

  • Local variables are created on the stack. Before a function exits, all local variables created inside the function will be destroyed.

  • Objects created with new are based on the heap.

  • Do not attempt to return a local static object. Since such an object exists throughout the entire program’s lifecycle, if multiple places need to use this object, they are actually pointing to the same object.

Handles: Things like pointers and references that can represent the internal data of an object refer to any identifier or reference that can be used to access or manipulate another entity (such as an object, resource, file, etc.).

In short: Unless you are absolutely certain, a function should return a complete object rather than a handle to that object.

4.3 Overloaded Functions

Function overloading: It allows us to define multiple functions with the same name, as long as their parameter lists differ in type, number, or order. The compiler automatically selects the best matching function version based on the arguments provided at the call, and can perform appropriate automatic type conversions.

Destructors must not be overloaded!

Delegating Constructors

When designing a class, it is sometimes encountered that multiple constructors contain a large amount of repetitive initialization code, which is generally considered a bad practice known as code duplication.

C++11 introduced delegating constructors to address this issue: a constructor can call another constructor of the same class in its member initializer list, thereby “delegating” common initialization tasks to it. This can form a delegation chain.

class class_c {
public:
int max;
int min;
int middle;

class_c(int my_max)
{
max = my_max > 0 ? my_max : 10;
min = 1;
middle = (max + min) / 2;
std::cout << "Delegated to: class_c(int)" << std::endl;
}

// This constructor delegates to class_c(int).
class_c(int my_max, int my_min) : class_c(my_max)
{
// max has already been initialized by class_c(my_max)
min = my_min > 0 && my_min < max ? my_min : 1;
middle = (this->max + this->min) / 2; // Use this-> to explicitly access members
std::cout << "Delegated to: class_c(int, int)" << std::endl;
}

// This constructor delegates to class_c(int, int).
class_c(int my_max, int my_min, int my_middle) : class_c(my_max, my_min)
{
// max and min have been initialized by class_c(my_max, my_min)
middle = my_middle < this->max && my_middle > this->min ? my_middle : (this->max + this->min) / 2;
std::cout << "Called: class_c(int, int, int)" << std::endl;
}
};

4.4 Default Arguments

Default arguments: Values specified for parameters when declaring a function

When defining a function’s parameter list, default parameters must be specified from right to left. That is, if a parameter has a default value, all parameters to its right must also have default values.

int harpo(int n, int m = 4, int j = 5);

// int chico(int n, int m = 6, int j); // Error! j has no default value, but m does

// Call example
int beeps;
beeps = harpo(2); // Equivalent to harpo(2, 4, 5);
beeps = harpo(1, 8); // Equivalent to harpo(1, 8, 5);
beeps = harpo(8, 7, 6); // All parameters are provided, no default values are used.

Default parameters should be specified in the function declaration (prototype). If a function has both a declaration and a definition (in different locations), the default parameters should not be repeated in the definition.
The best practice is to place function declarations with default parameters in header files.

// Example_MyClass.h
class MyClass {
public:
void func(int a, int b = 10);
};
// Example_MyClass.cpp
void MyClass::func(int a, int b) {
// ... Implementation ...
}

4.5 Inline Functions

Problems with regular functions: overhead of function calls

  • Pushing arguments onto the stack
  • Pushing the return address onto the stack
  • Jumping to the function’s code location
  • (After the function executes) preparing the return value
  • Restoring the stack, popping the previously pushed content, and returning control to the caller

Inline functions: directly “embed” or “expand” the function’s code body at the call site, instead of performing a regular function call jump. This is similar to text substitution in preprocessor macros, but inline functions are real functions with advantages such as type checking.

By adding the inline keyword before the function declaration and definition, you suggest (note: not “force”) the compiler to treat it as an inline function:

inline int plusOne(int x);
inline int plusOne(int x) { return ++x; };

The definition of an inline function is typically placed in a header file. This way, any source file that #includes the header file can obtain the function definition, allowing the compiler to perform inlining.

Even without explicitly using the inline keyword, any member function defined inside a class declaration may be considered an inline function.

Advantages:

  • Reduces the overhead of function calls, potentially improving execution speed, especially for small, frequently called functions.

  • Safer than C macros, as macros do not perform parameter type checking and are prone to errors, whereas inline functions do perform type checking.

Disadvantages:

  • If the function body is large, inlining can cause code bloat at the call site, increasing the size of the final executable file (a typical space-for-time trade-off).

  • Since inline functions are expanded at compile time, debuggers may not be able to set breakpoints or step through the inline function’s code as they would with regular functions.

It is important to note that the inline keyword is merely a suggestion to the compiler; the compiler may not necessarily adopt this suggestion and may decide whether to actually inline a function based on its own optimization strategies.

Suitable scenarios: The best candidates for inlining are functions with very small bodies (e.g., only one or two lines of code) that are called frequently, such as simple getter and setter functions.

Less suitable scenarios:

  • Functions with large bodies (e.g., a rough standard of over 20 lines of code, though this is not a hard rule): Inlining them may significantly increase code size, while performance gains may be negligible or even worsen due to reduced code cache efficiency.

  • Recursive functions.

  • Functions containing complex logic (e.g., loops, extensive branching).

  • Functions that include virtual function calls.

Warning The following content is supplementary and is not part of the course exam points.

4.6 Lambda

Lambda functions are a powerful feature introduced in the C++11 standard, allowing us to define anonymous function objects in code. Lambda functions are particularly suitable for scenarios that require short, one-time-use functions.

[capture_clause](parameters) mutable_specifier exception_specifier -> return_type 
{
// function body
}
  • [capture_clause]: The capture clause defines which variables from the enclosing scope the lambda function can access and how it accesses them (by value or by reference). The specific cases are as follows:

    • []: Does not capture any external variables.

    • [var]: Captures the variable var by value; inside the lambda function, var is a copy, and modifying it does not affect the external var.

    • [&var]: Captures the variable var by reference; modifying var inside the lambda function affects the external var.

    • [=]: Captures by value all local variables visible at the point of lambda definition (including this if the lambda is defined inside a member function).

    • [&]: Captures by reference all local variables visible at the point of lambda definition (including this if the lambda is defined inside a member function).

    • [this]: Captures the this pointer of the current object by value. Allows access to the class’s member variables and member functions.

    • [&, var]: Captures all variables by reference, but captures var by value.

    • [=, &var]: Captures all variables by value, but captures var by reference.

    • C++14 introduced init-capture, also known as generalized lambda capture, which allows declaring and initializing new variables within the capture clause. These variables are only visible inside the lambda.

[x = std::move(my_large_object)](){ /* ... */ }
[val = compute_value()](){ /* ... */ }
  • (parameters): Parameter list, similar to that of a regular function, defining the parameters accepted by the Lambda function. It can be empty.

  • mutable_specifier: Mutable specifier (optional)

    • By default, variables captured by value are const inside the Lambda function body; if you want to modify the copy of a variable captured by value within the Lambda function, you need to use the mutable keyword.
  • exception_specifier: Exception specifier (optional), used to specify the types of exceptions that the Lambda function may throw.

    • For example, noexcept indicates that the Lambda will not throw any exceptions.
  • -> return_type: Return type (optional), specifying the return type of the Lambda function.

    • In many cases, the compiler can automatically deduce the return type, so it can be omitted.

    • If the Lambda function body contains multiple return statements, or the expression type of the return statement is unclear, or you want to explicitly specify a different return type, you need to explicitly specify the return type.

  • { // function body }: Function body, containing the actual execution code of the Lambda function.

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

int main()
{
// 1. Basic lambda, no capture, no parameters, implicitly returns void
[] { std::cout << "Hello from Lambda!" << std::endl; }(); // Define and immediately invoke

// 2. Lambda with Parameters
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << "Sum: " << add(5, 3) << std::endl; // Output: Sum: 8

// 3. Automatic return type deduction (omitting -> double)
auto multiply = [](double a, double b) {
return a * b;
};
std::cout << "Product: " << multiply(2.5, 4.0) << std::endl; // Output: Product: 10.0

// 4. Example of a Catch Clause
int x = 10;
int y = 20;

// Capture x and y by value
auto capture_by_value = [x, y]() {
// x and y here are copies; modifying them will not affect the external x and y.
std::cout << "Inside by_value: x = " << x << ", y = " << y << std::endl;
// x = 100; // Compilation error because x is const (unless mutable is used)
};
capture_by_value();

// Capture x and y by reference
auto capture_by_reference = [&x, &y]() {
x = 100; // Modifying will affect the external x
y = 200; // Modifying will affect the external y
std::cout << "Inside by_reference: x = " << x << ", y = " << y << std::endl;
};
capture_by_reference();
std::cout << "Outside after by_reference: x = " << x << ", y = " << y << std::endl; // Output: x = 100, y = 200

// Implicitly capture all visible variables by value
int z = 30;
auto capture_all_by_value = [=]() {
std::cout << "Inside all_by_value: x = " << x << ", y = " << y << ", z = " << z << std::endl;
};
capture_all_by_value();

// Implicitly capture all visible variables by reference
int w = 40;
auto capture_all_by_reference = [&]() {
x = 1; y = 2; z = 3; w = 4;
std::cout << "Inside all_by_reference: x = " << x << ", y = " << y << ", z = " << z << ", w = " << w << std::endl;
};
capture_all_by_reference();
std::cout << "Outside after all_by_reference: x = " << x << ", y = " << y << ", z = " << z << ", w = " << w << std::endl;

// 5. mutable keyword
int val = 5;
auto mutable_lambda = [val]() mutable {
val = 10; // Now you can modify a copy of the val captured by value
std::cout << "Inside mutable_lambda, val = " << val << std::endl;
};
mutable_lambda();
std::cout << "Outside mutable_lambda, val = " << val << std::endl; // Output: 5 (the external val remains unchanged)

// 6. Using Lambda in STL Algorithms
std::vector<int> numbers = {1, 5, 2, 8, 3, 7};
int threshold = 4;

// Count the number of elements greater than the threshold.
int count = std::count_if(numbers.begin(), numbers.end(), [threshold](int n) {
return n > threshold;
});
std::cout << "Numbers greater than " << threshold << ": " << count << std::endl; // Output: 3

// Print all elements
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
std::cout << std::endl;

// 7. C++14: Generic Lambdas (using auto as parameter type)
auto generic_add = [](auto a, auto b) {
return a + b;
};
std::cout << "Generic add (int): " << generic_add(10, 20) << std::endl;
std::cout << "Generic add (double): " << generic_add(1.5, 2.5) << std::endl;
std::cout << "Generic add (string): " << generic_add(std::string("Hello, "), std::string("World!")) << std::endl;


// 8. C++14: Init Capture (Generalized Lambda Capture)
std::string message = "Original message";
auto generalized_capture_lambda = [captured_message = std::move(message)]() {
std::cout << "Inside generalized_capture_lambda: " << captured_message << std::endl;
// The message has been removed here and should generally no longer be used (unless reassigned).
};
generalized_capture_lambda();
std::cout << "After generalized_capture_lambda, message: \"" << message << "\"" << std::endl; //The message may be empty or in an undefined state.

return 0;
}

At the underlying level, compilers typically convert a lambda function into an anonymous function object (also known as a closure). This object has an overloaded operator(), whose body is the lambda’s function body. The captured variables become member variables of this anonymous class.

Lambda functions have the following characteristics:

  • Conciseness: For short functions, lambda expressions are more concise than defining a complete function or function object class.

  • Locality: Lambdas can be defined where they are used, making the logic more centralized and the code easier to read.

  • Closure: The capture clause allows lambdas to “remember” the context in which they were created, making them ideal for callbacks and event handling.

  • STL Algorithms: Lambdas are the perfect companion for C++ standard library algorithms, allowing custom operations to be passed easily.

  • Asynchronous Programming: In multithreading and asynchronous tasks, lambdas can conveniently encapsulate blocks of code to be executed.

4.7 Ranges and Views

Conceptually, a range is a single object or entity that encapsulates all the information needed to traverse a sequence of elements. This allows us to refer to and manipulate the entire “sequence” as a whole, rather than separately handling its “beginning” and “end.” In summary: a range is essentially a unified abstraction for any “iterable sequence.”

The <ranges> library implements the concept of a range in the following ways:

  • It defines a series of concepts (such as std::ranges::range, std::ranges::input_range, std::ranges::view, etc.). If a type satisfies the std::ranges::range concept, it is considered a range.

  • It provides free functions (non-member functions) like std::ranges::begin() and std::ranges::end(), which can be used with any type that satisfies the range concept, offering a unified way to obtain the beginning and end of a range.

The following can all be considered ranges:

  • C++ standard library containers

  • C-style arrays

  • Sequences defined by a pair of iterators

  • User-defined types that conform to the range concept

  • New ranges generated by views (to be introduced shortly)

#include <vector>
#include <algorithm> // for std::ranges::sort
#include <ranges> // Core ranges support

std::vector<int> vec = {1, 2, 3};
int arr[] = {4, 5, 6};

std::ranges::sort(vec); // Directly pass the container (range)
std::ranges::sort(arr); // Directly pass an array (range)

Views are a core component of the <ranges> library, representing a non-owning, lazily evaluated operation or transformation on an underlying sequence.

  • Non-owning: Views themselves do not store element data; they are merely a way of “looking at” or “referencing” the underlying data.

  • Lazily evaluated: Operations on views are typically not executed immediately; they are only evaluated when the result is actually needed (e.g., when iterating over the view or passing it to an algorithm that consumes data).

  • Lightweight: View objects themselves are usually small, with low overhead for creation and copying. They typically only store references/iterators to the underlying range and some configuration parameters.

  • Composable: This is the most powerful feature of views. Multiple view adaptors can be chained together using the pipe operator | to form a complex data processing pipeline while keeping the code clear and readable.

The <ranges> library provides a series of view adaptors, which are function objects that accept one or more ranges and return a new view.

  • std::views::filter: Filters elements based on a given predicate function.

  • std::views::transform: Applies a function to each element in the range and generates a new view containing the results.

  • std::views::take(n): Takes the first n elements from the range.

  • std::views::drop(n): Skips the first n elements from the range.

  • std::views::reverse: Reverses the order of elements in the range.

  • std::views::elements<n>: (For ranges of tuples or class-like structures) Extracts the nth element of each tuple.

  • std::views::keys: (For ranges of associative containers or similar structures) Extracts the keys.

  • std::views::values: (For ranges of associative containers or similar structures) Extracts the values.

  • std::views::iota(start, end): Generates a sequence of integers starting from start up to (but not including) end.

  • std::views::all(range): Converts a range into a view.

  • std::views::counted(iterator, count): Creates a view starting from an iterator, taking count elements.

#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm> // for std::ranges::for_each

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

auto even_numbers = [](int n) { return n % 2 == 0; };
auto square = [](int n) { return n * n; };

// Use the pipe operator | to combine views
auto result_view = numbers
| std::views::filter(even_numbers) // Filter even numbers {2, 4, 6, 8, 10}
| std::views::transform(square) // Square {4, 16, 36, 64, 100}
| std::views::take(3); // Take the first 3. {4, 16, 36}

// At this point, the operations above are lazily defined and no actual computation has been performed.
// When we iterate over result_view, the computation will occur on demand.
std::cout << "Processed numbers: ";
for (int n : result_view) { // Iterate through result_view to trigger computation.
std::cout << n << " ";
}
std::cout << std::endl; // Output: Processed numbers: 4 16 36

// It can also be directly passed to the ranges algorithm.
std::cout << "Processed numbers (using for_each): ";
std::ranges::for_each(numbers
| std::views::filter(even_numbers)
| std::views::transform(square)
| std::views::take(3),
[](int n){ std::cout << n << " "; });
std::cout << std::endl; // Output: Processed numbers (using for_each): 4 16 36

return 0;
}

Chapter 5 Copying and Moving

5.1 Copying

Refers to the process of creating a completely new, independent copy of an object based on an existing object.

Copying occurs when an object is passed by value as a parameter to a function:

// Assume Currency is a class.
void displayCurrency(Currency item) {
// item is a copy of bucks
std::cout << "Value: " << item.dollars() << std::endl;
}

int main() {
Currency bucks(100, 0);
displayCurrency(bucks); // bucks is copied into the parameter item
return 0;
}

In this example, when displayCurrency(bucks) is called, the contents of the bucks object are used to initialize the function parameter item, a process typically carried out by the copy constructor.

Copy Ctors

Copy constructors are a special type of constructor that defines how to initialize a new object from another object of the same class.

className::className(const className& other);

If we do not explicitly provide a copy constructor for a class, the C++ compiler will automatically generate one — this default copy constructor performs a member-wise copy.

For basic data types (such as int, double) and member objects that inherently have good copy semantics (such as std::string), this default behavior is usually correct.

However, if the class contains pointer members, the default copy constructor only copies the value of the pointer (i.e., the address), not the data it points to. This causes the pointer members of the original object and the copied object to point to the same memory area (i.e., shallow copy). In such cases, we need to define our own copy constructor.

When copying occurs:

  • Passing by value: when an object is passed as an argument to a function
  • Returning an object by value: when a function returns an object, the compiler often performs (named) return value optimization to avoid unnecessary copying
  • Object initialization: when one object is used to initialize another object
Person p1("Alice");
Person p2 = p1; // Calling the copy constructor
Person p3(p1); // Explicitly calling the copy constructor

If you want to make a class’s objects non-copyable, you can use the following methods:

  • Declare the copy constructor and copy assignment operator as = delete; (C++11 and later, recommended)

  • Declare them as private and provide no implementation, so any attempt to copy the object will fail at compile time (traditional approach)

When defining a custom copy constructor, remember to copy all member variables and call the appropriate copy constructors of all base classes (if any).

Shallow Copy v.s. Deep Copy

Problems with shallow copy: dangling pointers, double free, unintended modification of shared data

#include <cstring>
#include <iostream>

class Person
{
private:
char* name;

public:
Person(const char* n)
{
if (n)
{
name = new char[std::strlen(n) + 1];
std::strcpy(name, n);
}
else
{
name = new char[1];
name[0] = '\0';
}
std::cout << "Person created: " << (name ? name : "null") << std::endl;
}

~Person()
{
std::cout << "Person destroyed: " << (name ? name : "null") << std::endl;
delete[] name; // Free dynamically allocated memory
}

// Person(const Person& other) : name(other.name) {} // Wrong shallow copy!

// Correct deep copy constructor
Person(const Person& other)
{
if (other.name)
{
name = new char[std::strlen(other.name) + 1];
std::strcpy(name, other.name);
}
else
{
name = new char[1];
name[0] = '\0';
}
std::cout << "Person (deep) copied: " << name << std::endl;
}

// Usually, a copy assignment operator is also needed (see below).
Person& operator=(const Person& other)
{
std::cout << "Person copy assignment for: " << (name ? name : "null") << " from " << (other.name ? other.name : "null") << std::endl;
if (this == &other)
{ // Handling Self-Assignment
return *this;
}
delete[] name; // Release old resources
if (other.name)
{
name = new char[std::strlen(other.name) + 1];
std::strcpy(name, other.name);
}
else
{
name = new char[1];
name[0] = '\0';
}
return *this;
}

void print() const
{
std::cout << "Name: " << (name ? name : "N/A") << std::endl;
}
};

Copy Assignments

Construction: An object is constructed only once at the beginning of its lifecycle.

Person p1("Alice");  // Calling an ordinary constructor
Person p2 = p1; // Calling the copy constructor (initializing p2)

Assignment: An existing object can be assigned multiple times.

Person p3("Bob");
p3 = p1; // Call the copy assignment operator (p3 already exists)

The purpose of copy assignment operators is to completely replace the state of an existing object with the state of another existing object of the same type.

T& T::operator=(const T& other);

When overloading the copy assignment operator, especially when the class manages resources, it is strongly recommended to strictly follow these steps:

  • Handle self-assignment: This is a crucial step to prevent potential errors. If the source object and the target object are the same (obj = obj;), performing resource release and allocation without checking could lead to the object’s resources being incorrectly released, resulting in undefined behavior.
if (this == &other) 
{
return *this;
}
  • Release the resources currently held by the object: Before copying the new state from the other object, the current object (*this) may already be managing some resources. These resources must be properly released to avoid resource leaks (such as memory leaks).
delete[] this->name_;
this->name_ = nullptr;
  • Allocate new resources and copy the source object data: Similar to the copy constructor, if the class manages resources, the assignment operation must also perform a deep copy. Allocate new, independent resources for the current object (*this), and then copy the data from the other object (including the contents of the resources it manages).
if (other.name_) 
{
this->name_ = new char[std::strlen(other.name_) + 1]; // Allocate new memory for this->name_
std::strcpy(this->name_, other.name_); // Copy content
}
else
{
this->name_ = nullptr;
}
// Copy other non-resource-dependent member variables
this->age_ = other.age_;
  • Return a reference to the current object: return *this to support chained assignment.
return *this;

If a copy assignment operator is not explicitly defined, the compiler will automatically generate one, and it also performs memberwise assignment:

  • For members of built-in types, it directly copies the value.
  • For members of class types, it calls the copy assignment operator of that member.
  • For pointer-type members, it only copies the address value of the pointer (shallow copy), without copying the data pointed to by the pointer.

It can be seen that in most cases, the implementation code of the copy constructor and the copy assignment operator is very similar, meaning there is code duplication. However, even so, we should not have the copy assignment operator directly call the copy constructor, nor the other way around, because the former is equivalent to constructing an already existing object, while the latter attempts to assign a value to another object using an object that has not yet been constructed—both of which are meaningless.

The correct approach is to place the duplicated code into a third-party function (typically declared as private), and then have both the copy constructor and the copy assignment operator call this function.

Important Rules

  • Rule of Three (C++03): If a class defines any of the destructor, copy constructor, or copy assignment operator, then typically all three need to be defined, because customizing one of these functions usually indicates that the class manages resources and requires special handling of their lifecycle and copy behavior.

  • Rule of Five (C++11 and later): With the introduction of move semantics, this rule expands to the Rule of Five—if any of the above three is defined, one usually also needs to consider defining the move constructor and move assignment operator, or explicitly delete or default them.

  • Rule of Zero (C++11 and later): If a class does not directly manage any resources but instead manages them through its member objects, then the class typically does not need to define any destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator; the compiler-generated default versions usually work correctly.

delete and default

The delete introduced here is not the one used to release dynamically allocated memory!

After C++11, the delete keyword can also be used to disable the automatic generation or implicit use of a class’s special member functions (SMFs), typically to prevent operations such as copying or assigning objects.

class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // Disable copy constructor
NonCopyable& operator=(const NonCopyable&) = delete; // Disable copy assignment operator
};

The main function of default is to explicitly require the compiler to generate default special member functions.

class AnotherClass {
public:
// ...
AnotherClass(const AnotherClass& other) = default; // Explicitly using the default copy constructor
AnotherClass& operator=(const AnotherClass& other) = default; // Explicitly using the default copy assignment operator
~AnotherClass() = default; // Explicitly using the default destructor
};

std::copy()

std::copy is a generic algorithm in the C++ standard library’s <algorithm> header. It copies elements from one range to another location and can be used with various data structures, such as arrays and other containers.

The simplified function signature is as follows:

template<class InputIt, class OutputIt>
OutputIt copy(InputIt first, InputIt last, OutputIt result);
  • first: Input iterator pointing to the first element to be copied in the source range

  • last: Input iterator pointing to the position one past the last element to be copied in the source range (i.e., the “past-the-end” iterator); this element itself will not be copied

  • result: Output iterator pointing to the position in the target range where the first element will be copied

  • Returns an output iterator pointing to the position one past the last copied element in the target range

The caller must ensure that the memory area pointed to by the target range result is large enough to accommodate all elements in the source range [first, last). std::copy itself does not perform any memory allocation or size checking.

The caller must ensure that the memory area pointed to by the target range result is large enough to accommodate all elements in the source range [first, last). std::copy itself does not perform any memory allocation or size checking.

#include <iostream>
#include <vector>
#include <algorithm> // For std::copy
#include <iterator> // For std::back_inserter

int main() {
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination1(source.size()); // The target container needs to have enough space.

// Copy to a pre-allocated container
std::copy(source.begin(), source.end(), destination1.begin());

for (int val : destination1) {
std::cout << val << " "; // Output: 1 2 3 4 5
}
std::cout << std::endl;

std::vector<int> destination2;
// Use std::back_inserter to dynamically expand the target container (commonly used with vector, list, deque)
std::copy(source.begin(), source.end(), std::back_inserter(destination2));

for (int val : destination2) {
std::cout << val << " "; // Output: 1 2 3 4 5
}
std::cout << std::endl;

return 0;
}

std::copy_if is a variant of std::copy that only copies elements from the source range that satisfy a specific condition. It requires an additional parameter—a unary predicate (a function or function object that returns a boolean value to determine whether an element should be copied, typically written as a lambda expression).

#include <iostream>
#include <vector>
#include <algorithm> // For std::copy_if
#include <iterator> // For std::back_inserter

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> even_numbers;

// Copy only even numbers
std::copy_if(numbers.begin(), numbers.end(),
std::back_inserter(even_numbers),
[](int n){ return n % 2 == 0; });

for (int val : even_numbers) {
std::cout << val << " "; // ouput: 2 4 6 8 10
}
std::cout << std::endl;

return 0;
}

5.2 Moving

C++11 introduced move semantics, allowing resources (such as dynamically allocated memory) to be “transferred” from one object to another instead of performing expensive copies.

The core of move semantics is the rvalue reference, denoted by &&, which can bind to temporary objects (rvalues) that are about to be destroyed.

Move Ctors

When a new object is initialized with an rvalue (typically a temporary object or the result of std::move), the move constructor is called. Its task is not to copy resources, but to “steal” resources from the source object, leaving the source object in a valid but typically empty or unspecified state.

ClassName(ClassName&& other) noexcept;
  • ClassName&& other: The parameter is an rvalue reference to an object of the same class.

  • noexcept (optional, but strongly recommended): If the move constructor might throw an exception, certain components in the standard library may choose copy over move because they cannot provide a strong exception guarantee, thus losing performance benefits. Therefore, noexcept should be used to ensure the move constructor does not throw exceptions.

class MyString {
public:
char* data_;
size_t size_;

// ...
// Move Constructor
MyString(MyString&& other) noexcept
: data_(other.data_), size_(other.size_) {
std::cout << "Move constructor from: \"" << (other.data_ ? other.data_ : "") << "\"\n";
// 1. Steal resources (shallow copy pointer and size)
// data_ = other.data_;
// size_ = other.size_; // Already done in the member initializer list

// 2. Place the source object in a valid but unspecified (usually empty) state
// Ensure that the source object does not release the stolen resources when destructed
other.data_ = nullptr;
other.size_ = 0;
}
// ...
};

Move Assignments

When assigning an existing object with an rvalue (a temporary object or the result of std::move), the move assignment operator is called. It first releases the resources held by the current object, then “steals” the resources from the source object, leaving the source object in a valid but unspecified state.

ClassName& operator=(ClassName&& other) noexcept;
  • ClassName&& other: The parameter is an rvalue reference to an object of the same class.

  • noexcept (optional, but strongly recommended): If the move constructor might throw an exception, certain components in the standard library may choose copy over move because they cannot provide a strong exception guarantee, thus losing performance benefits. Therefore, noexcept should be used to prevent the move constructor from throwing exceptions.

  • Return ClassName& (i.e., *this) to support chained assignment.

// Continuing from the example above
class MyString {
public:
char* data_;
size_t size_;

// ...
// Move assignment operator
MyString& operator=(MyString&& other) noexcept {
std::cout << "Move assignment operator from: \"" << (other.data_ ? other.data_ : "") << "\"\n";
if (this == &other) { // Self-assignment check (although alias situations are rare for rvalue reference sources, it is still a good habit)
return *this;
}

// 1. Release the resources held by the current object
delete[] data_;

// 2. Steal the resources of the source object
data_ = other.data_;
size_ = other.size_;

// 3. Place the source object in a valid but unspecified (usually empty) state.
other.data_ = nullptr;
other.size_ = 0;

return *this;
}
// ...
};

std::move()

std::move itself does not perform any move operation. It is a library function (located in the <utility> header) that unconditionally converts its argument into an rvalue reference.

#include <vector>
#include <string>
#include <utility>
#include <iostream>

int main() {
std::string str1 = "hello";
std::cout << "Before move, str1: " << str1 << std::endl;

std::string str2 = std::move(str1); // Calling the move constructor, the resources of str1 are transferred to str2.

std::cout << "After move, str1: " << str1 << " (state is valid but unspecified)" << std::endl;
std::cout << "After move, str2: " << str2 << std::endl;

std::vector<std::string> vec1;
vec1.push_back("world");

std::vector<std::string> vec2 = std::move(vec1); // Move, the internal buffer of vec1 may be taken over by vec2.

std::cout << "vec1 size after move: " << vec1.size() << std::endl;
std::cout << "vec2 size after move: " << vec2.size() << std::endl;
if (!vec2.empty()) {
std::cout << "vec2[0] after move: " << vec2[0] << std::endl;
}

return 0;
}

State of the moved-from object: After calling std::move(obj), if a move operation actually occurs (i.e., a move constructor or move assignment is matched), the state of the object obj becomes “valid but unspecified.” This means:

  • The destructor can still be called on obj (it must be safely destructible).

  • obj can be reassigned.

  • However, you should no longer rely on the value or state that obj previously held, unless the class’s documentation explicitly specifies the state after moving (e.g., std::string is typically empty after being moved).

Do not use std::move on const objects: std::move converts const T to const T&&.

Be cautious when using std::move in a return statement:

std: :swap()

The std::swap function (located in <utility> (C++11 and later) or <algorithm> (before C++11)) is used to swap the values of two objects.

The implementation is similar to:

template<class T>
void swap(T& a, T& b) {
T temp = std::move(a); // Move Constructor
a = std::move(b); // Move assignment
b = std::move(temp); // Move assignment
}

Sometimes, we can provide a more efficient specialized version by customizing std::swap.

  • Provide member and non-member swap functions: used to perform the lowest-level, most efficient swap operations, typically swapping internal pointers or basic type members.

  • Use using std::swap; and an unqualified call when invoking swap.

Chapter 6 Operator Overloading

Operator overloading provides us with a more intuitive and mathematically or logically natural way to call an object’s member functions or global functions.

Unary or binary operators that can be overloaded:

+ - * / % ^ & | ~
= < > += -= *= /= %=
^= &= |= << >> >>= <<= ==
!= <= >= ! && || ++ --
, ->* -> () []
new delete
new[] delete[]

Operators that cannot be overloaded:

.        // Member Access Operator
.* // Member pointer access operator
:: // Scope Resolution Operator
?: // Conditional Operator (Ternary Operator)
sizeof // Length operator
typeid // Type identification operator
static_cast
dynamic_cast
const_cast
reinterpret_cast

At least one operand must be of a user-defined type (class type or enumeration type): This prevents altering the behavior of built-in type operators (e.g., the behavior of adding two ints cannot be changed).

Preserved characteristics:

  • Arity: The number of operands an operator takes cannot be changed. That is, a unary operator remains unary after overloading, and a binary operator remains binary.

  • Precedence: The precedence of operators is fixed and cannot be changed through overloading. For example, the precedence of * is always higher than that of +.

  • Associativity: The associativity of operators (left-associative or right-associative) also remains unchanged.

6.1 Declaration

The declaration of an overloaded operator is very similar to that of a regular function, except that the function name is special: it consists of the keyword operator followed by the operator symbol to be overloaded. For example, the function name for overloading the addition operator + would be operator+.

When an operator is overloaded as a member function of a class:

  • For binary operators, the left operand is the object itself that calls the member function (i.e., the object pointed to by the this pointer). Therefore, a binary operator in member function form requires only one explicit parameter (representing the right operand). Unary operators, on the other hand, require no explicit parameters.

  • The left operand (i.e., the this object) does not undergo implicit type conversion.

class Integer {
private:
int i;
public:
Integer(int val = 0) : i(val) {}

// Binary + as a member function
// this->i is the left operand, n.i is the right operand
const Integer operator+(const Integer& n) const {
return Integer(this->i + n.i);
}

// Unary minus - as a member function (prefix negation)
// this->i is the operand
const Integer operator-() const {
return Integer(-(this->i));
}

void display() const { std::cout << i; }
};

int main() {
Integer a(10), b(5);
Integer c = a + b; // Equivalent to a.operator+(b)
Integer d = -a; // Equivalent to a.operator-()
c.display(); // Output 15
d.display(); // Output -10
}

When an operator is overloaded as a global function:

  • All operands must be passed as explicit parameters, meaning a binary operator requires two parameters, and a unary operator requires one parameter.

  • All parameters can participate in implicit type conversion. This is especially useful when the left operand also needs to undergo type conversion.

  • By default, a global function cannot access the private and protected members of a class. If such access is needed, the class must declare it as a friend; otherwise, the global function can only manipulate objects through the class’s public interface.

class Integer {
private:
int i;
public:
Integer(int val = 0) : i(val) {}

// Declare the global operator+ as a friend so that it can access the private member i.
friend const Integer operator+(const Integer& lhs, const Integer& rhs);

void display() const { std::cout << i; }
};

// Definition of global operator+
const Integer operator+(const Integer& lhs, const Integer& rhs) {
return Integer(lhs.i + rhs.i);
}

int main() {
Integer a(10), b(5);
Integer c = a + b; // Equivalent to operator+(a, b)
// If Integer has a converting constructor that accepts an int (non-explicit):
// Integer e = 3 + a; // Equivalent to operator+(Integer(3), a)
c.display(); // Output 15
}

6.2 General Design Guideline

Parameter Passing:

  • For operators that do not modify the operand values, parameters are usually passed by const reference to avoid unnecessary copying and ensure the original objects are not modified.

  • For member function operators, if the operation does not modify the object’s own state, the member function should be declared as const.

  • For operators that modify the left operand (e.g., +=), if implemented as a global function, the left operand should be passed by non-const reference; if implemented as a member function, the implicit this pointer already points to the object to be modified.

Return Values:

  • The return type depends on the semantics of the operator.

  • Arithmetic operators (e.g., operator+) typically return a newly created object (by value), so they can return a const object to prevent accidental assignment to the result (usually a temporary object / rvalue).

  • Assignment operators (e.g., operator=, operator+=) usually return a reference to the left operand (*this) to support chaining.

  • Logical and comparison operators (e.g., operator==, operator<) should return a bool type.

  • Dereference operators (e.g., operator* for smart pointers) typically return a reference.

  • The subscript operator operator[] usually returns a reference to allow reading and writing of elements.

6.3 Details for Special Operators

Arithmetic / Bitwise Operators:

  • Member function: const T T::operatorX(const T& rhs) const;

  • Global function: const T operatorX(const T& lhs, const T& rhs);

Logical / Comparison Operators:

  • Unary:

    • Member: bool T::operator!() const;

    • Global: bool operator!(const T& operand);

  • Binary:

    • Member: bool T::operatorX(const T& rhs) const;

    • Global: bool operatorX(const T& lhs, const T& rhs);

  • Subscript operator []:

    • Must be a member function

    • Typically provides two versions: a const version returning a const reference (for const objects), and a non-const version returning a regular reference (for non-const objects, allowing modification).

    • Prototypes:

      • ValueType& T::operator[](IndexType index);

      • const ValueType& T::operator[](IndexType index) const;

Increment/Decrement Operators

Prefix form:

  • Member function: T& T::operator++();

  • Global function: T& operator++(T& obj);

  • Behavior: First modifies the object, then returns a reference to the modified object.

Postfix form:

  • Member function: T T::operator++(int);

  • Global function: T operator++(T& obj, int);

  • Special feature: The postfix version accepts an additional, unnamed int parameter. This parameter is only used to distinguish the postfix form from the prefix form. The compiler automatically passes a 0 as the value of this int parameter when calling, but this value is typically not used.

  • Behavior: First saves a copy of the object’s current value, then modifies the object, and finally returns the copy of the original value (returned by value).

class Integer {
private:
int i;
public:
Integer(int val = 0) : i(val) {}

// Prefix ++
Integer& operator++() {
i += 1;
return *this;
}

// Postfix ++
Integer operator++(int) {
Integer old(*this); // Save a copy of the original value
++(*this); // Using the prefix ++ to modify the object (or directly i += 1;)
return old; // Returns a copy of the original value
}

void display() const { std::cout << i; }
};

int main() {
Integer num(5);
Integer res1 = ++num;
num.display(); res1.display(); std::cout << std::endl;

Integer num2(10);
Integer res2 = num2++;
num2.display(); res2.display(); std::cout << std::endl;
}

Prefix increment/decrement is usually more efficient than the postfix form. Therefore, when it does not affect the logic (for example, when only incrementing a counter in a loop), prefer the prefix form.

Relation Operators

We usually only need to implement operator== and operator<, and the other relational operators can be implemented based on these two to reduce code duplication and maintain consistency.

class Integer {
private:
int i;
public:
Integer(int val = 0) : i(val) {}

bool operator==(const Integer& rhs) const {
return i == rhs.i;
}

bool operator<(const Integer& rhs) const {
return i < rhs.i;
}

// Implement other relational operators based on == and <
bool operator!=(const Integer& rhs) const { return !(*this == rhs); }
bool operator>(const Integer& rhs) const { return rhs < *this; }
bool operator<=(const Integer& rhs) const { return !(*this > rhs); } // Or !(rhs < *this)
bool operator>=(const Integer& rhs) const { return !(*this < rhs); }
};

Stream Operators

Stream operators must be overloaded as global/free functions (usually friends).

The stream extraction operator operator>>:

  • Prototype: std::istream& operator>>(std::istream& is, YourType& obj);

  • The first parameter is a reference to std::istream (e.g., cin)

  • The second parameter is a reference to the object that will receive the data (non-const, because it will be modified)

  • Inside the function, data is read from the input stream is and filled into obj

  • Returns is (a reference to the input stream) to support chaining, e.g., cin >> a >> b;

The stream insertion operator operator<<:

  • Prototype: std::ostream& operator<<(std::ostream& os, const YourType& obj);

  • The first parameter is a reference to std::ostream (e.g., cout)

  • The second parameter is a const reference to the object to be output (the object should generally not be modified during output)

  • Inside the function, the content of obj is formatted and written to the output stream os

  • Returns os (a reference to the output stream) to support chaining, e.g., cout << a << b;

#include <iostream>
#include <sstream>

class Point {
private:
int x, y;
public:
Point(int x_val = 0, int y_val = 0) : x(x_val), y(y_val) {}

friend std::ostream& operator<<(std::ostream& os, const Point& p);
friend std::istream& operator>>(std::istream& is, Point& p);
};

std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}

std::istream& operator>>(std::istream& is, Point& p) {
// Assume the input format is (x, y) or x y
char delim1, comma, delim2;
// Attempting to read (x, y) format
if (is >> delim1 && delim1 == '(' &&
is >> p.x >> comma && comma == ',' &&
is >> p.y >> delim2 && delim2 == ')') {
} else {
if (is.fail()) {
is.clear(); // Clear error flag
// Attempt to roll back the stream pointer (if supported and necessary)
// Here, it is simply assumed that it can be read again.
is >> p.x >> p.y;
}
}
return is;
}

int main() {
Point p1(1, 2), p2;
std::cout << "P1: " << p1 << std::endl; // ((cout << "P1: ") << p1) << endl;

std::cout << "Enter point p2 (e.g., 3 4 or (5,6)): ";
std::cin >> p2;
std::cout << "P2 entered: " << p2 << std::endl;
}

Assignment Operator

Must be a member function

If you do not provide a custom operator=, the compiler will automatically generate one for you. This default assignment operator performs memberwise assignment, similar to the memberwise copy behavior of the default copy constructor. For simple classes (those that do not dynamically allocate memory or manage other resources), the default version is usually sufficient.

The necessity of a custom operator=: If a class manages dynamically allocated memory (or other resources that require special handling, such as file handles), you must provide a custom assignment operator.

Design considerations:

  • Check for self-assignment to prevent issues such as premature resource release when an object is assigned to itself, and to improve efficiency.

    class Bitmap { ... };
    class Widget {
    ...
    private:
    Bitmap* pb;
    }
    • Identity test: Immediately return *this upon detecting self-assignment.

      Widget& Widget::opeator=(const Widget& rhs) {
      if (this == &rhs) return *this;

      delete pb;
      pb = new Bitmap(*rhs.pb);
      return *this;
      }
    • Carefully arrange statements: Sometimes, simply adjusting the order of statements can avoid self-assignment issues.

      Widget& Widget::opeator=(const Widget& rhs) {
      Bitmap *pOrig = pb; // Remember the original pb
      pb = new Bitmap(*rhs.pb);
      delete pOrig; // Delete the original pb
      return *this;
      }
    • Copy and swap: copy the incoming parameter (rvalue), then swap *this with the copy.

      class Widget {
      ...
      void swap(Widget& rhs);
      ...
      };

      Widget& Widget::opeator=(const Widget& rhs) {
      Widget temp(rhs); // Create a copy of rhs
      swap(temp); // Swap *this with the copy above.
      return *this;
      }
  • Properly handle resources: Before copying resources from rhs, the old resources currently held by the *this object must be correctly released.

  • Assign values to all data members: Ensure that all relevant data from the source object is correctly copied to the target object.

  • Return a reference to *this: This supports chained assignments, such as a = b = c; (which is parsed as a = (b = c);).

  • Prevent assignment: If you want objects of a class to be non-assignable, you can declare operator= as private (traditional approach) or use C++11’s = delete; (recommended approach).

6.4 Value Classes

Value classes refer to those classes that behave like built-in primitive data types.

  • They can be passed as function parameters and returned like ordinary values.
  • They often overload various operators to make their usage natural and intuitive (e.g., Complex, Date, String classes).
  • They may support conversions (implicit or explicit) to and from other types.

6.5 Type Conversion and Casting

If a constructor can be called with a single argument (or has multiple arguments, but all arguments after the first have default values), it can be used by the compiler to perform an implicit conversion from the argument type to the class type.

class PathName {
private:
std::string name;
public:
// This constructor allows implicit conversion from std::string to PathName.
PathName(const std::string& s) : name(s) {
std::cout << "PathName created from string: " << name << std::endl;
}
void print() const { std::cout << name; }
};

void processPath(PathName p) {
p.print();
}

int main() {
std::string str_path = "/usr/local";
PathName pn1(str_path); // Explicit construction
processPath(str_path); // Implicit Conversion: str_path -> PathName(str_path)
}

To prevent unwanted implicit conversions (which can lead to hard-to-detect errors or ambiguity), the explicit keyword can be used before a single-argument constructor, so that conversions must be explicit.

class PathNameExplicit {
private:
std::string name;
public:
explicit PathNameExplicit(const std::string& s) : name(s) {}
// ...
};

PathNameExplicit pn_expl = std::string("/usr");
// Error! Explicit constructors cannot be used for implicit conversions.
PathNameExplicit pn_expl(std::string("/usr"));
// Correct, explicit call.

Conversion operators: These are special member functions used to define a conversion from a class type to another type T. (Where T is the target type, such as double, int, or other class types)

  • Typically have no parameters (since the conversion is from the *this object)

  • Should generally be declared as const, because the conversion operation should not modify the source object

operator T() const;

class Rational
{
private:
int numerator_;
int denominator_;
public:
Rational(int num = 0, int den = 1) : numerator_(num), denominator_(den == 0 ? 1 : den) {}

// Type conversion operator: converts Rational to double
operator double() const
{
return static_cast<double>(numerator_) / denominator_;
}
};

int main()
{
Rational r(1, 2);
double d = r; // Implicit call to r.operator double()
std::cout << "d = " << d << std::endl; // Output d = 0.5

double val = 0.5 + r; // `r` is converted to double, and then addition is performed.
std::cout << "val = " << val << std::endl; // output val = 1.0
}

Over-reliance on implicit type conversions can make code difficult to understand and maintain, and can easily introduce ambiguity. It is generally recommended to:

  • Use explicit for single-argument constructors unless implicit conversion is truly needed and its behavior is very intuitive.

  • Prefer named conversion functions (such as toDouble(), toString()) over implicit type conversion operators, so that the conversion intent is clearer.

Modern Casting

const_cast: Used for casting away the constness, simply put, it removes the original const and volatile qualifiers.

dynamic_cast: Mainly used for safe downcasting, i.e., converting a base class reference/pointer to a derived class reference/pointer (opposite of upcasting).

  • If the types do not match, pointer type conversion returns nullptr, while reference type conversion throws std::bad_cast.

  • The base class must be polymorphic (i.e., have at least one virtual function).

reinterpret_cast: Used for low-level casting, whose actual behavior (and results) may depend on the compiler, meaning it is not portable.

static_cast: Used for forcing implicit conversions.

static_cast<new_type>(expression)
  • Mainly used for type conversion in the following scenarios:

    • Conversion between related types: Converting between types that have a well-defined conversion path, including: conversions between basic types; conversion of subclass pointers/references to parent class pointers/references; conversion between void pointers and pointers of other types

    • Explicitly invoking implicit conversions: When an implicit conversion path exists (e.g., through a single-argument constructor or a type conversion operator), static_cast can make the intent of such a conversion clearer.

    • Resolving ambiguity: In certain expressions, implicit conversions may lead to ambiguity; using static_cast can explicitly specify the desired conversion.

  • The safety of static_cast is reflected in: compile-time checking & no runtime checking

Chapter 7 Templates

The core features of templates can be summarized as:

  • Code reuse
  • Generic programming
  • Implicit interfaces
  • Compile-time polymorphism

Basic Components of a Template:

  • Template Parameters: These are unspecified placeholders when the template is defined, and are assigned specific values or types only when the template is used (instantiated).

    • Type Parameters: The keywords class or typename used in declarations (e.g., template <class T> or template <typename T>) are nearly equivalent. T in the template definition represents a pending type, which will be replaced by a concrete type such as int, std::string, or a user-defined class during instantiation.

However, typename can also be used to disambiguate dependent names (names that depend on template parameters). Since C++ treats T::somename as a non-type by default, when referencing a name nested inside a template parameter T, the compiler does not know whether it refers to a type or a non-type member. In such cases, using typename indicates that it is a type.

  • Non-type Parameters: These accept compile-time constant values as parameters, which must be determinable at compile time. They can be integer constants, enumeration values, pointers or references to objects, etc.

  • Template Template Parameters: These allow passing a template itself as a parameter to another template.

  • Template instantiation: Simply defining a template does not generate any executable code. Only when a template is instantiated does the compiler generate specific class or function code based on the provided template parameters.

    • Process: During instantiation, the compiler replaces the template parameters (such as T) in the template definition with actual types (such as int) or values. It then compiles this “filled-in” template code like ordinary code, performing syntax and type checking.

    • Implicit instantiation: The most common form of instantiation. When we use a template like a regular function or class and provide enough information for the compiler to deduce the template parameters, the compiler automatically instantiates it for us.

    • Explicit instantiation: In some cases, even if a specific version of a template is not directly used in the code, we may want to explicitly instruct the compiler to instantiate such a template. This can be useful when organizing large projects, separating compilation, or ensuring that a specific template version is compiled.

// Assume that MyList.h contains the declaration and definition of the MyList template
// In a certain .cpp file, we can explicitly instantiate:
template class MyList<double>;
// Force the compiler to generate the complete code for the MyList<double> class
template void print_array<char>(char*, int);
// Force generation of code for the print_array<char> function
  • Template specialization: Sometimes, for certain specific types, the general version of a template may be inefficient or even behave incorrectly. In such cases, template specialization is needed, allowing the template to maintain generality while gracefully handling various special cases, making the template more powerful and practical.

    • Full specialization: When we provide specific types or values for all parameters of a template, it constitutes full specialization. It provides a completely new implementation for this specific combination of parameters, fully replacing the general template.
// Generic function template
template <class T>
const char* getTypeName(T value) {
return "Unknown type";
}

// Full specialization for bool
// Empty angle brackets indicate that this is a full specialization
template <>
const char* getTypeName<bool>(bool value) {
// <bool> indicates specialization for the bool type
return "boolean";
}

// Generic class template
template <class T> class Widget { /* General Implementation */ };

// Full specialization of class template for void*
template <> class Widget<void*> { /* Special implementation for void* */ };
  • Partial specialization: Only applicable to class templates (function templates do not have partial specialization, but similar effects can be achieved through techniques such as overloading)
// Generic class template that accepts two type parameters
template <class T1, class T2> class DataProcessor { /* ... */ };

// Partial specialization: when the second type parameter is int, the first parameter T1 remains generic
template <class T1> class DataProcessor<T1, int> { /* ... Special implementation for (T1, int) ... */ };

// Partial specialization: when two type parameters are the same, type T remains generic
template <class T> class DataProcessor<T, T> { /* ... Special implementation for (T, T) ... */ };

// Partial specialization: for the case where T1 is a pointer type, T2 remains generic
template <class PointeeType, class T2> class DataProcessor<PointeeType*, T2> { /* ... For (PointeeType*, T2) ... */ };

7.1 Function Templates

template <class T>
void swap_values(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}

// Call
int x = 5, y = 10;
swap_values(x, y);

std::string s1 = "hello", s2 = "world";
swap_values(s1, s2);

Sometimes we need (or choose) to explicitly tell the compiler what the template parameters should be, including the following situations:

  • The compiler cannot deduce: when the template parameter does not appear in the function parameter list, or when the deduction process is ambiguous (for example, we want to pass a string, but the compiler treats it as a character pointer, which may lead to unexpected consequences if not explicitly specified).
template <typename ReturnType> 
ReturnType create_default_value() {
return ReturnType();// Call the default constructor
}
int my_int = create_default_value<int>();
// Must explicitly specify ReturnType as int
  • Overriding inferred results: Sometimes we may want to instantiate a template with a type different from the inferred result (as long as it is compatible).

  • Improving code readability: In certain complex situations, explicitly specifying parameters can make the code’s intent clearer.


When a function call may match an ordinary function or one or more instantiations of function templates, the compiler needs a set of rules to determine which one to call—this process is called overload resolution. The general resolution order is as follows:

  1. Look for an exact match with an ordinary function.

  2. Look for an exact match with a function template specialization.

  3. Attempt to match a generic function template (if multiple function templates can match, the compiler tries to select the more specialized template).

  4. Consider overloads of ordinary functions (such as standard conversions, user-defined conversions).

7.2 Class Templates

Member Functions

Member functions of a class template are themselves (implicitly) function templates. Their complete definitions (including the function body) are only instantiated by the compiler for a specific class template instance when they are actually called or their address is taken.

Additionally, there is a more explicit type of member function templates. They provide a generic operation that allows the function to accept all compatible types, avoiding function overloading (and sometimes we cannot determine how many functions to overload).

#include <iostream>
#include <vector>
#include <string>

class MyContainer {
private:
std::vector<int> data;
public:
// Member function template: allows adding any type that can be converted to int
template <typename T>
void addElement(T value) {
data.push_back(static_cast<int>(value)); // Try to convert
std::cout << "Added element of type " << typeid(T).name()
<< ", value: " << static_cast<int>(value) << std::endl;
}

void printElements() const {
std::cout << "Container elements: ";
for (int val : data) {
std::cout << val << " ";
}
std::cout << std::endl;
}
};

int main() {
MyContainer mc;
mc.addElement(10); // T = int
mc.addElement(3.14); // T = double
mc.addElement('A'); // T = char
mc.addElement(true); // T = bool

mc.printElements(); // Output: Container elements: 10 3 65 1

// mc.addElement("hello");
// Compilation error: string cannot be converted to int, reflecting type safety
return 0;
}

Member function templates can also be used to generalize copy constructors and assignment operations, but in this case, a normal copy constructor and copy assignment operator definition must also be retained.

Parameters

Class templates can accept multiple template parameters, which can be either type parameters or non-type parameters.

// A simple hash table template that accepts key type, value type, and an optional hash function object type.
template <
class Key,
class Value,
class HashFunction = std::hash<Key>
// Default template parameters
>
class HashTable {
public:
void insert(const Key& k, const Value& v) {
HashFunction hasher;
size_t hash_value = hasher(k);
// ... Actual insertion logic ...
}
bool find(const Key& k, Value& out_value) const {
// ...
return false;
}
// ...
private:
// ... Internal data structures, such as linked lists, arrays, or open addressing tables. ...
};

Default Parameters

Template parameters (including type parameters and non-type parameters) can also have default values, so we don’t need to explicitly provide all template parameters.

template <class T = int, int DefaultSize = 100>
class ArrayWrapper {
T arr[DefaultSize];
public:
// ...
};

ArrayWrapper<> default_arr;
// Instantiated as ArrayWrapper<int, 100>
ArrayWrapper<char, 256> char_arr;
// Instantiated as ArrayWrapper<char, 256>

Non-type Parameters

template <class T, int MaxSize>
class FixedSizeBuffer {
T data[MaxSize]; // MaxSize is determined at compile time
int current_size;
public:
FixedSizeBuffer() : current_size(0) {}
bool push(const T& item) {
if (current_size < MaxSize) {
data[current_size++] = item;
return true;
}
return false;
}
// ...
};

// Template Instantiation
FixedSizeBuffer<char, 1024> network_packet_buffer;

Whether to use non-type parameters to embed attributes like size depends on the specific scenario, balancing the performance/type safety benefits against the costs in flexibility and code size.

Complex Parameters

Templates can be freely combined and nested, just like ordinary types.

Template nesting: The instantiation result of one template can itself serve as a parameter for another template.

  • Example: Vector< Vector<int> > represents a Vector whose elements are themselves Vector<int> (i.e., a vector of integer vectors, similar to a two-dimensional array).

>> will be parsed by the compiler as the right-shift operator, so a space must be added between the two > symbols.

Complex type parameters: Template parameters themselves can be very complex types, such as function pointers, pointers to members, etc.

  • Example: Vector< int (*)(double, double) > defines a Vector that stores pointers to functions that “take two double parameters and return an int.”

Static Members

When a class template contains static data members, each distinct instantiation of the class template has its own independent copy of the static data members.

template <class T>
class ObjectTracer {
public:
static int objects_created_count;
ObjectTracer() {
objects_created_count++;
}
};

// Definition of static data member (usually placed in the header file as well, because it depends on the template parameter T)
template <class T>
int ObjectTracer<T>::objects_created_count = 0; // 初始化为0

void static_member_example() {
ObjectTracer<int> int_obj1;
ObjectTracer<int> int_obj2;
// ObjectTracer<int>::objects_created_count is now 2.

ObjectTracer<double> double_obj1;
// ObjectTracer<double>::objects_created_count is now 1.

std::cout << "Int objects created: " << ObjectTracer<int>::objects_created_count << std::endl;
std::cout << "Double objects created: " << ObjectTracer<double>::objects_created_count << std::endl;
}

Friends

Declaring friend relationships in class templates is more complex than in ordinary classes, because we need to consider whether the friend itself is also a template, and whether the friend relationship applies to a specific instance of the template or to all instances.

  • Non-template friend functions or friend classes: An ordinary (non-template) function or class can be declared as a friend of a class template (or all its instances).

    // Ordinary function
    void inspect_internals(const MyTemplatedClass<int>& obj);
    // Assume that it is only for int version

    template <typename T>
    class MyTemplatedClass {
    T data;
    // A friend who declares an ordinary function as MyTemplatedClass<T> (all instances)
    // Note: The signature of this friend function must match the external definition.
    friend void global_helper_func(MyTemplatedClass<T>& mtc) {
    // can access mtc.data
    }
    // Or if global_helper_func is an existing non-template function
    // friend void another_global_func(const MyTemplatedClass<T>&);

    // Declare an ordinary class as the friend element of all instances
    friend class Inspector;
    };

    class Inspector {
    public:
    template <typename T>
    void examine(const MyTemplatedClass<T>& obj) {
    // can access obj.data
    }
    };
  • Template as Friend

    • Constrained Friend: Making a specific instance of a function template or class template a friend of the current class template instance. This typically requires the friend template and the current class template to use the same template parameters.

    • Unconstrained Friend: Making all instances of a function template or class template friends of a particular instance (or all instances) of the current class template.

    • Defining a friend function template directly inside a class template is a common practice, especially for operator overloading.

7.3 Templates and Inheritance

Templates and inheritance are two powerful code reuse mechanisms in C++. They can be combined in various ways to build flexible and well-layered generic code structures. The following are several types of combinations:

Template classes inheriting from non-template classes: providing a common non-generic base interface or sharing some non-generic functionality for a group of related generic classes.

class Identifiable { 
// Non-template base class
static int next_id;
protected:
int id;
public:
Identifiable() : id(next_id++) {}
int get_id() const { return id; }
};
int Identifiable::next_id = 0;

template <class AssetType>
class ManagedAsset : public Identifiable {
// Template class inheriting from a non-template class
AssetType asset_data;
public:
ManagedAsset(AssetType data) : asset_data(data) {}
AssetType get_asset() const { return asset_data; }
// ManagedAsset instances also have id and get_id() methods
};

// Template instantiation
ManagedAsset<std::string> text_asset("important_document.txt");
int id = text_asset.get_id();

Template class inherits from class template: Based on an existing generic class, its functionality is extended or specialized through inheritance while maintaining generic characteristics.

template <class Item>
class BaseCollection { // Generic base class
protected:
std::vector<Item> items;
public:
void add(const Item& item) { items.push_back(item); }
size_t count() const { return items.size(); }
};

// Derived template class, inheriting from an instance of the base class template
// Often, derived templates pass the same template parameters to the base class template.
template <class Item>
class SortedCollection : public BaseCollection<Item> { // Inherited from BaseCollection<Item>
public:
// Rewrite the add method to maintain order.
void add(const Item& item) {
BaseCollection<Item>::add(item); // Calling the base class's add
std::sort(this->items.begin(), this->items.end());
}
};

// Template Instantiation
SortedCollection<int> sorted_numbers;
sorted_numbers.add(5); sorted_numbers.add(1); sorted_numbers.add(3);
// At this point, sorted_numbers.items should be {1, 3, 5}
  • In this case, the compiler encounters a special issue when looking up inherited names, known as dependent name lookup. Fortunately, we have the following methods to explicitly tell the compiler to look up these inherited names in the base class:

    • Use this-> (recommended): Since the type of the this pointer depends on the template parameters, any member accessed via this-> is treated as a dependent name.

    • Use a using declaration: This introduces members from the base class into the scope of the derived class. Once introduced, these members become non-dependent names in the derived class.

Non-template classes inheriting from a specific instantiation (specialization) of a class template: This can be done when we need a class of a specific type based on a generic framework; it effectively “solidifies” the generic base class with a concrete type.

template <class T>
class GenericMessageHandler {
public:
virtual void process_message(const T& message) {
std::cout << "Generic handler processing: " << message << std::endl;
}
};

struct EmailMessage { std::string from, to, body; };
// Overload the stream operator for printing.
std::ostream& operator<<(std::ostream& os, const EmailMessage& msg) {
return os << "Email from " << msg.from;
}


// EmailHandler is a concrete class that specializes GenericMessageHandler to handle EmailMessage.
class EmailHandler : public GenericMessageHandler<EmailMessage> {
public:
void process_message(const EmailMessage& message) override { // Rewrite base class methods
std::cout << "Email specific handler: Routing email from "
<< message.from << " to " << message.to << std::endl;
// ... email Processing Logic ...
}
};

EmailHandler email_processor;
EmailMessage email = {"sender@example.com", "receiver@example.com", "Hello!"};
email_processor.process_message(email);

7.4 Guidelines

Template declarations and definitions should be placed in header files.


When the C++ compiler processes templates, it typically performs two-phase name lookup:

  • First phase (at template definition): The compiler checks names that do not depend on template parameters (e.g., global function names, type names defined outside the template). The validity of these names can be determined at the point of template definition.

  • Second phase (at template instantiation): The compiler checks names that depend on template parameters (e.g., T::member_type, or a function call with a parameter type of T). The validity of these names can only be determined after T is replaced with a concrete type.

This has the following implications:

  • At template definition, if a dependent name (such as T::some_type) is actually a type but the compiler cannot determine it in the first phase, the typename keyword may be needed to explicitly inform the compiler: typename T::some_type.

  • When accessing members of a dependent base class, it may be necessary to use this->member or Base<T>::member.


Since templates generate a separate copy of code for each distinct combination of template parameters used, if a template is instantiated with many different types, the size of the final executable can increase significantly. This is known as “code bloat.” The following methods can help mitigate code bloat:

  • Extract non-dependent parts

  • Use non-type template parameters wisely

  • Explicit instantiation (for library developers)

  • Linker optimizations


Jumping directly into writing complex templates can be daunting—following this structured approach can help us design good templates:

  1. First, implement the functionality using concrete types.

  2. Write thorough tests for the concrete version.

  3. (If needed) Optimize the performance of the concrete version.

  4. Review the code to identify parts that need to be “parameterized.”

    • Which data types are variable? These will become type template parameters T.

    • Are there any constant values in the code that should also be configurable? These can become non-type template parameters.

    • Does the code rely on specific behaviors of the parameter types? For example, does the algorithm need to compare objects of type T (operator<)? Does it need to copy them (copy constructor, copy assignment operator)? Does it need to default-construct them? These are implicit or explicit constraints that generic code imposes on template parameters.

  5. Convert the concrete version into a template.

    • Add a template <...> declaration before the function or class definition.

    • Replace the concrete types used in the code with the template parameter T.

    • Ensure that all operations dependent on the template parameters remain valid.

  6. Test the template version using the previous test cases.

7.5 Interfaces in C++

The interface introduced here is not like the interface keyword in languages such as Java or Go (which corresponds to abstract base classes and pure virtual functions in C++), but rather a broader concept at the level of software design and architecture. Specifically, it refers to the conventions and specifications that modules, components, or libraries expose to the outside for interaction and use by other code (usually user or client code). This is somewhat abstract, but that is precisely what “abstraction” means.

In C++, there are interfaces such as public functions, public classes, class templates and function templates, namespaces, header files, and so on.

When designing these interfaces, the most essential requirement is: make interfaces easy to use correctly and hard to use incorrectly.

Correct use: consistency of the interface, compatibility with the behavior of built-in types.

Hard to misuse: create new types, restrict operations on types, constrain object values, eliminate the user’s responsibility for resource management.

Any interface that requires users to remember to do something has a tendency to be used incorrectly, because users may forget to do it.

Chapter 8 Exception

A run-time error refers to an unexpected situation that occurs during the execution of a program.

An exception is an abnormal or unexpected situation that occurs while a program is running, which may prevent the program from continuing its normal flow. The exception mechanism provides a structured way to handle these runtime errors by separating the core logic code that describes “what the program should do” from the code that handles error conditions, making the program structure clearer.

When a function may throw an exception, the caller can choose different handling strategies:

  • Very concerned: Use a try-catch block to catch and handle specific types of exceptions, preventing the exception from propagating further.

  • Moderately concerned: Catch the exception for partial handling (such as logging), then use throw; to rethrow the exception, allowing the upper-level caller to continue processing.

  • Not concerned about certain special cases: Use multiple catch blocks to handle specific exception types separately, and use catch (...) to handle all other types of exceptions.

  • Not concerned at all: The caller’s code may not even be aware of the issue, and the exception will continue to propagate upward until it finds a matching catch block or causes the program to terminate.

try {
// Code that may throw an exception
} catch (ExceptionType1 param1) {
// Handle exceptions of type ExceptionType1
} catch (ExceptionType2 param2) {
// Handling exceptions of type ExceptionType2
} catch (...) {
// Handle all other types of exceptions
}

The catch block checks all handlers in order from top to bottom:

  • First, it looks for an exact match of the exception type.

  • Then, it applies basic class conversion, which only applies when catching exceptions by reference (&) or pointer (*), allowing a base class reference/pointer to catch derived class exceptions (polymorphism). That is, even if a derived class exception is thrown, if the catch block for the base class exception appears before the catch block for the derived class exception, the exception will be caught by the former first (somewhat counterintuitive).

  • Finally, if none match, the ellipsis (...) catches exceptions of any type.

8.1 Throwing Execption

In C++, we use the throw keyword to throw exceptions.

The throw statement has two main forms:

throw expression;: This is the most common form, used to throw a new exception object. The expression can be of any type and any value. The value of this expression is used to initialize a temporary exception object, which is then passed to the exception handling mechanism.

template <class T>
T& Vector<T>::operator[](int indx) {
if (indx < 0 || indx >= m_size) {
// Throw an exception object indicating an index out of bounds
// Here, an object of type VectorIndexError is thrown
throw VectorIndexError(indx);
}
return m_elements[indx];
}

We can throw almost any type of value, but it is generally recommended to throw objects of class types, especially standard exception classes that inherit from std::exception or custom exception classes, so that more information about the error can be carried.

  • Throwing an object (recommended): throw MyErrorType("Detail message");

  • Throwing a basic type (not recommended): throw 42; (lacks error information)

  • Throwing a pointer (strongly not recommended): throw new MyErrorType(); (requires manual delete in the catch block, prone to memory leaks)

When throw expression; is executed:

  • The execution of the current function stops immediately

  • The program begins the process of stack unwinding, starting from the current scope and sequentially calling the destructors of local objects

  • During stack unwinding, the program searches up the call stack for a matching catch block; the search process considers the type of the exception object and the parameter types declared in the catch block, including base-to-derived class reference/pointer conversions

  • Once a matching catch block is found, stack unwinding stops, and control transfers to the beginning of that catch block

  • If no matching catch block is found all the way back to the main function, std::terminate() is called, which by default terminates the program

throw;: This form is called re-throwing, and it can only be used inside a catch block. Its purpose is to throw the currently handled exception (i.e., the exception just caught by this catch block) again to an outer scope.

void outer2() {
String err("exception caught");
try {
func(); // func() may throw VectorIndexError
} catch (VectorIndexError& e) {
// Caught VectorIndexError exception
std::cout << err << ": " << e.diagnostic() << std::endl; // Perform local processing (e.g., logging)

// Rethrow the same exception for the upper-level caller to handle.
throw; // Spread anomaly
}
// If not rethrown and no other exception occurs, control returns here
// But because of the throw above, control continues to propagate upward
}

8.2 Exception and Inheritance

The inheritance mechanism can be used to build a hierarchy of exception classes. By defining a base exception class and multiple derived exception classes, we can organize and handle different types of errors more flexibly.

class MathErr {
...
virtual void diagnostic();
};
class OverflowErr : public MathErr { ... }
class UnderflowErr : public MathErr { ... }
class ZeroDivideErr : public MathErr { ... }

// ...

try {
// code to exercise math options
throw UnderFlowErr(); // Throwing a derived class exception
} catch (ZeroDivideErr& e) {
// Handle ZeroDivideErr (Exact Match)
// handle zero divide case
} catch (MathErr& e) {
// Handle MathErr and its derived classes (such as UnderflowErr)
// handle other math errors
} catch (...) {
// Handle any other exceptions
}

8.3 Exception in Standard Library

The C++ standard library provides a set of standard exception classes that form a hierarchy through inheritance. These are typically used to report errors that may occur in standard library functions. Common standard library exceptions include:

  • std::exception: The base class for all standard library exceptions

  • std::bad_alloc: Thrown when a new operation fails to allocate memory. When new fails, it does not return nullptr (unless std::nothrow is specified), but instead throws a std::bad_alloc exception by default

  • std::runtime_error: The base class for runtime errors, such as std::overflow_error, std::range_error

  • std::logic_error: The base class for program logic errors, such as std::domain_error, std::invalid_argument, std::out_of_range, std::length_error

8.4 Exception Specifications

Exception specification (throw(...)) is used to declare the types of exceptions that a function may throw.

// May throw an exception of type MathErr.
void abc(int a) throw(MathErr) {
...
}

// Declare that no exceptions will be thrown
void goodguy() throw() {
...
}

// No exception specification; any exception may be thrown.
void average() { }

// Introduced in C++11, explicitly indicates that the function will not throw exceptions.
void lala() noexcept;

8.5 Exception in Ctors and Dtors

A special characteristic of constructors is that they have no return value, so failure cannot be indicated through a return value.

Traditional methods for handling constructor failures include using “uninitialized flags” or placing initialization work in a separate Init() function.

However, a better approach is to throw an exception in the constructor to indicate initialization failure.

When a constructor throws an exception, the destructor of that object will not be called. Therefore, before throwing an exception in the constructor, it is necessary to ensure that any allocated resources are properly cleaned up. This is typically achieved through the RAII (Resource Acquisition Is Initialization) technique.


A destructor is called in the following situations:

  • Normal invocation: when an object goes out of its scope.

  • When an exception occurs: during stack unwinding, as the exception propagates upward and passes through the lifetime scope of an object, the destructor of that object is called to perform resource cleanup.

Exceptions should be avoided in destructors.

8.6 Exception Safety

Exception safety: The ability of a program to maintain the validity of its state and avoid resource leaks when an exception occurs.

When an exception is thrown, an exception-safe function does not leak any resources and does not allow data corruption.

Exception safety is typically categorized into the following levels (from weakest to strongest):

  • Basic guarantee: If an operation fails and throws an exception, the program’s state remains valid. No data corruption occurs, and all invariants are preserved. No resource leaks occur (e.g., memory, file handles, locks, etc.). However, the program’s state is not guaranteed to be predictable or identical to its state before the operation.

  • Strong guarantee: If an operation fails and throws an exception, the program’s state is rolled back to its state before the operation, as if the operation never happened. This ensures an “all-or-nothing” change to the program’s state (similar to the atomicity of database concurrency control). No resource leaks occur. It provides the highest level of logical consistency and simplifies error handling.

  • No-throw guarantee: The function guarantees that it will not throw any exceptions. It either completes successfully or fails without throwing an exception (e.g., by returning an error code or setting an error state). This is the strongest guarantee, simplifying error handling for the caller since no try-catch is needed.

The exception safety guarantee provided by a function is typically only as strong as the weakest guarantee among all the functions it calls.

8.7 Summary

When designing exceptions, the following principles should be followed:

  • Exceptions should be used to indicate error situations that prevent the program from proceeding normally, rather than for normal control flow.

  • Exceptions are a mechanism for handling errors, not an excuse for writing poor code. When other language features (such as RAII) can be used to simplify resource management, they should be prioritized.

Purpose of the C++ exception mechanism:

  • Enables dynamic propagation of error information

  • Ensures that objects on the stack are properly destroyed during exception propagation

  • Can be used to terminate functions that are unable to continue execution

  • Particularly suitable for handling cases where constructors cannot complete their tasks

Chapter 9 Smart Pointers

In C++ programming, managing dynamically allocated memory is a critical task. Traditional manual memory management (using new and delete) is error-prone and often leads to issues such as memory leaks (e.g., forgetting to release memory) or dangling pointers (e.g., accessing already freed memory). Smart pointers were introduced to address these challenges in a safer and more automated manner.

9.1 RAII

RAII (Resource Acquisition Is Initialization) is a powerful and widely used programming technique in C++. Its core idea is to bind the lifecycle of a resource to that of an object. Resource acquisition occurs during object construction, and resource release occurs during object destruction.

When designing an RAII object, its copy (and move) semantics must be carefully considered. The main handling strategies are as follows:

  • Prohibit copying: When a resource should not be shared among multiple owners, or when copying is too expensive, copying of the RAII object should be prohibited. Specifically, the copy constructor and copy assignment operator should be disabled (by making them private without implementation, or using = delete).

  • Shared ownership: Allow multiple RAII objects to jointly manage the same resource, using mechanisms such as reference counting to ensure that the resource is released only when the last owner is destroyed.

  • Copy the underlying resource (i.e., deep copy): When the resource itself can be copied and it is reasonable to create an independent copy, this strategy can be adopted: when copying the RAII object, not only the object itself is copied, but also the resource it manages, thereby creating a completely independent copy of the resource.

  • Transfer ownership: Allow RAII objects to transfer ownership of the managed resource through move operations (such as using move constructors and move assignment operators) without performing a deep copy. After the move, the source object is typically left in a valid but unspecified state (often an “empty” state).

9.2 Smart Pointers in Standard Library

Smart pointers are a concrete application of the RAII principle in dynamic memory management. They are class objects that behave like raw pointers but automatically release the dynamically allocated memory they point to when their lifetime ends.

The C++ standard library provides several main types of smart pointers to meet different memory management needs:

std::unique_ptr

  • Exclusive ownership: std::unique_ptr has unique and exclusive ownership of the object it points to, meaning that at any given time, only one std::unique_ptr instance can point to and manage a specific piece of dynamic memory.

  • Non-copyable but movable: To ensure ownership uniqueness, std::unique_ptr does not support copy operations (i.e., the copy constructor and copy assignment operator are disabled). However, it supports move operations (std::move), allowing ownership to be transferred from one std::unique_ptr to another. After the transfer, the original std::unique_ptr no longer points to the object.

  • Lightweight: std::unique_ptr has very low overhead, typically comparable to a raw pointer.

  • Creation method: It is recommended to use std::make_unique<T>(...) to create a std::unique_ptr. This approach is safer and results in cleaner code.

#include <iostream>
#include <memory> // This header file needs to be included to use smart pointers.

struct MyData {
int value;
MyData(int v) : value(v) {
std::cout << "MyData(" << value << ") constructed." << std::endl;
}
~MyData() {
std::cout << "MyData(" << value << ") destructed." << std::endl;
}
void print() const {
std::cout << "Value: " << value << std::endl;
}
};

void process_unique_data(std::unique_ptr<MyData> ptr) {
if (ptr) {
ptr->print();
} else {
std::cout << "Pointer is null." << std::endl;
}
}
// ptr goes out of scope here; if it owns an object, the object will be destroyed

int main() {
// Use std::make_unique to create a unique_ptr
std::unique_ptr<MyData> u_ptr1 = std::make_unique<MyData>(10);

if (u_ptr1) {
u_ptr1->print(); // Accessing members through ->
std::cout << "u_ptr1's data value: " << u_ptr1->value << std::endl;
}

// Transfer of Ownership
std::unique_ptr<MyData> u_ptr3 = std::move(u_ptr1);
// u_ptr1 is now empty

// Passing unique_ptr as a parameter to a function (ownership is transferred)
process_unique_data(std::move(u_ptr3));
// u_ptr3 becomes empty after calling process_unique_data because ownership has been transferred to the function
return 0;
// Auto-destruct
}

std::shared_ptr

  • Shared Ownership: std::shared_ptr allows multiple instances to jointly point to and own the same dynamically allocated object. It internally uses a reference count mechanism to track how many std::shared_ptr instances are sharing the object.

  • Reference Counting: When a new std::shared_ptr points to the object (e.g., through copy construction or assignment), the reference count increases. When a std::shared_ptr is destroyed or no longer points to the object, the reference count decreases.

  • Automatic Destruction: The object is automatically deleted only when the last std::shared_ptr pointing to it is destroyed, causing the reference count to drop to zero.

  • Creation Method: It is also recommended to use std::make_shared<T>(...) to create a std::shared_ptr.

#include <iostream>
#include <memory>
#include <vector>

struct SharedResource {
int id;
SharedResource(int i) : id(i) {
std::cout << "SharedResource(" << id << ") created." << std::endl;
}
~SharedResource() {
std::cout << "SharedResource(" << id << ") destroyed." << std::endl;
}
void use() const {
std::cout << "Using SharedResource(" << id << ")." << std::endl;
}
};

int main() {
// Using std::make_shared to create a shared_ptr
std::shared_ptr<SharedResource> s_ptr1 = std::make_shared<SharedResource>(101);

std::cout << "s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // Output 1

s_ptr1->use();

std::shared_ptr<SharedResource> s_ptr2 = s_ptr1; // Copy construction, reference count increases
std::cout << "s_ptr1 use_count: " << s_ptr1.use_count() << std::endl; // Output 2
std::cout << "s_ptr2 use_count: " << s_ptr2.use_count() << std::endl; // Output 2

std::vector<std::shared_ptr<SharedResource>> ptr_vector;
ptr_vector.push_back(s_ptr1);
ptr_vector.push_back(s_ptr2);
ptr_vector.push_back(std::make_shared<SharedResource>(202));

std::cout << "s_ptr1 use_count after vector push: " << s_ptr1.use_count() << std::endl; // Output 4 (s_ptr1, s_ptr2, vector[0], vector[1])
std::cout << "ptr_vector[2] use_count: " << ptr_vector[2].use_count() << std::endl; // Output 1

for (const auto& ptr : ptr_vector) {
ptr->use();
}

s_ptr1.reset(); // s_ptr1 No longer pointing to an object, reference count decreased.
std::cout << "After s_ptr1.reset(), s_ptr2 use_count: " << s_ptr2.use_count() << std::endl; // Output 3

ptr_vector.clear(); //The shared_ptr in the vector is destroyed, and the reference count decreases accordingly.
std::cout << "After vector.clear(), s_ptr2 use_count: " << s_ptr2.use_count() << std::endl; // Output 1

// s_ptr2 goes out of scope at the end of the main function, the reference count becomes 0, and SharedResource(101) is destroyed
// SharedResource(202) reference count becomes 0 and is destroyed after ptr_vector.clear()
return 0;
}

std::weak_ptr

  • Non-owning pointer: std::weak_ptr is an observer pointer that points to an object managed by std::shared_ptr, but it does not participate in the object’s reference counting. That is, it neither increases nor decreases the object’s strong reference count. Therefore, the existence of such a pointer does not prevent the object it points to from being destroyed.

  • Solving circular dependencies: The main purpose of std::weak_ptr is to break circular dependency issues that may arise with std::shared_ptr. When two or more objects strongly reference each other through std::shared_ptr, even if there are no external pointers pointing to them, their reference counts cannot drop to zero, leading to memory leaks. By using std::weak_ptr in one or more links of the circular reference chain, this cycle can be broken, allowing the objects to be properly reclaimed.

  • Checking object liveness: Since std::weak_ptr does not own the object, the object it points to may be destroyed during its lifetime. Therefore, before accessing the object pointed to by std::weak_ptr, it must be converted to a std::shared_ptr by calling its lock() method. If the object still exists, lock() returns a valid std::shared_ptr; otherwise, it returns an empty std::shared_ptr.

#include <iostream>
#include <memory>

struct NodeB; // Forward declaration

struct NodeA {
std::shared_ptr<NodeB> b_ptr;
NodeA() { std::cout << "NodeA created." << std::endl; }
~NodeA() { std::cout << "NodeA destroyed." << std::endl; }
};

struct NodeB {
// If `std::shared_ptr<NodeA> a_ptr;` is used here, it will cause a circular dependency.
std::weak_ptr<NodeA> a_ptr; // Using weak_ptr to break cycles
NodeB() { std::cout << "NodeB created." << std::endl; }
~NodeB() { std::cout << "NodeB destroyed." << std::endl; }

void check_a() {
if (auto locked_a = a_ptr.lock()) { // Attempt to obtain shared_ptr
std::cout << "NodeA is still alive." << std::endl;
// It is safe to use locked_a.
} else {
std::cout << "NodeA has been destroyed." << std::endl;
}
}
};

int main() {
std::shared_ptr<NodeA> a = std::make_shared<NodeA>();
std::shared_ptr<NodeB> b = std::make_shared<NodeB>();

a->b_ptr = b; // NodeA points to NodeB
b->a_ptr = a; // NodeB points to NodeA (via weak_ptr)

std::cout << "a use_count: " << a.use_count() << std::endl; // Usually 1 (a in main)
std::cout << "b use_count: " << b.use_count() << std::endl; // Usually 2 (b in main and a->b_ptr)

b->check_a();

// When a and b go out of scope:

// 1. a's shared_ptr is destroyed, NodeA's reference count becomes 0 (because b->a_ptr is a weak_ptr), and NodeA is destroyed.

// 2. When NodeA is destroyed, its member a->b_ptr (shared_ptr) is destroyed, reducing NodeB's reference count.

// 3. b's shared_ptr is destroyed. If NodeB's reference count becomes 0 at this point, NodeB is destroyed.

// If b->a_ptr were a shared_ptr, neither a nor b's reference counts could drop to 0, resulting in a memory leak.

return 0; // a and b are destroyed
}

Custom Deleter

The aforementioned std::shared_ptr and std::unique_ptr both support custom deleters. These provide a callable object for the smart pointers. When releasing resources, this callable object is invoked to perform the actual deallocation instead of the default delete.

Many resources are not allocated via new/delete, but are acquired and released through specific C APIs or library functions. For example:

  • FILE* (C file handle): acquired via fopen(), released via fclose().

  • HANDLE (Windows API handle): acquired via CreateEvent(), etc., released via CloseHandle().

  • sqlite3* (SQLite database connection): acquired via sqlite3_open(), released via sqlite3_close().

  • Memory allocated via malloc(): released via free().

In std::shared_ptr, the custom deleter is passed as the second argument to the constructor:

std::shared_ptr<T> ptr(raw_pointer, custom_deleter);

raw_pointer is a raw pointer pointing to the resource to be managed.

custom_deleter is a callable object that accepts a parameter of type T* and performs the release of the resource.

std::unique_ptr also supports custom deleters, but its syntax is slightly different because the deleter is part of its template parameters. This allows unique_ptr to know the type of the deleter at compile time, potentially resulting in lower memory overhead and faster runtime performance. The following example demonstrates its usage:

std::unique_ptr<FILE, decltype(&close_file_func)> unique_file_ptr(fopen("test_unique.txt", "w"), &close_file_func);
// Or use lambda
std::unique_ptr<FILE, decltype([](FILE* f){ if(f) fclose(f); })> unique_file_ptr_lambda(fopen("test_unique_lambda.txt", "w"), [](FILE* f){ if(f) fclose(f); });

Chapter 10 Design Concepts

Regarding code quality, there are two important concepts: coupling and cohesion.

10.1 Coupling

Coupling refers to the connections between different units in a program. If two classes depend on each other in many details, we consider them to be tightly coupled.

Our goal is to loosen this coupling, as it helps us understand one class without reading others and change one class without affecting others, thereby improving maintainability.

Methods to loosen coupling include:

  • Call-back

  • Message mechanism

10.2 Cohesion

Cohesion refers to the number and diversity of tasks for which a single unit is responsible. This concept applies to both classes and methods. If a unit is responsible for only one logical task, we say that unit has high cohesion.

Method cohesion: A method (function) should be responsible for only one well-defined task.

Class cohesion: A class should represent a single well-defined entity.

Our goal is to enhance cohesion, as it helps in understanding the role of a class or method, using descriptive names, and reusing classes or methods.

10.3 Responsibility-Driven Design

Each class should be responsible for the data it manipulates, so we aim to localize change—meaning that a single change should only affect a small number of classes, thereby reducing coupling and achieving responsibility-driven design.

10.4 Refactoring

When maintaining classes, we often see the code of classes and methods grow. Therefore, during maintenance, it is often necessary to refactor the code to maintain its cohesion and low coupling.

When refactoring code, do not let the refactoring change other parts of the code or the functionality of the code.

Before and after refactoring, the code needs to be checked to ensure that nothing has been broken.

Chapter 11 Streams

A stream is a common logical interface provided for devices, allowing values to be read or written sequentially.

Input Output Header
General scenario istream ostream <iostream>
File ifstream ofstream <fstream>
String istringstream ostringstream <sstream>
  • Extractor: reads values from a stream, overloaded operator >>

  • Insertor: inserts values into a stream, overloaded operator <<

  • Manipulator: changes the state of a stream

  • Others …

Predefined streams:

  • cin: standard input, an instance of std::istream

  • cout: standard output, an instance of std::ostream

  • cerr: unbuffered error (debug) output

  • clog: buffered error (debug) output


The predefined extractor istream >> lvalue ignores leading whitespace characters, and its definition is as follows:

istream& operator>>(istream& is, T& obj) 
{
// specific code to read obj
return is;
}

Other input operators:

  • int get(): Returns the next character in the stream, or EOF if there are no characters in the stream.

    int ch;
    while ((ch = cin.get()) != EOF)
    cout.put(ch);
  • istream& get(char &ch): Places the next character into the parameter, functioning similarly to the previous input operator.

  • istream& getline(istream& is, string& str, char delim = '\n');: Reads the input stream until the character delim is encountered, then stores the result in the buffer str.

  • ignore(int limit = 1, int delim = EOF): Skips limit characters and the delimiter.

  • int gcount(): Returns the number of characters just read.

    string buffer;
    getline(cin, buffer);
    cout << "read " << cin.gcount() << " characters"
  • void putback(char): Push a single character back into the stream

  • char peek(): Check the next character without reading it


The predefined inserter ostream << expression is defined as follows:

ostream& operator<<(ostream& os, const T& obj) 
{
// specific code to write obj
return os;
}

Other output operators:

  • put(char): prints a single character

    cout.put('a');
    cerr.put('!');
  • flush(): Force output stream content

    cout << "Enter a number";
    cout.flush();

11.1 Manipulators

Using manipulators for formatting, modifying stream state

Include header file: #include<iomanip>

Manipulator Effect Type
dec, hex, oct Set numeric conversion I, O
endl Insert a new line and flush the stream O
flush Flush the stream (also available with cout and endl) O
setw(int) Set field width I, O
setfill(char) Change fill character I, O
setbase(int) Set numeric base O
ws Skip whitespace characters I
setprecision(int) Set floating-point precision O
#include <iostream>
#include <iomanip>
int main()
{
cout << setprecision(2) << 1000.243 << endl;
cout << setw(20) << "OK!";
return 0;
}

Since each execution of endl flushes the stream buffer, it is relatively costly; therefore, to improve program performance, it is better to use the newline character '\n' instead of endl.

We can define custom manipulators:

// skeleton for an output stream manipulator
ostream& manip(ostream& out)
{
...
return out;
}
ostream& tab(ostream& out)
{
return out << '\t';
}
cout << "Hello" << tab << "World!" << endl;

11.2 Stream Control

std::ios defines the common functionality and status information for all input/output operations.

Stream flags control formatting:

Flag Purpose (when set)
ios::skipws Skip leading whitespace characters
ios::left, ios::right Alignment
ios::internal Fill between sign and value
ios::dec, ios::hex, ios::oct Numeric format
ios::showbase Show numeric base
ios::showpoint Always show decimal point
ios::uppercase Display base in uppercase
ios::showpos Show + for positive numbers

Setting flags:

  • Using manipulators: setiosflags(flags), resetiosflags(flags)

  • Using stream member functions: setf(flags), unsetf(flags)

#include <iostream>
#include <iomanip>
int main()
{
cout setf(ios::showpos | ios::scientific);
cout << 123 << " " << 456.78 << endl;
cout << resetiosflags(ios::showpos) << 123;
return 0;
}

Stream error status:

Status Check:

  • good(): Returns true when the state is valid

  • eof(): Returns true when EOF is encountered

  • fail(): Returns true when a minor failure or bad state occurs

  • bad(): Returns true when the state is bad

  • clear(): Used to reset the error state to good()

int n;
cout << "Enter a value for n, then [Enter]" << flush;
while (cin.good())
{
cin >> n;
if (cin)
{ // input was ok
cin.ignore(INT_MAX, '\n'); // flush newline
break;
}
if (cin.fail())
{
cin.clear(); // clear the error state
cin.ignore(INT_MAX, '\n'); // skip garbage
cout << "No good, try again!" << flush;
}
}

11.3 File Streams

  • ifstream and ofstream connect files with streams

  • Need to include the header file: #!cpp #include <fstream>

  • Use modes to specify how to create the file:

Mode Purpose
ios:app Append
ios:ate At end
ios:binary Handle binary I/O
ios:in Open file for input
ios:out Open file for output

Related stream operations:

  • open(const char *, int flags, int): Open the specified file

  • is_open(): Check if the file is open

  • close(): Close the stream

  • fail()

int main() {
/// associating file on construction
std::ofstream ofs(“hello.txt”)
if (ofs.is_open()) {
ofs << “Hello CS106L!” << ‘\n’;
}
ofs.close();
ofs << “this will not get written”;
ofs.open(“hello.txt”, std::ios::app);
ofs << “this will though! It’s open again”;
return 0;
}
int inputFileStreamExample() {
std::ifstream ifs(“append.txt”)
if (ifs.is_open()) {
std::string line;
std::getline(ifs, line);
std::cout << “Read from the file: “ << line << ‘\n’;
}
if (ifs.is_open()) {
std::string lineTwo;
std::getline(ifs, lineTwo);
std::cout << “Read from the file: “ << lineTwo << ‘\n’;
}
return 0;
}

11.4 I/0 Stream Buffers

Every I/O has a stream buffer

The class streambuf defines the abstraction of this buffer

The member function rdbuf() returns a pointer to the stream buffer

<< is overloaded to directly connect buffers

#include <fstream>
#include <assert>
int main(int argc, char *argv[])
{
assert(argc == 2);
ifstream in(argv[1]);
assert(in); // check that stream opened
cout << in.rdbuf(); // Drain file!
}

About this Post

This post is written by Rinic, licensed under CC BY-NC 4.0.