Master systems programming with modern C++: from fundamentals to C++23, including templates, concurrency, and advanced metaprogramming.
February 2026
C++ is built on a fundamental principle: “What you don’t use, you don’t pay for. And what you do use, you couldn’t hand code any better.” This zero-overhead abstraction principle distinguishes C++ from its contemporaries. Unlike Java or C#, which enforce garbage collection overhead, C++ provides high-level abstractions that compile down to machine code equivalent to hand-optimized C.
The language offers three programming paradigms that coexist seamlessly:
C++ has evolved dramatically since Bjarne Stroustrup’s original design in 1979. Understanding this evolution is crucial for reading existing codebases and writing idiomatic modern code.
| Standard | Year | Key Features |
|---|---|---|
| C++98/03 | 1998/2003 | STL, templates, exceptions, namespaces |
| C++11 | 2011 | Auto, lambdas, smart pointers, move semantics, concurrency |
| C++14 | 2014 | Generic lambdas, return type deduction, binary literals |
| C++17 | 2017 | Structured bindings, std::optional, filesystem, parallel algorithms |
| C++20 | 2020 | Concepts, ranges, coroutines, modules, three-way comparison |
| C++23 | 2023 | std::expected, monadic operations, multidimensional subscript |
Unlike interpreted languages, C++ undergoes a multi-stage compilation process that enables aggressive optimization.
C++ supports function overloading, templates, and namespaces, which requires name mangling: the compiler transforms human-readable names into unique symbols. For example, void foo(int) might become _Z3fooi in the object file.
The One Definition Rule (ODR) states that every entity can have only one definition across all translation units. Violating ODR results in undefined behavior, though linkers often fail to detect violations.
C++ is statically typed with strong type checking at compile time. However, it also provides mechanisms for compile-time polymorphism through templates, eliminating runtime dispatch overhead.
// Runtime polymorphism (virtual dispatch)
class Animal { virtual void speak() = 0; };
class Dog : public Animal { void speak() override; };
// Compile-time polymorphism (template instantiation)
template<typename T>
void process(T& obj) { obj.speak(); }
The template version generates specialized code for each type, enabling inlining and optimization impossible with virtual functions.
C++ provides direct control over memory layout and lifetime. Unlike garbage-collected languages, C++ uses deterministic destruction through RAII (Resource Acquisition Is Initialization): resources are tied to object lifetimes.
| Storage Duration | Lifetime | Example |
|---|---|---|
| Automatic | Scope-based | Local variables |
| Static | Program duration | Global variables, static locals |
| Dynamic | Manual (new/delete) | Heap allocations |
| Thread | Thread duration | thread_local variables |
Modern C++ heavily favors automatic storage with smart pointers (unique_ptr, shared_ptr) managing dynamic memory automatically.
#include <iostream> int () { std::cout << "Modern C++ initialized\n"; return 0; }
Like C, C++ defines behavior in terms of an abstract machine. Operations outside the specification result in undefined behavior (UB), where the compiler may assume such cases never occur and optimize accordingly.
Common sources of UB:
Modern compilers include sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer) to detect UB at runtime during development.
Waiting for signal...
C++ provides a rich type system that balances low-level control with high-level abstractions. Every type belongs to one of several categories, each with distinct properties regarding copying, lifetime, and memory representation.
| Category | Types | Guaranteed Properties |
|---|---|---|
| Boolean | bool | true or false, implementation-defined size |
| Character | char, wchar_t, char8_t, char16_t, char32_t | Character encoding representation |
| Integer | short, int, long, long long | Minimum sizes specified |
| Floating-Point | float, double, long double | IEEE 754 typical but not mandated |
| Void | void | Incomplete type, used for “no value” |
C++ guarantees size relationships: sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long). However, exact sizes are implementation-defined. Use <cstdint> for fixed-width types: int32_t, uint64_t, etc.
C++11 introduced auto for automatic type deduction, reducing verbosity while maintaining static typing. The compiler deduces types from initializers at compile time—there’s no runtime overhead.
auto x = 42; // int
auto y = 3.14; // double
auto z = "hello"; // const char*
auto ptr = new int{5}; // int*
// Complex types become manageable
auto it = vec.begin(); // std::vector<T>::iterator
auto lambda = [](int x) { return x * 2; }; // Closure type
The deduction follows template argument deduction rules with some exceptions:
auto& preserves lvalue referencesauto removes top-level const (use const auto)auto&int arr[5] = {1,2,3,4,5}; p = arr; // Deduces to int* auto& r = arr; // Deduces to int(&)[5]
While auto deduces from initializers, decltype queries the type of any expression without evaluating it.
int x = 10;
decltype(x) y = 20; // y is int
int& getRef();
decltype(getRef()) z = x; // z is int&
// Combining auto and decltype (C++14 return type deduction)
auto func(int x) -> decltype(x * 2) {
return x * 2; // Return type is int
}
C++14 added decltype(auto) for perfect forwarding of return types:
template<typename Container>
decltype(auto) getElement(Container& c, size_t i) {
return c[i]; // Preserves reference if container returns reference
}
The const qualifier is fundamental to C++‘s type system, enabling the compiler to enforce immutability guarantees and enable optimizations.
const int max_retries = 3; // Cannot be modified
// max_retries = 5; // Compile error
int const alt_syntax = 5; // Equivalent to const int
The placement of const determines what is immutable:
int value = 10;
const int* ptr1 = &value; // Pointer to const int (can't modify *ptr1)
int* const ptr2 = &value; // Const pointer to int (can't modify ptr2)
const int* const ptr3 = &value; // Both const
*ptr1 = 20; // Error: can't modify through ptr1
ptr1 = nullptr; // OK: pointer itself is mutable
*ptr2 = 20; // OK: can modify through ptr2
ptr2 = nullptr; // Error: pointer itself is const
Mnemonic: Read right-to-left. const int* is “pointer to const int.”
Member functions can be marked const, promising not to modify object state:
class Point {
int x_, y_;
public:
int getX() const { return x_; } // Doesn't modify object
void setX(int x) { x_ = x; } // Modifies object
};
const Point p{10, 20};
p.getX(); // OK
// p.setX(5); // Error: can't call non-const method on const object
C++11 introduced using for type aliases, superseding typedef:
// Old style (C++98)
typedef std::vector<int> IntVector;
typedef void (*FunctionPtr)(int);
// Modern style (C++11+)
using IntVector = std::vector<int>;
using FunctionPtr = void(*)(int);
// Template aliases (only possible with using)
template<typename T>
using Vec = std::vector<T>;
Vec<int> v; // Equivalent to std::vector<int>
Integer types can be signed or unsigned. Mixing them in expressions causes implicit conversions that can be surprising:
int signed_val = -1;
unsigned int unsigned_val = 1;
if (signed_val < unsigned_val) {
// This branch is NOT taken!
// -1 converts to a large unsigned value
}
Best Practice: Prefer signed types for arithmetic, use unsigned only for bit manipulation or when the domain is truly non-negative (e.g., array indices—though even this is debated).
Waiting for signal...
Unlike C, C++ allows multiple functions with the same name but different parameter lists. The compiler selects the appropriate overload through overload resolution at compile time based on argument types.
void print(int x) { std::cout << "int: " << x; }
void print(double x) { std::cout << "double: " << x; }
void print(const char* x) { std::cout << "string: " << x; }
print(42); // Calls print(int)
print(3.14); // Calls print(double)
print("hello"); // Calls print(const char*)
The compiler follows a precise algorithm:
char → int) or float → doubleIf multiple functions match at the same level, the call is ambiguous and compilation fails.
void process(int x) { }
void process(double x) { }
process(42L); // Error: ambiguous (long can convert to int or double)
void func(int) { } void func(double) { } void func(const char*) { } func(); // Calls func(int) - exact match
Functions can specify default values for trailing parameters. Defaults are evaluated at call site, not function definition.
void configure(int timeout = 30, bool verbose = false);
configure(); // configure(30, false)
configure(60); // configure(60, false)
configure(60, true); // configure(60, true)
// configure(, true); // Error: can't skip non-trailing arguments
Best Practice: Declare defaults in header files (declarations), not source files (definitions). This ensures callers see the defaults.
// header.hpp
void setup(int port = 8080);
// source.cpp
void setup(int port) { // No default here
// implementation
}
The inline keyword suggests the compiler should substitute the function body at call sites, eliminating call overhead. Modern compilers ignore this hint and inline based on optimization heuristics.
However, inline has a critical semantic meaning: it relaxes the One Definition Rule, allowing identical definitions in multiple translation units.
// header.hpp
inline int square(int x) {
return x * x;
}
// Can be included in multiple .cpp files without linker errors
Modern Usage: inline is essential for defining functions in headers, especially templates and constexpr functions (implicitly inline).
Templates enable generic programming by allowing functions to operate on any type satisfying certain requirements.
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
auto x = max(10, 20); // T = int
auto y = max(3.14, 2.71); // T = double
auto z = max<long>(5, 10); // Explicit T = long
The compiler deduces template parameters from function arguments. Deduction fails if types are ambiguous:
template<typename T>
void process(T a, T b) { }
process(10, 20); // OK: T = int
process(10, 3.14); // Error: T is int or double?
process<double>(10, 3.14); // OK: explicit T = double
You can provide specialized implementations for specific types:
template<typename T>
T zero() { return T{}; }
template<>
const char* zero<const char*>() { return ""; }
C++11 allows return type after parameter list, useful when return type depends on parameters:
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
C++14 simplified this with return type deduction:
template<typename T, typename U>
auto add(T t, U u) {
return t + u; // Type deduced from return statement
}
Functions marked constexpr can execute at compile time if arguments are constant expressions:
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr auto result = factorial(5); // Computed at compile time
int arr[factorial(4)]; // Array size must be compile-time constant
C++14 relaxed restrictions, allowing loops and multiple statements in constexpr functions.
Waiting for signal...
References are aliases to existing objects, unlike pointers which are objects themselves. C++ provides two kinds: lvalue references (T&) and rvalue references (T&&, C++11).
int x = 10;
int& ref = x; // Lvalue reference to x
ref = 20; // Modifies x
int* ptr = &x; // Pointer is a separate object
*ptr = 30; // Modifies x through pointer
int& & is illegal (but see reference collapsing)int a = 5;
int& r1 = a;
// int& r2; // Error: must be initialized
// r1 = b; // This assigns b's value to a, doesn't rebind r1
Every C++ expression belongs to a value category. The fundamental distinction:
int x = 10; // x is an lvalue
int* p = &x; // OK: can take address of lvalue
// int* p = &10; // Error: can't take address of rvalue
// int& r = 42; // Error: lvalue ref can't bind to rvalue
C++11 introduced finer categories:
Rvalue references (T&&) bind to temporaries, enabling move semantics: transferring resources instead of copying.
std::string s1 = "hello";
std::string s2 = s1; // Copy: s1 still valid
std::string s3 = std::move(s1); // Move: s1 left in valid but unspecified state
void process(std::string&& s) {
// s is an rvalue reference (but s itself is an lvalue!)
}
process(std::string("temp")); // OK: binds to temporary
process(std::move(s2)); // OK: std::move casts to rvalue
// process(s2); // Error: lvalue can't bind to &&
Inside a function, an rvalue reference parameter is itself an lvalue (it has a name):
void func(std::string&& s) {
std::string copy = s; // Copy, not move (s is lvalue)
std::string moved = std::move(s); // Move (explicitly cast to rvalue)
}
A const T& can bind to both lvalues and rvalues, making it perfect for read-only parameters:
void print(const std::string& s) { // Accepts anything
std::cout << s;
}
std::string str = "hello";
print(str); // OK: binds to lvalue
print("world"); // OK: binds to temporary
print(str + " world"); // OK: binds to temporary result
This is why pre-C++11 code used const T& everywhere: it’s the only reference type that accepts both lvalues and rvalues.
When references are formed through templates or typedef, C++ applies reference collapsing:
T& & → T&
T& && → T&
T&& & → T&
T&& && → T&&
Rule: An rvalue reference to an rvalue reference becomes rvalue reference; everything else becomes lvalue reference.
template<typename T>
void wrapper(T&& arg) { // Forwarding reference (see next section)
// If T is int&, then T&& becomes int& (reference collapsing)
// If T is int, then T&& is int&&
}
In template contexts, T&& has special meaning—it’s a forwarding reference that can bind to anything:
template<typename T>
void forward_wrapper(T&& arg) { // Forwarding reference
target(std::forward<T>(arg)); // Perfect forwarding
}
int x = 10;
forward_wrapper(x); // T = int&, arg is int&
forward_wrapper(10); // T = int, arg is int&&
std::forward<T> preserves the value category: forwards lvalues as lvalues, rvalues as rvalues.
int x = 5; int& r = x; r = 10; std::cout << ; // Outputs 10
Waiting for signal...
Classes are the foundation of object-oriented programming in C++. They combine data (members) and behavior (methods) into cohesive types with controlled access.
class Point {
private:
double x_, y_; // Data members (by convention, trailing underscore)
public:
Point(double x, double y) : x_(x), y_(y) {} // Constructor
double distance() const { // Const member function
return std::sqrt(x_ * x_ + y_ * y_);
}
void setX(double x) { x_ = x; } // Mutator
double getX() const { return x_; } // Accessor
};
C++ provides three access levels:
Default access: class defaults to private, struct defaults to public.
class MyClass {
int private_by_default;
public:
int explicitly_public;
};
struct MyStruct {
int public_by_default;
private:
int explicitly_private;
};
Convention: Use class for types with invariants requiring encapsulation; use struct for passive data containers (POD types).
Constructors initialize objects. C++ provides several kinds:
class Widget {
public:
Widget() : value_(0) {} // Default constructor
private:
int value_;
};
Widget w; // Calls default constructor
class Point {
public:
Point(double x, double y) : x_(x), y_(y) {}
private:
double x_, y_;
};
Point p(3.0, 4.0); // Direct initialization
Point q = {5.0, 6.0}; // Aggregate initialization (C++11)
Always prefer initializer lists over assignment in constructor body:
class Container {
std::string name_;
std::vector<int> data_;
public:
// Good: Direct initialization
Container(std::string n) : name_(n), data_(100) {}
// Bad: Default construction then assignment
// Container(std::string n) { name_ = n; data_.resize(100); }
};
Reasons:
Constructors can delegate to other constructors:
class Rectangle {
double width_, height_;
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
Rectangle() : Rectangle(0, 0) {} // Delegates to above
};
Destructors execute when objects are destroyed (scope exit, delete, exception). They’re the foundation of RAII (Resource Acquisition Is Initialization): tying resource lifetime to object lifetime.
class FileHandle {
FILE* file_;
public:
FileHandle(const char* path) : file_(fopen(path, "r")) {
if (!file_) throw std::runtime_error("Failed to open");
}
~FileHandle() {
if (file_) fclose(file_); // Always cleanup
}
// Prevent copying (for now)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void process() {
FileHandle f("data.txt");
// Use file...
} // Destructor automatically closes file, even if exceptions thrown
The compiler auto-generates certain functions if not user-declared:
T(const T&)T& operator=(const T&)T(T&&) (C++11)T& operator=(T&&) (C++11)Best Practice: If you can, don’t declare any of the special members. Let the compiler generate them. Use standard library types that manage resources correctly.
class Good {
std::string name_;
std::vector<int> data_;
// Compiler-generated special members are correct
};
If you declare any of destructor, copy constructor, or copy assignment, you should probably declare all three (Rule of Three). In modern C++, also declare move constructor and move assignment (Rule of Five).
class Buffer {
char* data_;
size_t size_;
public:
// Constructor
Buffer(size_t s) : data_(new char[s]), size_(s) {}
// Destructor
~Buffer() { delete[] data_; }
// Copy constructor
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new char[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
return *this;
}
// Move constructor
Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
};
However, prefer std::vector<char> and Rule of Zero!
Waiting for signal...
C++ allows redefining operators for user-defined types, enabling natural syntax for custom abstractions. Operators are functions with special names: operator+, operator[], operator<<, etc.
class Complex {
double real_, imag_;
public:
Complex(double r = 0, double i = 0) : real_(r), imag_(i) {}
// Member operator
Complex operator+(const Complex& other) const {
return Complex(real_ + other.real_, imag_ + other.imag_);
}
Complex& operator+=(const Complex& other) {
real_ += other.real_;
imag_ += other.imag_;
return *this;
}
};
Complex a(1, 2), b(3, 4);
Complex c = a + b; // Calls a.operator+(b)
Member operators: Left operand is *this
class Vector {
public:
Vector operator+(const Vector& rhs) const; // this + rhs
};
Non-member operators: Both operands are parameters
Vector operator+(const Vector& lhs, const Vector& rhs);
When to use each:
-, ++, *), compound assignment (+=, *=), subscript ([]), call (()), member access (->)+, -, *), stream operators (<<, >>)class Rational {
int num_, den_;
friend Rational operator+(const Rational& lhs, const Rational& rhs);
public:
Rational(int n, int d = 1) : num_(n), den_(d) {}
};
// Non-member allows: 2 + Rational(3,4) via implicit conversion
Rational operator+(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.num_ * rhs.den_ + rhs.num_ * lhs.den_,
lhs.den_ * rhs.den_);
}
Implement compound assignment (+=), then derive binary operator (+):
class Number {
int value_;
public:
Number& operator+=(const Number& rhs) {
value_ += rhs.value_;
return *this;
}
};
// Non-member, defined in terms of +=
Number operator+(Number lhs, const Number& rhs) {
lhs += rhs; // lhs is a copy, so modify it
return lhs;
}
C++20 introduced the spaceship operator (<=>), but pre-C++20:
class Point {
int x_, y_;
public:
bool operator==(const Point& other) const {
return x_ == other.x_ && y_ == other.y_;
}
bool operator!=(const Point& other) const {
return !(*this == other); // Implement in terms of ==
}
bool operator<(const Point& other) const {
return (x_ < other.x_) || (x_ == other.x_ && y_ < other.y_);
}
// Define >, <=, >= in terms of < and ==
};
C++20 spaceship simplifies this:
class Point {
int x_, y_;
public:
auto operator<=>(const Point&) const = default; // Generates all comparisons
};
Prefix returns reference; postfix returns copy:
class Counter {
int count_;
public:
// Prefix: ++c
Counter& operator++() {
++count_;
return *this;
}
// Postfix: c++
Counter operator++(int) { // Dummy int parameter
Counter temp = *this;
++(*this); // Use prefix version
return temp;
}
};
class Array {
int* data_;
size_t size_;
public:
int& operator[](size_t index) {
return data_[index];
}
const int& operator[](size_t index) const {
return data_[index];
}
};
C++23 allows multidimensional subscript:
class Matrix {
public:
double& operator[](size_t row, size_t col); // C++23
};
Always non-member to allow std::cout << obj:
class Person {
std::string name_;
int age_;
friend std::ostream& operator<<(std::ostream&, const Person&);
public:
Person(std::string n, int a) : name_(n), age_(a) {}
};
std::ostream& operator<<(std::ostream& os, const Person& p) {
return os << p.name_ << " (" << p.age_ << ")";
}
Allow implicit or explicit conversion to other types:
class Fraction {
int num_, den_;
public:
// Implicit conversion to double
operator double() const {
return static_cast<double>(num_) / den_;
}
// Explicit conversion to bool (C++11)
explicit operator bool() const {
return num_ != 0;
}
};
Fraction f(3, 4);
double d = f; // Implicit: calls operator double()
// bool b = f; // Error: explicit conversion required
bool b = static_cast<bool>(f); // OK
if (f) { } // OK: contextual conversion to bool
Waiting for signal...
Inheritance models taxonomic relationships where a derived class “is a” specialized version of a base class. C++ supports single and multiple inheritance with three access modes.
class Shape {
protected:
std::string color_;
public:
Shape(std::string c) : color_(c) {}
virtual double area() const = 0; // Pure virtual
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius_;
public:
Circle(std::string c, double r) : Shape(c), radius_(r) {}
double area() const override {
return 3.14159 * radius_ * radius_;
}
};
| Derivation | Base public → Derived | Base protected → Derived | Base private → Derived |
|---|---|---|---|
| public | public | protected | inaccessible |
| protected | protected | protected | inaccessible |
| private | private | private | inaccessible |
Public inheritance models “is-a”: Circle is-a Shape. Most common. Protected inheritance models “implemented-in-terms-of” (rare). Private inheritance models “implemented-in-terms-of” (prefer composition).
class Stack : private std::vector<int> { // Private inheritance
public:
void push(int x) { push_back(x); }
int pop() { int x = back(); pop_back(); return x; }
};
// Stack is NOT a std::vector publicly
Virtual functions enable runtime polymorphism: the called function depends on the dynamic type of the object, not the static type of the pointer/reference.
class Animal {
public:
virtual void speak() const {
std::cout << "Some sound\\n";
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override { // override keyword: C++11
std::cout << "Woof!\\n";
}
};
void makeNoise(const Animal& a) {
a.speak(); // Dynamic dispatch
}
Dog d;
makeNoise(d); // Outputs: Woof!
Virtual functions are implemented via a virtual table (vtable): a compile-time array of function pointers for each polymorphic class. Each object contains a hidden vptr pointing to its class’s vtable.
Cost: One pointer per object, one indirection per virtual call. This is the “overhead” of polymorphism.
A pure virtual function has no implementation and is declared with = 0. Classes with pure virtuals are abstract: cannot be instantiated.
class Drawable {
public:
virtual void draw() const = 0; // Pure virtual
virtual ~Drawable() = default;
};
class Rectangle : public Drawable {
public:
void draw() const override {
// Implementation
}
};
// Drawable d; // Error: cannot instantiate abstract class
Rectangle r; // OK: Rectangle is concrete
Interface idiom: Abstract class with only pure virtuals and no data members.
C++11 added override and final to catch errors:
class Base {
public:
virtual void foo(int x);
virtual void bar() const;
};
class Derived : public Base {
public:
void foo(double x) override; // Error: doesn't override (different signature)
void bar() override; // Error: doesn't override (missing const)
};
final prevents further overriding or derivation:
class Base {
public:
virtual void method() final; // Cannot be overridden
};
class Derived final : public Base { // Cannot be further derived
};
If a class has virtual functions, its destructor must be virtual. Otherwise, deleting a derived object through a base pointer causes undefined behavior:
class Base {
public:
~Base() { std::cout << "Base destroyed\\n"; }
};
class Derived : public Base {
int* data_;
public:
Derived() : data_(new int[100]) {}
~Derived() { delete[] data_; } // CRITICAL: must run
};
Base* p = new Derived;
delete p; // UB: Only Base destructor runs, memory leak!
Solution: Make base destructor virtual:
class Base {
public:
virtual ~Base() { } // Virtual destructor
};
Assigning a derived object to a base object slices off the derived parts:
class Base { int x_; };
class Derived : public Base { int y_; };
Derived d;
Base b = d; // Slicing: y_ is lost
Base& ref = d; // OK: Reference preserves dynamic type
Base* ptr = &d; // OK: Pointer preserves dynamic type
Rule: Pass polymorphic types by pointer or reference, never by value.
Waiting for signal...
Templates enable writing code that works with any type satisfying certain requirements, with full type checking at compile time and zero runtime overhead. Unlike runtime polymorphism (virtual functions), templates generate specialized code for each type used.
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
auto x = max(10, 20); // T = int
auto y = max(3.14, 2.71); // T = double
auto z = max<long>(5, 10); // Explicit T = long
The compiler deduces template parameters from function arguments. Deduction follows strict rules:
template<typename T>
void process(T value, T* ptr);
int x = 5;
process(x, &x); // OK: T = int
double d = 3.14;
// process(x, &d); // Error: T is int or double?
template<typename T>
class Stack {
std::vector<T> data_;
public:
void push(const T& value) { data_.push_back(value); }
T pop() {
T value = data_.back();
data_.pop_back();
return value;
}
bool empty() const { return data_.empty(); }
};
Stack<int> intStack;
Stack<std::string> stringStack;
Templates can appear at multiple levels:
template<typename T>
class Container {
public:
// Member function template
template<typename U>
void insert(U&& value) {
// ...
}
};
Container<int> c;
c.insert(42); // U = int
c.insert("hello"); // U = const char*
Provide a completely different implementation for a specific type:
template<typename T>
class TypeName {
public:
static const char* get() { return "unknown"; }
};
template<>
class TypeName<int> {
public:
static const char* get() { return "int"; }
};
template<>
class TypeName<double> {
public:
static const char* get() { return "double"; }
};
Specialize for a subset of template parameters:
template<typename T, typename U>
class Pair {
T first_;
U second_;
};
// Partial specialization: both types the same
template<typename T>
class Pair<T, T> {
T first_, second_;
public:
bool equal() const { return first_ == second_; }
};
// Partial specialization: pointer types
template<typename T>
class Pair<T*, T*> {
// Special implementation for pointers
};
Templates can take compile-time constant values:
template<typename T, size_t N>
class Array {
T data_[N];
public:
constexpr size_t size() const { return N; }
T& operator[](size_t i) { return data_[i]; }
const T& operator[](size_t i) const { return data_[i]; }
};
Array<int, 10> arr1; // Different type from...
Array<int, 20> arr2; // ...this
Non-type parameters can be: integers, enums, pointers, references, nullptr_t, and (C++20) floating-point and class types.
Templates accepting arbitrary numbers of arguments:
template<typename... Args>
void print(Args... args) {
((std::cout << args << ' '), ...); // Fold expression (C++17)
std::cout << '\\n';
}
print(1, 2.5, "hello", 'x'); // Any number/types of arguments
template<typename... Ts>
class Tuple; // Declaration
// Recursive inheritance
template<typename T, typename... Ts>
class Tuple<T, Ts...> : public Tuple<Ts...> {
T value_;
public:
Tuple(T v, Ts... vs) : Tuple<Ts...>(vs...), value_(v) {}
};
template<>
class Tuple<> { // Base case
};
When template substitution fails, the candidate is removed from overload set instead of causing error:
template<typename T>
typename T::value_type getValue(T container) { // Only if T has value_type
return container[0];
}
template<typename T>
T getValue(T value) { // Fallback
return value;
}
std::vector<int> vec{1, 2, 3};
auto x = getValue(vec); // Calls first overload
auto y = getValue(42); // Calls second overload (first SFINAE'd out)
Modern C++ uses std::enable_if for SFINAE:
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
increment(T value) {
return value + 1;
}
increment(10); // OK
// increment(3.14); // Error: no matching function
C++20 concepts provide cleaner syntax:
template<std::integral T>
T increment(T value) {
return value + 1;
}
Waiting for signal...
Raw pointers require manual memory management with new/delete, leading to leaks, double-deletes, and dangling pointers. Smart pointers from <memory> automate lifetime management through RAII.
unique_ptr<T> represents exclusive ownership: exactly one owner, non-copyable but movable. The pointed-to object is destroyed when the unique_ptr is destroyed.
#include <memory>
std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1; // Error: cannot copy
std::unique_ptr<int> p2 = std::move(p1); // OK: transfer ownership
// p1 is now nullptr
auto p3 = std::make_unique<int>(100); // Preferred (C++14)
// Problematic:
process(std::unique_ptr<T>(new T), might_throw());
// If might_throw() throws after new but before unique_ptr construction, leak!
// Safe:
process(std::make_unique<T>(), might_throw());
// make_unique is atomic: allocation and unique_ptr construction happen together
std::unique_ptr<int[]> arr(new int[10]);
arr[0] = 5; // Operator[] available for array version
// Prefer std::vector or std::array
std::vector<int> vec(10); // Better
struct FileCloser {
void operator()(FILE* fp) const {
if (fp) fclose(fp);
}
};
std::unique_ptr<FILE, FileCloser> file(fopen("data.txt", "r"));
// File automatically closed when unique_ptr destroyed
shared_ptr<T> uses reference counting: the object is destroyed when the last shared_ptr owning it is destroyed. Copyable and movable.
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::shared_ptr<int> p2 = p1; // Both own the object
// Reference count: 2
p1.reset(); // p1 releases ownership, count = 1
// p2 still valid, object alive
Each shared_ptr has two pointers:
// Preferred: single allocation for object + control block auto p1 = <int>(42); // Avoid: two allocations std::shared_ptr<int> p2(new int(42));
struct Node {
std::shared_ptr<Node> next;
};
std::shared_ptr<Node> a = std::make_shared<Node>();
std::shared_ptr<Node> b = std::make_shared<Node>();
a->next = b; // a owns b, count = 2
b->next = a; // b owns a, count = 2
// Neither can be destroyed: memory leak!
weak_ptr<T> observes a shared_ptr without affecting reference count. Used to break cycles and check if object still exists.
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Doesn't own
};
std::shared_ptr<Node> a = std::make_shared<Node>();
std::shared_ptr<Node> b = std::make_shared<Node>();
a->next = b;
b->prev = a; // Weak reference: no cycle
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
if (auto locked = wp.lock()) { // Attempt to get shared_ptr
std::cout << *locked << '\\n'; // Object still exists
} else {
std::cout << "Object destroyed\\n";
}
| Aspect | unique_ptr | shared_ptr |
|---|---|---|
| Ownership | Exclusive | Shared |
| Copyable | No | Yes |
| Size | Size of pointer | 2 pointers |
| Overhead | Zero | Reference counting (atomic for thread safety) |
| Use When | Clear single owner | Unclear ownership, need sharing |
Rule of Thumb: Default to unique_ptr. Use shared_ptr only when ownership truly must be shared.
// Return unique_ptr when transferring ownership
std::unique_ptr<Widget> createWidget() {
return std::make_unique<Widget>();
}
auto w = createWidget(); // Ownership transferred via move
// Prefer returning by value for copyable types
Widget createWidget2() {
return Widget{}; // Copy elision / move
}
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {};
std::unique_ptr<Base> p = std::make_unique<Derived>();
// Polymorphism works: Derived destructor called when p destroyed
int* raw = new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // DISASTER: double-delete!
// Each shared_ptr has independent control block
Never create multiple shared_ptr from the same raw pointer. Use make_shared or copy existing shared_ptr.
Waiting for signal...
Before C++11, returning or passing large objects by value meant expensive copies. Move semantics allow transferring resources from temporary objects instead of copying them.
class Buffer {
char* data_;
size_t size_;
public:
// Constructor
Buffer(size_t s) : data_(new char[s]), size_(s) {}
// Copy constructor (deep copy)
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
// Move constructor (transfer ownership)
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
~Buffer() { delete[] data_; }
};
Move operations should be noexcept when possible—this enables optimizations in standard containers:
class Widget {
std::string name_;
std::vector<int> data_;
public:
// Move constructor
Widget(Widget&& other) noexcept
: name_(std::move(other.name_)),
data_(std::move(other.data_)) {
}
// Move assignment
Widget& operator=(Widget&& other) noexcept {
if (this != &other) {
name_ = std::move(other.name_);
data_ = std::move(other.data_);
}
return *this;
}
};
Note: std::move on member variables is necessary because a named rvalue reference (like other) is itself an lvalue!
std::move doesn’t move anything—it just casts its argument to an rvalue reference, enabling move semantics:
std::string s1 = "hello";
std::string s2 = std::move(s1); // s1's data moved to s2
// s1 is now in "valid but unspecified" state
After std::move: The object remains alive and destructible, but its value is unspecified. Only reassign or destroy it.
std::string s = "hello";
std::string t = std::move(s);
// s.clear(); // OK: Reset to known state
// s = "new"; // OK: Reassign
// auto len = s.size(); // DANGEROUS: Value unspecified
Some types cannot be copied, only moved: unique_ptr, thread, iostream, etc.
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // Error: cannot copy
std::unique_ptr<int> p2 = std::move(p1); // OK: move
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(10)); // Move into vector
Compilers can elide copies/moves when returning by value:
Widget createWidget() {
return Widget{}; // RVO: No move/copy, constructed directly in caller
}
Widget createWidget2() {
Widget w;
// ...
return w; // NRVO: Named Return Value Optimization (maybe)
}
Don’t std::move return values—it inhibits RVO:
Widget createWidget() {
Widget w;
return std::move(w); // BAD: Prevents NRVO, forces move
}
When writing wrapper functions, you want to forward arguments exactly as received—preserving lvalue/rvalue-ness. This requires forwarding references (a.k.a. universal references).
template<typename T>
void wrapper(T&& arg) { // Forwarding reference
target(std::forward<T>(arg)); // Perfect forwarding
}
int x = 10;
wrapper(x); // T = int&, arg forwarded as lvalue
wrapper(10); // T = int, arg forwarded as rvalue
wrapper(std::move(x)); // T = int, arg forwarded as rvalue
T&& is a forwarding reference only when:
T is a template type parametertemplate<typename T>
void func(T&& x); // Forwarding reference
template<typename T>
void func(std::vector<T>&& x); // NOT forwarding reference (no deduction for &&)
void func(int&& x); // NOT forwarding reference (not a template)
Combining variadic templates with perfect forwarding:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
auto p = make_unique<std::pair<int, std::string>>(42, "hello");
template<typename T>
void process(T&& arg) {
// Use arg multiple times...
target(std::forward<T>(arg)); // Forward on last use
}
Move operations should be noexcept when possible. Standard containers use move only if noexcept:
class Widget {
public:
Widget(Widget&& other) noexcept { /* ... */ }
Widget& operator=(Widget&& other) noexcept { /* ... */ }
};
std::vector<Widget> vec;
vec.push_back(Widget{}); // Uses move (noexcept)
// If move is not noexcept, vector uses copy (for strong exception guarantee)
Waiting for signal...
C++11 introduced lambdas: inline anonymous function objects with concise syntax. Lambdas are extensively used with algorithms, callbacks, and asynchronous operations.
auto sum = [](int a, int b) { return a + b; };
std::cout << sum(3, 4); // Outputs: 7
std::vector<int> v = {4, 2, 5, 1, 3};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
// v is now {5, 4, 3, 2, 1}
[captures](parameters) specifiers -> return_type { body }
())mutable, constexpr, noexceptauto f = [] { return 42; }; // No parameters, deduced return type
Lambdas can capture variables from their enclosing scope:
int x = 10;
auto lambda = [x]() { return x * 2; }; // Captures x by value
x = 20;
std::cout << lambda(); // Outputs: 20 (captured value at lambda creation)
int x = 10;
auto lambda = [&x]() { return x * 2; }; // Captures x by reference
x = 20;
std::cout << lambda(); // Outputs: 40 (current value of x)
int x = 1, y = 2, z = 3;
auto lambda = [x, &y, z]() {
// x and z by value, y by reference
};
int a = 1, b = 2;
auto lambda1 = [=]() { return a + b; }; // Capture all by value
auto lambda2 = [&]() { return a + b; }; // Capture all by reference
auto lambda3 = [=, &b]() { return a + b; }; // All by value except b
auto lambda4 = [&, a]() { return a + b; }; // All by reference except a
Warning: Capturing [=] doesn’t capture this in member functions (C++17 changed this). Prefer explicit captures.
By default, value-captured variables are const. Use mutable to modify them:
int x = 0;
auto counter = [x]() mutable {
return ++x; // Modifies lambda's copy of x
};
std::cout << counter(); // 1
std::cout << counter(); // 2
std::cout << x; // 0 (original x unchanged)
Parameters can use auto for generic types:
auto print = [](const auto& x) {
std::cout << x << '\\n';
};
print(42); // Works with int
print(3.14); // Works with double
print("hello"); // Works with const char*
Equivalent to a template function:
struct Lambda {
template<typename T>
void operator()(const T& x) const {
std::cout << x << '\\n';
}
};
Capture with initialization, enabling move-only types:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
};
// ptr is now nullptr, lambda owns the unique_ptr
Rename and transform during capture:
int x = 5;
auto lambda = [y = x * 2]() {
return y; // y is 10
};
Usually automatic, but can be explicit:
auto lambda1 = [](int x) { return x * 2; }; // Deduced: int
auto lambda2 = [](int x) -> double { // Explicit: double
return x * 2.5;
};
Required when return type is ambiguous:
auto lambda = [](bool flag) {
if (flag)
return 1; // int
else
return 2.5; // double - ERROR: inconsistent types
};
// Fix with explicit return type:
auto lambda = [](bool flag) -> double {
if (flag) return 1;
else return 2.5;
};
Each lambda has a unique compiler-generated type. Use auto to store:
auto lambda = [](int x) { return x * 2; };
std::function provides type erasure but has overhead:
#include <functional>
std::function<int(int)> func = [](int x) { return x * 2; };
// More flexible but slower than auto
Prefer auto unless you need type erasure or reassignment.
Useful for complex initialization:
const auto value = [&]() {
if (condition1) return compute1();
else if (condition2) return compute2();
else return default_value();
}(); // Immediately invoked
Classic use case: predicates and projections
std::vector<int> v = {1, 2, 3, 4, 5};
// Remove odd numbers
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 != 0; }),
v.end());
// Count elements > 3
auto count = std::count_if(v.begin(), v.end(),
[](int x) { return x > 3; });
Waiting for signal...
The STL provides several sequence containers, each with different performance characteristics. Choose based on access patterns and operation frequencies.
Contiguous memory, random access, amortized O(1) push_back:
std::vector<int> v;
v.push_back(10);
v.push_back(20);
v.emplace_back(30); // Construct in-place
v[0] = 15; // O(1) random access
v.resize(100);
v.reserve(1000); // Pre-allocate capacity
for (const auto& elem : v) {
std::cout << elem << ' ';
}
std::vector<int> v;
v.reserve(100); // capacity = 100, size = 0
v.resize(50); // capacity = 100, size = 50 (elements default-constructed)
Like vector but efficient insertion/deletion at both ends:
std::deque<int> d;
d.push_front(10); // O(1)
d.push_back(20); // O(1)
d[1] = 25; // O(1) random access
d.pop_front(); // O(1)
d.pop_back(); // O(1)
Not contiguous memory—typically a sequence of fixed-size arrays (chunks). Random access is O(1) but slightly slower than vector.
Bidirectional list, O(1) insertion/deletion anywhere (if you have iterator):
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = std::find(lst.begin(), lst.end(), 3);
lst.insert(it, 10); // Insert before 3: {1, 2, 10, 3, 4, 5}
lst.erase(it); // Remove 3: {1, 2, 10, 4, 5}
lst.push_front(0);
lst.push_back(6);
Move elements between lists in O(1):
std::list<int> l1 = {1, 2, 3};
std::list<int> l2 = {4, 5, 6};
l1.splice(l1.end(), l2); // Move all of l2 to end of l1
// l1: {1, 2, 3, 4, 5, 6}, l2: {}
Like list but single direction, smaller memory footprint:
std::forward_list<int> fwd = {1, 2, 3};
fwd.push_front(0); // O(1)
// No push_back (would be O(n))
auto it = fwd.before_begin();
fwd.insert_after(it, 10); // Insert after position
Compile-time fixed size, no dynamic allocation:
std::array<int, 5> arr = {1, 2, 3, 4, 5};
arr[0] = 10; // O(1) access
arr.at(2) = 30; // Bounds-checked access
// Size is part of type
// std::array<int, 5> != std::array<int, 6>
int c_array[5]; // No bounds checking, decays to pointer
std::array<int, 5> arr; // Knows size, range-based for, STL algorithms
| Operation | vector | deque | list | forward_list |
|---|---|---|---|---|
| Random Access | O(1) | O(1) | O(n) | O(n) |
| Insert/Delete Front | O(n) | O(1) | O(1) | O(1) |
| Insert/Delete Back | O(1)† | O(1) | O(1) | O(n) |
| Insert/Delete Middle | O(n) | O(n) | O(1)‡ | O(1)‡ |
| Memory | Contiguous | Chunked | Node | Node |
† Amortized
‡ If you already have an iterator
Waiting for signal...
Associative containers organize elements by key for fast lookup. Two families: ordered (red-black trees) and unordered (hash tables).
Stores unique elements in sorted order:
std::set<int> s = {5, 2, 8, 2, 1}; // {1, 2, 5, 8} - duplicates removed
s.insert(3); // O(log n)
s.erase(2); // O(log n)
if (s.count(5)) { // O(log n), returns 0 or 1
std::cout << "Found 5\\n";
}
auto it = s.find(8); // O(log n), returns iterator
if (it != s.end()) {
std::cout << *it << '\\n';
}
Elements are always sorted:
std::set<int> s = {5, 1, 3, 2, 4};
for (auto x : s) {
std::cout << x << ' '; // Outputs: 1 2 3 4 5
}
struct Person {
std::string name;
int age;
};
struct CompareByAge {
bool operator()(const Person& a, const Person& b) const {
return a.age < b.age;
}
};
std::set<Person, CompareByAge> people;
Associates keys with values, sorted by key:
std::map<std::string, int> ages;
ages["Alice"] = 30;
ages["Bob"] = 25;
ages.insert({"Charlie", 35});
ages.emplace("David", 28);
std::cout << ages["Alice"]; // 30
// operator[] creates element if missing!
std::cout << ages["Unknown"]; // Creates entry with value 0
// Use find to avoid creation:
if (auto it = ages.find("Eve"); it != ages.end()) {
std::cout << it->second;
} else {
std::cout << "Not found\\n";
}
for (const auto& [name, age] : ages) {
std::cout << name << ": " << age << '\\n';
}
Allow duplicate keys:
std::multiset<int> ms = {1, 2, 2, 3, 3, 3};
std::cout << ms.count(3); // 3
std::multimap<std::string, int> mm;
mm.insert({"Alice", 90});
mm.insert({"Alice", 85}); // Same key, different value
auto range = mm.equal_range("Alice");
for (auto it = range.first; it != range.second; ++it) {
std::cout << it->second << ' '; // 90 85
}
Hash table based: O(1) average, no ordering:
std::unordered_set<int> us = {5, 2, 8, 1};
us.insert(3); // O(1) average
// Iteration order is arbitrary
for (auto x : us) {
std::cout << x << ' '; // Unpredictable order
}
std::unordered_map<std::string, int> um;
um["key"] = 42; // O(1) average
struct Person {
std::string name;
int age;
};
struct PersonHash {
size_t operator()(const Person& p) const {
return std::hash<std::string>{}(p.name) ^ std::hash<int>{}(p.age);
}
};
struct PersonEqual {
bool operator()(const Person& a, const Person& b) const {
return a.name == b.name && a.age == b.age;
}
};
std::unordered_set<Person, PersonHash, PersonEqual> people;
| Aspect | Ordered (set/map) | Unordered (unordered_set/map) |
|---|---|---|
| Lookup | O(log n) | O(1) average, O(n) worst |
| Insertion | O(log n) | O(1) average |
| Iteration | Sorted order | Arbitrary order |
| Memory | Less overhead | More overhead (buckets) |
| Use When | Need sorted order, range queries | Only need fast lookup |
Default choice: unordered_map/unordered_set unless you need ordering.
Ordered containers support efficient range queries:
std::set<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto lower = s.lower_bound(3); // First element >= 3
auto upper = s.upper_bound(7); // First element > 7
for (auto it = lower; it != upper; ++it) {
std::cout << *it << ' '; // 3 4 5 6 7
}
Avoid creating temporary objects for lookup:
std::set<std::string, std::less<>> s; // Note: std::less<> not std::less<std::string>
s.find("hello"); // Doesn't create std::string temporary
Waiting for signal...
The <algorithm> header provides ~100 algorithms operating on iterator ranges, enabling generic operations on any container.
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = std::find(v.begin(), v.end(), 3);
if (it != v.end()) {
std::cout << "Found: " << *it << '\\n';
}
auto it2 = std::find_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// Finds first even number
auto n = std::count(v.begin(), v.end(), 3);
auto even_count = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
bool all_positive = std::all_of(v.begin(), v.end(), [](int x) { return x > 0; });
bool has_even = std::any_of(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
bool no_negatives = std::none_of(v.begin(), v.end(), [](int x) { return x < 0; });
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(src.size());
std::copy(src.begin(), src.end(), dst.begin());
std::vector<int> evens;
std::copy_if(src.begin(), src.end(), std::back_inserter(evens),
[](int x) { return x % 2 == 0; });
Apply function to each element:
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
[](int x) { return x * x; });
// squared = {1, 4, 9, 16, 25}
// Binary transform
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {4, 5, 6};
std::vector<int> sum(3);
std::transform(a.begin(), a.end(), b.begin(), sum.begin(),
[](int x, int y) { return x + y; });
// sum = {5, 7, 9}
std::vector<int> v(10);
std::fill(v.begin(), v.end(), 42); // All elements = 42
int n = 0;
std::generate(v.begin(), v.end(), [&n]() { return n++; });
// v = {0, 1, 2, ..., 9}
Careful: Doesn’t actually remove, just moves elements to end:
std::vector<int> v = {1, 2, 3, 4, 5};
auto new_end = std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
v.erase(new_end, v.end()); // Actually erase
// v = {1, 3, 5}
// Idiomatic erase-remove:
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x < 3; }),
v.end());
C++20 added std::erase_if for convenience:
std::erase_if(v, [](int x) { return x < 3; });
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end()); // O(n log n), unstable
// v = {1, 2, 5, 8, 9}
std::sort(v.begin(), v.end(), std::greater<>()); // Descending
// v = {9, 8, 5, 2, 1}
std::stable_sort(v.begin(), v.end()); // Preserves relative order of equal elements
Partially sorts so that nth element is in correct position:
std::vector<int> v = {5, 2, 8, 1, 9, 3, 7};
std::nth_element(v.begin(), v.begin() + 3, v.end());
// v[3] is the 4th smallest element
// Elements before v[3] are <= v[3]
// Elements after v[3] are >= v[3]
// Relative order not specified
std::vector<int> v = {1, 2, 3, 4, 5, 6};
auto pivot = std::partition(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
// Even numbers before pivot, odd after
std::vector<int> v = {1, 2, 2, 2, 3, 4, 5};
auto lower = std::lower_bound(v.begin(), v.end(), 2); // First >= 2
auto upper = std::upper_bound(v.begin(), v.end(), 2); // First > 2
std::cout << std::distance(lower, upper); // Count of 2's: 3
auto [first, last] = std::equal_range(v.begin(), v.end(), 2); // C++17
bool found = std::binary_search(v.begin(), v.end(), 3); // O(log n)
std::vector<int> a = {1, 2, 3, 4, 5};
std::vector<int> b = {3, 4, 5, 6, 7};
std::vector<int> result;
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
std::back_inserter(result));
// result = {3, 4, 5}
result.clear();
std::set_union(a.begin(), a.end(), b.begin(), b.end(),
std::back_inserter(result));
// result = {1, 2, 3, 4, 5, 6, 7}
result.clear();
std::set_difference(a.begin(), a.end(), b.begin(), b.end(),
std::back_inserter(result));
// result = {1, 2}
<numeric>)std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0); // 15
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>()); // 120
// C++17 reduce (parallel)
int sum2 = std::reduce(std::execution::par, v.begin(), v.end());
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> result(v.size());
std::partial_sum(v.begin(), v.end(), result.begin());
// result = {1, 3, 6, 10, 15} (cumulative sum)
std::adjacent_difference(v.begin(), v.end(), result.begin());
// result = {1, 1, 1, 1, 1} (differences)
Waiting for signal...
Iterators abstract the concept of position within a sequence, enabling algorithms to work with any container. They’re the foundation of generic programming in C++.
Iterators form a hierarchy based on operations they support:
Read-only, single-pass:
*it (read)++it, it++==, !=Example: std::istream_iterator
Write-only, single-pass:
*it = value (write)++it, it++Example: std::ostream_iterator, std::back_inserter
Multi-pass read/write:
*it = valueExample: std::forward_list::iterator
Forward + backward:
--it, it--Example: std::list::iterator, std::set::iterator
Bidirectional + arithmetic:
it + n, it - nit[n]<, >, <=, >=Example: std::vector::iterator, std::deque::iterator
Random access + contiguous memory:
Example: std::vector::iterator, std::array::iterator
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin(); // Iterator to first element
auto end = v.end(); // Past-the-end iterator
while (it != end) {
std::cout << *it << ' '; // Dereference
++it; // Advance
}
// Range-based for is syntactic sugar for iterators
for (auto x : v) {
std::cout << x << ' ';
}
Convert containers into output iterators:
std::vector<int> v;
std::fill_n(std::back_inserter(v), 5, 42); // Pushes 5 copies of 42
std::list<int> lst;
std::fill_n(std::front_inserter(lst), 3, 10); // Pushes to front
std::set<int> s;
std::copy(v.begin(), v.end(), std::inserter(s, s.begin()));
Iterate backwards:
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.rbegin(); it != v.rend(); ++it) {
std::cout << *it << ' '; // 5 4 3 2 1
}
Causes dereference to return rvalue reference:
std::vector<std::unique_ptr<int>> v1;
v1.push_back(std::make_unique<int>(1));
v1.push_back(std::make_unique<int>(2));
std::vector<std::unique_ptr<int>> v2(
std::make_move_iterator(v1.begin()),
std::make_move_iterator(v1.end())
);
// v1's unique_ptrs moved to v2
Read from input stream:
std::istringstream iss("1 2 3 4 5");
std::vector<int> v(std::istream_iterator<int>(iss),
std::istream_iterator<int>());
// v = {1, 2, 3, 4, 5}
Write to output stream:
std::vector<int> v = {1, 2, 3, 4, 5};
std::copy(v.begin(), v.end(),
std::ostream_iterator<int>(std::cout, " "));
// Outputs: 1 2 3 4 5
Move iterator by n positions:
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
std::advance(it, 3); // Now points to 4
Count elements between iterators:
auto dist = std::distance(lst.begin(), lst.end()); // 5
Return advanced iterator without modifying original:
auto it = v.begin();
auto next = std::next(it, 2); // Points 2 positions ahead
auto prev = std::prev(it, 1); // Points 1 position back
Creating custom iterators requires defining specific operations based on category:
class Range {
int current_;
int end_;
public:
class Iterator {
int value_;
public:
using iterator_category = std::forward_iterator_tag;
using value_type = int;
using difference_type = int;
using pointer = int*;
using reference = int&;
Iterator(int v) : value_(v) {}
int operator*() const { return value_; }
Iterator& operator++() { ++value_; return *this; }
Iterator operator++(int) { auto temp = *this; ++value_; return temp; }
bool operator==(const Iterator& other) const { return value_ == other.value_; }
bool operator!=(const Iterator& other) const { return !(*this == other); }
};
Range(int start, int end) : current_(start), end_(end) {}
Iterator begin() const { return Iterator(current_); }
Iterator end() const { return Iterator(end_); }
};
// Usage:
for (auto x : Range(1, 6)) {
std::cout << x << ' '; // 1 2 3 4 5
}
Different operations invalidate iterators differently:
| Container | Operation | Invalidation |
|---|---|---|
| vector | push_back | All if reallocation |
| vector | insert/erase | From point onward |
| deque | push_back/front | All (except end) |
| list | insert/erase | Only erased iterators |
| map/set | insert/erase | Only erased iterators |
Waiting for signal...
Exceptions provide a structured way to handle errors, separating error handling from normal flow. When an exception is thrown, stack unwinding occurs: all local objects are destroyed until a matching catch handler is found.
#include <stdexcept>
double divide(double a, double b) {
if (b == 0.0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}
try {
double result = divide(10, 0);
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << '\\n';
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << '\\n';
} catch (...) {
std::cerr << "Unknown exception\\n";
}
All standard exceptions derive from std::exception:
std::exception
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::domain_error
│ ├── std::length_error
│ └── std::out_of_range
├── std::runtime_error
│ ├── std::range_error
│ ├── std::overflow_error
│ └── std::underflow_error
└── std::bad_alloc
throw std::invalid_argument("Invalid input");
throw std::out_of_range("Index out of bounds");
throw std::bad_alloc(); // Memory allocation failed
class FileError : public std::runtime_error {
std::string filename_;
public:
FileError(const std::string& file, const std::string& msg)
: std::runtime_error(msg), filename_(file) {}
const std::string& filename() const { return filename_; }
};
void openFile(const std::string& name) {
// ...
throw FileError(name, "Failed to open file");
}
try {
openFile("data.txt");
} catch (const FileError& e) {
std::cerr << "File error in " << e.filename()
<< ": " << e.what() << '\\n';
}
Code can provide different levels of exception safety:
Operation never throws (or only throws if program terminates):
void swap(T& a, T& b) noexcept {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
If an exception is thrown, program state is unchanged:
void push_back(const T& value) {
if (size_ == capacity_) {
// Allocate new buffer
T* new_data = new T[capacity_ * 2];
// Copy old data (might throw)
std::uninitialized_copy(data_, data_ + size_, new_data);
// Only now commit changes (no-throw operations)
delete[] data_;
data_ = new_data;
capacity_ *= 2;
}
data_[size_++] = value;
}
Invariants are preserved, no resources leak, but state may change:
void process() {
auto ptr = new Resource; // Potential leak if next line throws
risky_operation();
delete ptr;
}
// Better with RAII:
void process() {
auto ptr = std::make_unique<Resource>(); // Automatic cleanup
risky_operation();
}
Undefined behavior or resource leaks if exception thrown.
RAII (Resource Acquisition Is Initialization) ensures cleanup even during exceptions:
class FileHandle {
FILE* file_;
public:
FileHandle(const char* name) : file_(fopen(name, "r")) {
if (!file_) throw std::runtime_error("Cannot open file");
}
~FileHandle() {
if (file_) fclose(file_); // Always called during stack unwinding
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void process() {
FileHandle f("data.txt");
// Use file...
throw std::runtime_error("Error"); // File automatically closed
}
Declares that a function won’t throw exceptions:
void guaranteed() noexcept {
// If an exception escapes, std::terminate is called
}
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
// noexcept if T's move constructor is noexcept
}
Importance: noexcept enables optimizations. Standard containers use move operations only if noexcept.
class Widget {
public:
Widget(Widget&&) noexcept; // vector will use move
// Widget(Widget&&); // vector will use copy instead (for safety)
};
template<typename T>
class Stack {
public:
void push(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) {
// noexcept if T is nothrow copyable
}
};
try {
risky_operation();
} catch (const std::exception& e) {
log_error(e.what());
throw; // Rethrow same exception (preserves type)
}
// DON'T do this:
catch (const std::exception& e) {
throw e; // Slices exception to std::exception!
}
Handle exceptions in constructor initialization:
class Widget {
Resource r_;
public:
Widget(const std::string& name)
try : r_(name) { // Function try block
// Constructor body
}
catch (const std::exception& e) {
// Handle exception from r_ construction
// Note: Exception is always rethrown after this catch block
}
};
C++98 dynamic exception specifications are deprecated:
void func() throw(std::runtime_error); // Deprecated, don't use
void func() noexcept; // Modern C++
Waiting for signal...
Namespaces prevent name collisions in large codebases by grouping related declarations under a common name.
namespace graphics {
class Shape {};
void draw(const Shape& s) {}
}
namespace physics {
class Shape {}; // Different from graphics::Shape
void simulate(const Shape& s) {}
}
graphics::Shape s1;
physics::Shape s2;
namespace company {
namespace graphics {
namespace renderer {
class Pipeline {};
}
}
}
// C++17 shorthand:
namespace company::graphics::renderer {
class Pipeline {};
}
Introduces specific name:
using std::cout;
using std::endl;
cout << "Hello" << endl; // No std:: prefix needed
Imports entire namespace (use sparingly):
using namespace std; // BAD: Pollutes global namespace
// OK in implementation files with limited scope:
void func() {
using namespace std::chrono; // Limited to function scope
auto now = system_clock::now();
}
Best Practice: Avoid using namespace in headers—it forces pollution on all includers.
Also called “Koenig Lookup”: when calling a function, the compiler searches namespaces of argument types:
namespace lib {
struct Widget {};
void process(const Widget& w) {}
}
lib::Widget w;
process(w); // Finds lib::process via ADL (no lib:: needed!)
std::vector<int> v;
std::sort(v.begin(), v.end()); // ADL finds std::sort
// Also works:
sort(v.begin(), v.end()); // ADL looks in std:: because iterators are in std::
namespace lib {
template<typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
}
int main() {
using std::swap; // Bring std::swap into scope
int a = 1, b = 2;
swap(a, b); // Calls std::swap (ADL + using declaration)
lib::CustomType x, y;
swap(x, y); // Calls lib::swap via ADL
}
Provide internal linkage (like static in C):
namespace { // Anonymous namespace
int internal_helper() {
return 42;
}
}
// internal_helper is only visible in this translation unit
Use: Replace file-scope static:
// Old style:
static void helper() {}
// Modern C++:
namespace {
void helper() {}
}
Members of inline namespace are also members of enclosing namespace:
namespace lib {
inline namespace v2 {
void func() {}
}
namespace v1 {
void func() {}
}
}
lib::func(); // Calls lib::v2::func (inline namespace)
lib::v1::func(); // Explicitly call v1 version
Use Case: Versioning libraries while maintaining backward compatibility:
namespace mylib {
inline namespace v3 {
class Widget {}; // Current version
}
namespace v2 {
class Widget {}; // Old version still available
}
}
mylib::Widget w; // Uses v3
mylib::v2::Widget w2; // Uses v2
Shorten long namespace names:
namespace fs = std::filesystem;
namespace chrono = std::chrono;
fs::path p = "/home/user";
auto now = chrono::system_clock::now();
class Derived : public Base {
public:
using Base::Base; // Inherit constructors
using Base::method; // Bring method into Derived scope
};
Prefix :: refers to global namespace:
int value = 10; // Global
namespace lib {
int value = 20;
void func() {
std::cout << value; // 20 (lib::value)
std::cout << ::value; // 10 (global value)
}
}
using namespace in headersusing declarations over directivesstaticWaiting for signal...
The <type_traits> header provides utilities for querying and transforming types at compile time, enabling sophisticated template metaprogramming.
Query type properties at compile time:
#include <type_traits>
static_assert(std::is_integral_v<int>);
static_assert(!std::is_integral_v<double>);
static_assert(std::is_floating_point_v<float>);
static_assert(std::is_pointer_v<int*>);
static_assert(std::is_array_v<int[10]>);
static_assert(std::is_class_v<std::string>);
| Trait | Checks |
|---|---|
is_void | Type is void |
is_integral | Integer types |
is_floating_point | float, double, long double |
is_arithmetic | Integral or floating-point |
is_pointer | Pointer type |
is_reference | Lvalue or rvalue reference |
is_const | Const-qualified |
is_class | Class or struct |
is_enum | Enumeration type |
static_assert(std::is_same_v<int, int>);
static_assert(!std::is_same_v<int, long>);
static_assert(std::is_base_of_v<Base, Derived>);
static_assert(std::is_convertible_v<Derived*, Base*>);
Modify types at compile time:
// Remove const/volatile
using T1 = std::remove_const_t<const int>; // int
using T2 = std::remove_cv_t<const volatile int>; // int
// Add const/volatile
using T3 = std::add_const_t<int>; // const int
// Remove reference
using T4 = std::remove_reference_t<int&>; // int
using T5 = std::remove_reference_t<int&&>; // int
// Remove pointer
using T6 = std::remove_pointer_t<int*>; // int
// Decay (array/function to pointer, remove cv/reference)
using T7 = std::decay_t<int[10]>; // int*
using T8 = std::decay_t<const int&>; // int
Choose type based on condition:
template<typename T>
using StorageType = std::conditional_t<
sizeof(T) <= 8,
T,
T*
>;
StorageType<int> x; // int (small)
StorageType<BigClass> y; // BigClass* (large)
Enable function/class only if condition is true:
// Only enable for integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
square(T value) {
return value * value;
}
square(5); // OK
// square(3.14); // Error: SFINAE'd out
static_assert(std::is_default_constructible_v<std::string>);
static_assert(std::is_copy_constructible_v<int>);
static_assert(std::is_move_constructible_v<std::unique_ptr<int>>);
static_assert(!std::is_copy_assignable_v<std::unique_ptr<int>>);
// Nothrow versions
static_assert(std::is_nothrow_move_constructible_v<std::string>);
template<typename T>
void my_swap(T& a, T& b) noexcept(std::is_nothrow_move_constructible_v<T> &&
std::is_nothrow_move_assignable_v<T>) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
Compile-time conditional branches:
template<typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // Dereference pointer
} else {
return t; // Return value directly
}
}
int x = 42;
auto v1 = get_value(&x); // Dereferences, returns 42
auto v2 = get_value(x); // Returns 42 directly
Code in discarded branches doesn’t need to be valid:
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << value * 2; // Only instantiated for integral types
} else {
std::cout << value.toString(); // Only instantiated for types with toString()
}
}
Alternative to SFINAE using type tags:
template<typename Iterator>
void advance_impl(Iterator& it, int n, std::random_access_iterator_tag) {
it += n; // O(1) for random access
}
template<typename Iterator>
void advance_impl(Iterator& it, int n, std::input_iterator_tag) {
while (n--) ++it; // O(n) for input iterators
}
template<typename Iterator>
void advance(Iterator& it, int n) {
advance_impl(it, n, typename std::iterator_traits<Iterator>::iterator_category{});
}
template<typename T>
class Buffer {
static_assert(std::is_trivially_copyable_v<T>,
"T must be trivially copyable");
static_assert(sizeof(T) <= 64,
"T is too large for inline storage");
T data_[100];
};
template<typename, typename = void>
struct has_value_type : std::false_type {};
template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};
static_assert(has_value_type<std::vector<int>>::value);
static_assert(!has_value_type<int>::value);
Waiting for signal...
C++11 introduced a standard threading library (<thread>), enabling portable concurrent programming without platform-specific APIs.
#include <thread>
#include <iostream>
void task(int id) {
std::cout << "Thread " << id << " executing\\n";
}
int main() {
std::thread t1(task, 1);
std::thread t2(task, 2);
t1.join(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
return 0;
}
std::thread t(task);
// Option 1: Join (wait for completion)
t.join(); // Blocks until thread completes
// Option 2: Detach (independent execution)
t.detach(); // Thread runs independently
// Cannot join() after detach()
Critical: Every thread must be either joined or detached before destruction, otherwise std::terminate is called.
{
std::thread t(task);
} // CRASH: thread destructor called without join/detach
class thread_guard {
std::thread& t_;
public:
explicit thread_guard(std::thread& t) : t_(t) {}
~thread_guard() {
if (t_.joinable()) t_.join();
}
thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
};
void func() {
std::thread t(task);
thread_guard g(t);
// Even if exception thrown, thread is joined
}
void task(int x, const std::string& s) {
std::cout << x << ": " << s << '\\n';
}
std::thread t(task, 42, "hello"); // Args passed by value
void modify(int& x) {
x = 100;
}
int value = 0;
std::thread t(modify, std::ref(value)); // Must use std::ref
t.join();
std::cout << value; // 100
Concurrent modification of shared data without synchronization causes data races (undefined behavior):
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // DATA RACE!
}
}
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter; // Unpredictable result (< 200000)
std::mutex provides mutual exclusion:
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock();
++counter;
mtx.unlock();
}
}
Problem: If exception thrown between lock/unlock, mutex stays locked (deadlock).
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // Locks mutex
++counter;
} // Automatically unlocks when lock goes out of scope
}
More flexible than lock_guard:
std::mutex mtx;
void task() {
std::unique_lock<std::mutex> lock(mtx);
// Do work...
lock.unlock(); // Manually unlock
// Do work without lock...
lock.lock(); // Relock
// More work...
} // Automatically unlocks if locked
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // Don't lock yet
// ...
lock.lock(); // Lock when ready
std::mutex m1, m2;
void thread1() {
std::lock_guard<std::mutex> lock1(m1);
std::lock_guard<std::mutex> lock2(m2); // Potential deadlock
}
void thread2() {
std::lock_guard<std::mutex> lock2(m2);
std::lock_guard<std::mutex> lock1(m1); // Opposite order
}
void thread1() {
std::unique_lock<std::mutex> lock1(m1, std::defer_lock);
std::unique_lock<std::mutex> lock2(m2, std::defer_lock);
std::lock(lock1, lock2); // Atomically locks both, deadlock-free
}
C++17 std::scoped_lock simplifies this:
void thread1() {
std::scoped_lock lock(m1, m2); // Locks both, unlocks in destructor
}
std::thread::id main_thread_id = std::this_thread::get_id();
void task() {
auto id = std::this_thread::get_id();
std::cout << "Thread ID: " << id << '\\n';
}
unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware supports " << n << " concurrent threads\\n";
Waiting for signal...
The <atomic> header provides atomic operations—indivisible operations that appear to occur instantaneously to other threads, without requiring mutexes.
#include <atomic>
std::atomic<int> counter{0};
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // Atomic increment (no mutex needed)
}
}
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter; // Guaranteed to be 200000
std::atomic<int> x{10};
x.store(20); // Atomic write
int val = x.load(); // Atomic read
int old = x.exchange(30); // Atomic swap
// Compare-and-swap
int expected = 30;
bool success = x.compare_exchange_strong(expected, 40);
// If x == expected, sets x = 40 and returns true
// Otherwise, sets expected = x and returns false
std::atomic<int> counter{0};
if (counter.is_lock_free()) {
std::cout << "True atomic (no locks)\\n";
} else {
std::cout << "Uses mutex internally\\n";
}
// Compile-time check (C++17)
static_assert(std::atomic<int>::is_always_lock_free);
Most standard types are lock-free on modern architectures:
atomic<int>, atomic<long>: Usually lock-freeatomic<std::string>: Not lock-free (too large)atomic<bool>, atomic<char>: Always lock-freeThe C++ memory model defines how operations on different threads are ordered. Six memory ordering modes:
No synchronization, only atomicity:
std::atomic<int> x{0}, y{0};
// Thread 1
x.store(1, std::memory_order_relaxed);
y.store(2, std::memory_order_relaxed);
// Thread 2
int a = y.load(std::memory_order_relaxed);
int b = x.load(std::memory_order_relaxed);
// a=2, b=0 is possible! (reordering allowed)
Use: Independent counters, statistics
Establishes happens-before relationship:
std::atomic<bool> ready{false};
int data = 0;
// Producer thread
data = 42; // Non-atomic write
ready.store(true, std::memory_order_release); // Release
// Consumer thread
while (!ready.load(std::memory_order_acquire)) {} // Acquire
std::cout << data; // Guaranteed to see 42
Release-acquire pair synchronizes: all writes before release are visible after acquire.
Both acquire and release:
std::atomic<int> x{0};
int prev = x.fetch_add(1, std::memory_order_acq_rel);
// Acquires previous value, releases new value
Sequentially consistent: total global order:
std::atomic<int> x{0}, y{0};
// Thread 1
x.store(1); // Default: memory_order_seq_cst
y.store(2);
// Thread 2
int a = y.load();
int b = x.load();
// If a == 2, then b must == 1 (total order guaranteed)
Most expensive but easiest to reason about. Default for most operations.
Like acquire but weaker (rarely used, complex semantics).
Fundamental building block for lock-free algorithms:
std::atomic<int> value{0};
void increment() {
int expected = value.load();
while (!value.compare_exchange_weak(expected, expected + 1)) {
// expected was updated to current value, retry
}
}
// Strong: Only fails if value != expected
compare_exchange_strong(expected, desired);
// Weak: May spuriously fail even if value == expected
// (Faster on some architectures)
compare_exchange_weak(expected, desired);
Use _weak in loops (spurious failures don’t matter).
Simplest atomic type, guaranteed lock-free:
std::atomic_flag flag = ATOMIC_FLAG_INIT;
bool was_set = flag.test_and_set(); // Atomically set to true, return old value
flag.clear(); // Set to false
class spinlock {
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {
// Spin until lock acquired
}
}
void unlock() {
flag_.clear(std::memory_order_release);
}
};
Warning: Spinlocks waste CPU cycles. Use for very short critical sections only.
template<typename T>
class lock_free_stack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head_{nullptr};
public:
void push(const T& value) {
Node* new_node = new Node{value, head_.load()};
while (!head_.compare_exchange_weak(new_node->next, new_node)) {
// Retry with updated head
}
}
bool pop(T& result) {
Node* old_head = head_.load();
while (old_head && !head_.compare_exchange_weak(old_head, old_head->next)) {
// Retry
}
if (old_head) {
result = old_head->data;
delete old_head;
return true;
}
return false;
}
};
Caution: Lock-free programming is extremely difficult. Prefer mutexes unless profiling shows they’re a bottleneck.
| Use Atomics When | Use Mutexes When |
|---|---|
| Single variable | Multiple variables |
| Simple operations | Complex critical sections |
| Performance critical | Simplicity matters |
| Lock-free required | Standard case |
Waiting for signal...
Beyond mutexes and atomics, C++ provides higher-level synchronization primitives: condition variables for signaling between threads, and futures/promises for asynchronous result retrieval.
Condition variables enable threads to wait for specific conditions without busy-waiting:
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
queue.push(i);
}
cv.notify_one(); // Wake one waiting thread
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !queue.empty(); }); // Wait until queue not empty
int value = queue.front();
queue.pop();
lock.unlock();
std::cout << "Consumed: " << value << '\\n';
}
}
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // Atomically unlocks mtx and sleeps
// When woken, reacquires mtx
// Equivalent to:
while (!predicate()) {
cv.wait(lock);
}
Condition variables can wake spuriously (without notify). Always use predicate form:
// Bad: No protection against spurious wakeups
cv.wait(lock);
if (!queue.empty()) { /* ... */ }
// Good: Handles spurious wakeups
cv.wait(lock, []{ return !queue.empty(); });
cv.notify_one(); // Wakes one waiting thread
cv.notify_all(); // Wakes all waiting threads
Use notify_all when multiple threads might need to act on the condition.
using namespace std::chrono_literals;
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, 1s, []{ return ready; })) {
// Condition met within 1 second
} else {
// Timeout
}
auto deadline = std::chrono::system_clock::now() + 5s;
if (cv.wait_until(lock, deadline, []{ return ready; })) {
// Condition met before deadline
}
Futures provide a mechanism to retrieve results from asynchronous operations:
#include <future>
void compute(std::promise<int> prom) {
// Do expensive computation
int result = 42;
prom.set_value(result); // Fulfill promise
}
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(compute, std::move(prom));
int result = fut.get(); // Blocks until promise fulfilled
std::cout << "Result: " << result << '\\n';
t.join();
void task(std::promise<int> prom) {
try {
throw std::runtime_error("Error!");
} catch (...) {
prom.set_exception(std::current_exception());
}
}
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(task, std::move(prom));
try {
int result = fut.get(); // Rethrows exception
} catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << '\\n';
}
t.join();
Higher-level alternative to manual thread + promise:
std::future<int> fut = std::async(std::launch::async, []{
// Runs in separate thread
return 42;
});
int result = fut.get(); // Retrieve result
// Force async execution (new thread)
auto f1 = std::async(std::launch::async, task);
// Defer execution until get() called (lazy)
auto f2 = std::async(std::launch::deferred, task);
// Implementation chooses (default)
auto f3 = std::async(task);
Multiple threads can wait on the same result:
std::shared_future<int> sf = std::async(task).share();
auto lambda = [sf]{ return sf.get(); };
std::thread t1(lambda);
std::thread t2(lambda);
// Both can call get()
t1.join();
t2.join();
Wraps callable for future retrieval:
std::packaged_task<int(int, int)> task([](int a, int b) {
return a + b;
});
std::future<int> result = task.get_future();
std::thread t(std::move(task), 3, 4);
std::cout << result.get(); // 7
t.join();
class ThreadSafeQueue {
std::queue<int> queue_;
mutable std::mutex mtx_;
std::condition_variable cv_;
public:
void push(int value) {
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(value);
}
cv_.notify_one();
}
int pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this]{ return !queue_.empty(); });
int value = queue_.front();
queue_.pop();
return value;
}
bool empty() const {
std::lock_guard<std::mutex> lock(mtx_);
return queue_.empty();
}
};
Waiting for signal...
C++17’s std::optional<T> represents a value that may or may not be present, avoiding special sentinel values or pointers.
#include <optional>
std::optional<int> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt; // No value
}
}
auto result = parse_int("42");
if (result) { // or result.has_value()
std::cout << *result << '\\n'; // Dereference to get value
} else {
std::cout << "Parse failed\\n";
}
std::optional<int> opt = 42;
// Safe access with default
int value = opt.value_or(0); // Returns 42, or 0 if empty
// Direct access (throws if empty)
int v1 = opt.value(); // Throws std::bad_optional_access if empty
// Unsafe access (UB if empty)
int v2 = *opt; // Like pointer dereference
// Check before access
if (opt.has_value()) {
std::cout << opt.value();
}
std::optional<int> empty; // No value
std::optional<int> zero{0}; // Contains 0
std::optional<int> forty_two = 42;
std::optional<int> none = std::nullopt;
// In-place construction
std::optional<std::string> str{std::in_place, 10, 'x'}; // "xxxxxxxxxx"
std::optional<int> opt;
opt = 42; // Assign value
opt.reset(); // Clear value (now empty)
opt.emplace(100); // Construct value in-place
std::optional<int> opt = 42;
// and_then: Apply function returning optional
auto result = opt.and_then([](int x) -> std::optional<int> {
return x > 0 ? std::optional{x * 2} : std::nullopt;
});
// transform: Apply function returning value
auto doubled = opt.transform([](int x) { return x * 2; });
// or_else: Provide alternative
auto val = std::optional<int>{}.or_else([]{ return std::optional{42}; });
class Config {
std::optional<std::string> username_;
std::optional<int> port_;
public:
void set_username(const std::string& name) { username_ = name; }
std::optional<std::string> get_username() const { return username_; }
int get_port() const { return port_.value_or(8080); } // Default port
};
std::variant is a type-safe union that can hold one of several types at any time.
#include <variant>
std::variant<int, double, std::string> value;
value = 42; // Holds int
value = 3.14; // Now holds double
value = "hello"; // Now holds string
// Access by index
int i = std::get<0>(value); // Throws if not int
// Access by type (must be unique)
std::string s = std::get<std::string>(value);
// Safe access
if (auto ptr = std::get_if<std::string>(&value)) {
std::cout << *ptr;
}
std::variant<int, double, std::string> v = 42;
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << arg << '\\n';
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << arg << '\\n';
} else {
std::cout << "string: " << arg << '\\n';
}
}, v);
Helper for multiple lambdas:
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>; // Deduction guide
std::variant<int, double, std::string> v = "hello";
std::visit(overloaded{
[](int x) { std::cout << "int: " << x; },
[](double x) { std::cout << "double: " << x; },
[](const std::string& x) { std::cout << "string: " << x; }
}, v);
std::variant<int, double> v = 3.14;
std::cout << v.index(); // 1 (double is second alternative)
if (std::holds_alternative<double>(v)) {
std::cout << "Holds double\\n";
}
Alternative to exceptions:
struct Error {
std::string message;
};
std::variant<int, Error> divide(int a, int b) {
if (b == 0) {
return Error{"Division by zero"};
}
return a / b;
}
auto result = divide(10, 2);
std::visit(overloaded{
[](int value) { std::cout << "Result: " << value; },
[](const Error& e) { std::cerr << "Error: " << e.message; }
}, result);
| Feature | optional | variant | union |
|---|---|---|---|
| Type Safety | Yes | Yes | No |
| Knows Type | Yes | Yes | No |
| Empty State | Yes | No* | N/A |
| Non-Trivial Types | Yes | Yes | No |
*variant has std::monostate for empty state
std::variant<std::monostate, int, double> v; // Default: monostate (empty)
Waiting for signal...
C++17/20 introduced view types that refer to existing data without owning it, avoiding unnecessary copies.
Lightweight non-owning reference to a string:
#include <string_view>
void print(std::string_view sv) { // No copy!
std::cout << sv << '\\n';
}
std::string str = "hello";
const char* cstr = "world";
print(str); // OK: string converts to string_view
print(cstr); // OK: const char* converts to string_view
print("literal"); // OK: no temporary std::string created
// Bad: Creates temporary string for each call
void process(const std::string& s);
process("literal"); // Allocates memory!
// Good: No allocation
void process(std::string_view s);
process("literal"); // Just pointer + length
std::string_view sv = "Hello, World!";
sv.size(); // 13
sv.length(); // 13
sv.empty(); // false
sv.data(); // Pointer to first character
sv[0]; // 'H'
sv.front(); // 'H'
sv.back(); // '!'
sv.substr(7, 5); // "World"
sv.remove_prefix(7); // sv = "World!"
sv.remove_suffix(1); // sv = "World"
Critical: string_view doesn’t own data—ensure underlying data outlives the view:
std::string_view get_view() {
std::string temp = "temporary";
return temp; // DANGER: Returns view to destroyed string!
}
auto sv = get_view();
std::cout << sv; // UB: Dangling reference!
Safe usage:
std::string str = "persistent";
std::string_view sv = str; // OK: str outlives sv
void func(std::string_view sv) {
// OK: sv only used during function
}
Non-owning view over contiguous sequence:
#include <span>
void process(std::span<int> data) {
for (int& x : data) {
x *= 2;
}
}
std::vector<int> vec = {1, 2, 3, 4, 5};
int arr[] = {1, 2, 3, 4, 5};
process(vec); // Works with vector
process(arr); // Works with array
// Dynamic extent (size at runtime)
std::span<int> dynamic_span = vec;
// Fixed extent (size at compile time)
std::span<int, 5> fixed_span = arr;
// fixed_span.size() is constexpr
static_assert(fixed_span.size() == 5);
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp = vec;
sp.size(); // 5
sp.size_bytes(); // 5 * sizeof(int)
sp.empty(); // false
sp.data(); // Pointer to first element
sp[0] = 10; // Modify through span
sp.front() = 20;
sp.back() = 30;
// Subspans
auto first3 = sp.first(3); // First 3 elements
auto last2 = sp.last(2); // Last 2 elements
auto middle = sp.subspan(1, 3); // 3 elements starting at index 1
void print(std::span<const int> data) { // const elements
for (int x : data) {
std::cout << x << ' ';
}
}
std::vector<int> vec = {1, 2, 3};
print(vec); // OK: converts to span<const int>
Before:
void process(const std::vector<int>& v);
void process(const std::array<int, 10>& a);
void process(const int* data, size_t size);
After:
void process(std::span<const int> data); // Handles all cases
// Old: Easy to mismatch size
void process(const int* data, size_t size);
// New: Size is part of the span
void process(std::span<const int> data);
| Aspect | string_view | const string& |
|---|---|---|
| Copy Cost | Cheap (2 pointers) | Depends (reference) |
| Temporary Creation | No | Yes (for literals) |
| Null-Termination | Not guaranteed | Guaranteed |
| Substring | O(1) | O(n) (copies) |
| Modify Original | No | No |
Guideline: Use string_view for read-only string parameters, especially when substrings are common.
| Aspect | span | vector& |
|---|---|---|
| Type | View | Container |
| Ownership | No | Yes |
| Works With | Any contiguous | Only vector |
| Resize | No | Yes |
Guideline: Use span for functions operating on contiguous data regardless of container type.
Waiting for signal...
C++17 structured bindings provide a concise syntax for unpacking tuples, pairs, arrays, and structs into individual variables.
std::pair<int, std::string> get_person() {
return {42, "Alice"};
}
auto [id, name] = get_person(); // Structured binding
std::cout << id << ": " << name; // 42: Alice
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << '\\n';
}
std::tuple<int, double, std::string> data{42, 3.14, "hello"};
auto [i, d, s] = data;
int arr[] = {1, 2, 3};
auto [a, b, c] = arr; // a=1, b=2, c=3
// Number of bindings must match array size
// auto [x, y] = arr; // Error: 3 elements, 2 bindings
Works with any struct/class where all members are public:
struct Point {
double x, y;
};
Point p{3.0, 4.0};
auto [x, y] = p; // x=3.0, y=4.0
double distance = std::sqrt(x*x + y*y);
std::map<std::string, int> map = {{"key", 42}};
// Reference binding: can modify original
for (auto& [key, value] : map) {
value *= 2; // Modifies map
}
// Const reference: read-only
for (const auto& [key, value] : map) {
std::cout << key << ": " << value << '\\n';
}
C++20 allows placeholder with [[maybe_unused]]:
auto [a, [[maybe_unused]] b, c] = std::tuple{1, 2, 3};
// b is not used but no warning
C++26 will support _ as placeholder:
auto [a, _, c] = std::tuple{1, 2, 3}; // C++26
Make custom types decomposable:
class Rectangle {
double width_, height_;
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
// Tuple-like interface
template<size_t I>
double get() const {
if constexpr (I == 0) return width_;
else if constexpr (I == 1) return height_;
}
};
// Specializations for std::tuple_size and std::tuple_element
namespace std {
template<>
struct tuple_size<Rectangle> : integral_constant<size_t, 2> {};
template<size_t I>
struct tuple_element<I, Rectangle> { using type = double; };
}
Rectangle r{3.0, 4.0};
auto [w, h] = r; // w=3.0, h=4.0
Declare and initialize variables in the condition statement with tighter scope.
if (auto it = map.find(key); it != map.end()) {
std::cout << it->second;
// it is in scope here
}
// it is NOT in scope here
Before C++17:
auto it = map.find(key); // it pollutes outer scope
if (it != map.end()) {
std::cout << it->second;
}
switch (auto status = get_status(); status) {
case Status::OK:
// use status
break;
case Status::ERROR:
// use status
break;
}
// status not in scope
Combine init statement with structured binding:
if (auto [iter, inserted] = map.insert({key, value}); inserted) {
std::cout << "Inserted: " << iter->first << '\\n';
} else {
std::cout << "Already exists\\n";
}
for (auto v = init_value(); auto& elem : container) {
// v and elem both in scope
process(v, elem);
}
if (std::lock_guard lock(mutex); condition()) {
// Protected section
}
// Lock released
if (auto opt = get_optional(); opt.has_value()) {
std::cout << *opt;
}
if (auto [result, error] = perform_operation(); error) {
handle_error(error);
} else {
use_result(result);
}
Waiting for signal...
C++17 introduced <filesystem> for portable, type-safe file and directory operations, replacing platform-specific APIs.
#include <filesystem>
namespace fs = std::filesystem;
fs::path p = "/home/user/document.txt";
p.filename(); // "document.txt"
p.stem(); // "document"
p.extension(); // ".txt"
p.parent_path(); // "/home/user"
p.root_path(); // "/"
// Path concatenation
fs::path dir = "/home/user";
fs::path file = "document.txt";
fs::path full = dir / file; // "/home/user/document.txt"
fs::path p = "/home/user/docs/file.txt";
for (const auto& part : p) {
std::cout << part << ' ';
}
// Output: "/" "home" "user" "docs" "file.txt"
fs::path p = "some/path";
fs::exists(p); // Does path exist?
fs::is_regular_file(p); // Is it a regular file?
fs::is_directory(p); // Is it a directory?
fs::is_symlink(p); // Is it a symbolic link?
fs::is_empty(p); // Is file empty or directory empty?
// File size
auto size = fs::file_size(p); // In bytes
// Last modification time
auto time = fs::last_write_time(p);
// Iterate over directory contents
for (const auto& entry : fs::directory_iterator("/path/to/dir")) {
std::cout << entry.path() << '\n';
}
// Recursive iteration
for (const auto& entry : fs::recursive_directory_iterator("/path/to/dir")) {
if (fs::is_regular_file(entry)) {
std::cout << entry.path() << ": " << fs::file_size(entry) << " bytes\n";
}
}
// Copy file
fs::copy("source.txt", "dest.txt");
// Copy with options
fs::copy("source.txt", "dest.txt",
fs::copy_options::overwrite_existing);
// Rename/move
fs::rename("old_name.txt", "new_name.txt");
// Remove file
fs::remove("file.txt");
// Remove directory and contents
fs::remove_all("directory");
// Create directory
fs::create_directory("new_dir");
// Create directory and parents
fs::create_directories("path/to/deep/dir");
fs::path p = "file.txt";
// Get permissions
auto perms = fs::status(p).permissions();
// Set permissions
fs::permissions(p, fs::perms::owner_read | fs::perms::owner_write);
// Add permissions
fs::permissions(p, fs::perms::others_read, fs::perm_options::add);
// Remove permissions
fs::permissions(p, fs::perms::others_write, fs::perm_options::remove);
fs::space_info info = fs::space("/");
std::cout << "Capacity: " << info.capacity << " bytes\n";
std::cout << "Free: " << info.free << " bytes\n";
std::cout << "Available: " << info.available << " bytes\n";
// Get current working directory
fs::path cwd = fs::current_path();
// Set current working directory
fs::current_path("/new/path");
fs::path temp = fs::temp_directory_path();
fs::path temp_file = temp / "my_temp_file.txt";
// Exception-based
try {
fs::copy("source.txt", "dest.txt");
} catch (const fs::filesystem_error& e) {
std::cerr << e.what() << '\n';
std::cerr << "Path1: " << e.path1() << '\n';
std::cerr << "Path2: " << e.path2() << '\n';
}
// Error code-based
std::error_code ec;
fs::copy("source.txt", "dest.txt", ec);
if (ec) {
std::cerr << "Error: " << ec.message() << '\n';
}
std::vector<fs::path> find_files(const fs::path& dir, const std::string& ext) {
std::vector<fs::path> result;
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (entry.path().extension() == ext) {
result.push_back(entry.path());
}
}
return result;
}
auto cpp_files = find_files("/project", ".cpp");
uintmax_t directory_size(const fs::path& dir) {
uintmax_t size = 0;
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (fs::is_regular_file(entry)) {
size += fs::file_size(entry);
}
}
return size;
}
void backup_file(const fs::path& source) {
if (!fs::exists(source)) return;
fs::path backup = source;
backup += ".bak";
if (fs::exists(backup)) {
fs::remove(backup);
}
fs::copy(source, backup);
}
Waiting for signal...
C++20 concepts provide a way to specify requirements on template parameters, replacing complex SFINAE with readable constraints.
#include <concepts>
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
template<Numeric T>
T add(T a, T b) {
return a + b;
}
add(5, 10); // OK: int is Numeric
add(3.14, 2.71); // OK: double is Numeric
// add("hello", "world"); // Error: const char* is not Numeric
#include <concepts>
// Fundamental concepts
std::integral<int>; // Integer types
std::floating_point<double>; // Floating-point types
std::signed_integral<int>; // Signed integers
std::unsigned_integral<size_t>; // Unsigned integers
// Comparison concepts
std::equality_comparable<std::string>;
std::totally_ordered<int>; // Has <, >, <=, >=, ==, !=
// Object concepts
std::movable<std::unique_ptr<int>>;
std::copyable<std::string>;
std::semiregular<int>; // Default constructible and copyable
std::regular<int>; // Semiregular and equality comparable
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // Must support +
};
template<typename T>
concept Container = requires(T t) {
typename T::value_type; // Must have value_type
{ t.begin() }; // Must have begin()
{ t.end() }; // Must have end()
{ t.size() } -> std::convertible_to<size_t>; // size() returns size_t-like
};
template<Container C>
void process(const C& container) {
for (const auto& elem : container) {
// ...
}
}
template<typename T>
T max(T a, T b) requires std::totally_ordered<T> {
return (a > b) ? a : b;
}
std::integral auto square(std::integral auto x) {
return x * x;
}
square(5); // OK
// square(3.14); // Error: double is not integral
template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<typename T>
concept Range = requires(T t) {
t.begin();
t.end();
};
template<typename T>
concept Drawable = requires(T t) {
{ t.draw() } -> std::same_as<void>; // Must have void draw()
{ t.position() } -> std::convertible_to<std::pair<int, int>>;
t.visible; // Must have visible member
};
template<typename T>
concept Arithmetic = requires(T a, T b) {
a + b;
a - b;
a * b;
a / b;
};
template<typename T>
concept Complex = requires(T t) {
requires std::regular<T>; // Must be regular
requires sizeof(T) >= 8; // Size requirement
{ t.real() } -> std::floating_point;
{ t.imag() } -> std::floating_point;
};
More constrained concept is preferred:
template<typename T>
concept Integral = std::is_integral_v<T>;
template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;
void process(Integral auto x) {
std::cout << "Integral\\n";
}
void process(SignedIntegral auto x) { // More constrained
std::cout << "Signed integral\\n";
}
process(5); // Calls SignedIntegral version
process(5u); // Calls Integral version
void print(std::integral auto x) {
std::cout << "Integer: " << x << '\\n';
}
void print(std::floating_point auto x) {
std::cout << "Float: " << x << '\\n';
}
print(42); // Calls integral version
print(3.14); // Calls floating_point version
template<typename T>
concept Sortable = requires(T t) {
{ t.begin() } -> std::random_access_iterator;
{ t.end() } -> std::random_access_iterator;
};
std::list<int> lst;
// std::sort(lst.begin(), lst.end());
// Clear error: list iterators are not random_access_iterator
// Before concepts
template<typename T>
void func(T x);
// With concepts (C++20)
void func(auto x); // Unconstrained
void func(std::integral auto x); // Constrained
// Multiple parameters
void func2(std::integral auto x, std::floating_point auto y);
template<typename Range, typename Pred>
requires std::ranges::range<Range> &&
std::predicate<Pred, std::ranges::range_value_t<Range>>
auto filter(Range&& r, Pred p) {
std::vector<std::ranges::range_value_t<Range>> result;
for (auto&& elem : r) {
if (p(elem)) {
result.push_back(elem);
}
}
return result;
}
Waiting for signal...
C++20 ranges revolutionize how we work with sequences, enabling composable, lazy-evaluated operations with cleaner syntax.
#include <ranges>
#include <algorithm>
namespace rng = std::ranges;
std::vector<int> vec = {5, 2, 8, 1, 9, 3};
// Old style
std::sort(vec.begin(), vec.end());
// Range style
rng::sort(vec); // Operates on entire range
// With projection
struct Person { std::string name; int age; };
std::vector<Person> people = /*...*/;
rng::sort(people, {}, &Person::age); // Sort by age
Views are lightweight range adaptors that don’t own data and evaluate lazily:
#include <ranges>
namespace views = std::views;
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// filter: Select elements matching predicate
auto even = vec | views::filter([](int x) { return x % 2 == 0; });
// No computation yet - lazy!
// transform: Apply function to each element
auto squared = vec | views::transform([](int x) { return x * x; });
// take: First n elements
auto first_three = vec | views::take(3);
// drop: Skip first n elements
auto skip_two = vec | views::drop(2);
Views compose using the pipe operator:
auto result = vec
| views::filter([](int x) { return x % 2 == 0; })
| views::transform([](int x) { return x * x; })
| views::take(3);
for (int x : result) {
std::cout << x << ' '; // 4 16 36
}
// Computation happens here during iteration
Generate sequence of numbers:
// Infinite sequence: 1, 2, 3, ...
for (int i : views::iota(1) | views::take(5)) {
std::cout << i << ' '; // 1 2 3 4 5
}
// Bounded sequence
for (int i : views::iota(1, 6)) {
std::cout << i << ' '; // 1 2 3 4 5
}
std::vector<int> vec = {1, 2, 3, 4, 5};
for (int x : vec | views::reverse) {
std::cout << x << ' '; // 5 4 3 2 1
}
For associative containers:
std::map<std::string, int> map = {{"a", 1}, {"b", 2}, {"c", 3}};
for (const auto& key : map | views::keys) {
std::cout << key << ' '; // a b c
}
for (int value : map | views::values) {
std::cout << value << ' '; // 1 2 3
}
std::string str = "hello,world,test";
for (auto word : str | views::split(',')) {
for (char c : word) {
std::cout << c;
}
std::cout << '\\n';
}
// hello
// world
// test
Flatten range of ranges:
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}};
for (int x : nested | views::join) {
std::cout << x << ' '; // 1 2 3 4 5 6
}
Combine multiple ranges:
std::vector<int> nums = {1, 2, 3};
std::vector<std::string> words = {"one", "two", "three"};
for (auto [num, word] : views::zip(nums, words)) {
std::cout << num << ": " << word << '\\n';
}
// 1: one
// 2: two
// 3: three
Convert views to containers:
auto view = vec
| views::filter([](int x) { return x % 2 == 0; })
| views::transform([](int x) { return x * 2; });
// Convert to vector
std::vector<int> result(view.begin(), view.end());
// Or use ranges::to (C++23)
auto result2 = view | ranges::to<std::vector>();
Create view of existing range:
std::vector<int> vec = {1, 2, 3, 4, 5};
auto v = views::all(vec);
Non-owning view (reference):
std::vector<int> vec = {1, 2, 3};
ranges::ref_view view(vec);
Takes ownership of range:
auto owned = ranges::owning_view(std::vector{1, 2, 3});
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Filter odd numbers, square them, take first 3
auto result = numbers
| views::filter([](int x) { return x % 2 == 1; })
| views::transform([](int x) { return x * x; })
| views::take(3);
// Result: 1, 9, 25
auto result = numbers
| views::reverse
| views::drop(2)
| views::take(3);
// Start from end, skip 2, take 3: 8, 7, 6
Range algorithms accept projections:
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// Find person with age > 30
auto it = rng::find_if(people, [](int age) { return age > 30; }, &Person::age);
// Sort by name
rng::sort(people, {}, &Person::name);
std::string text = "line1\\nline2\\nline3\\nline4";
for (auto line : text | views::split('\\n') | views::take(2)) {
for (char c : line) std::cout << c;
std::cout << '\\n';
}
auto fibonacci = views::iota(0)
| views::transform([](int n) {
// ... compute nth fibonacci
})
| views::take(10);
Waiting for signal...
C++20 coroutines are functions that can suspend execution and resume later, enabling asynchronous programming and lazy generators without callbacks.
A function is a coroutine if it contains any of:
co_await: Suspend until expression completesco_yield: Suspend and produce a valueco_return: Complete and optionally return a valuegenerator<int> counter() {
for (int i = 0; i < 5; ++i) {
co_yield i; // Suspend and produce value
}
}
Simple generator coroutine:
#include <coroutine>
#include <iostream>
template<typename T>
struct generator {
struct promise_type {
T current_value;
generator get_return_object() {
return generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> coro;
generator(std::coroutine_handle<promise_type> h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
bool next() {
coro.resume();
return !coro.done();
}
T value() {
return coro.promise().current_value;
}
};
generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
// Usage:
auto gen = range(0, 5);
while (gen.next()) {
std::cout << gen.value() << '\\n';
}
Every coroutine requires a promise_type that defines behavior:
struct promise_type {
// Called to create the coroutine's return object
ReturnType get_return_object();
// Should coroutine suspend before executing body?
auto initial_suspend(); // Returns suspend_always or suspend_never
// Should coroutine suspend after completing?
auto final_suspend() noexcept;
// Handle co_yield value
auto yield_value(T value);
// Handle co_return
void return_void();
// OR
void return_value(T value);
// Exception handling
void unhandled_exception();
};
Manages coroutine state:
std::coroutine_handle<PromiseType> handle;
handle.resume(); // Resume coroutine execution
handle.done(); // Check if coroutine completed
handle.destroy(); // Destroy coroutine state
handle.promise(); // Access promise object
Suspend until awaitable completes:
Task async_operation() {
auto result = co_await some_async_call();
// Resumed when async_call completes
co_return result;
}
struct awaitable {
bool await_ready(); // Can we skip suspension?
void await_suspend(std::coroutine_handle<>); // Called when suspending
T await_resume(); // Called when resuming, returns value
};
generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
auto fib = fibonacci();
for (int i = 0; i < 10; ++i) {
fib.next();
std::cout << fib.value() << ' ';
}
// 0 1 1 2 3 5 8 13 21 34
Coroutines enable lazy evaluation—values computed on demand:
generator<int> lazy_transform(const std::vector<int>& vec) {
for (int x : vec) {
co_yield x * x; // Computed only when requested
}
}
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task async_work() {
co_await some_operation();
co_await another_operation();
co_return;
}
Make generator iterable:
template<typename T>
struct generator {
// ... previous code ...
struct iterator {
std::coroutine_handle<promise_type> coro;
iterator& operator++() {
coro.resume();
return *this;
}
bool operator!=(const iterator&) const {
return !coro.done();
}
T operator*() const {
return coro.promise().current_value;
}
};
iterator begin() {
coro.resume();
return {coro};
}
iterator end() {
return {nullptr};
}
};
// Now works with range-based for:
for (int x : range(0, 5)) {
std::cout << x << ' ';
}
Coroutine state is heap-allocated (can be optimized away):
// State includes:
// - Parameters
// - Local variables
// - Promise object
// - Suspension point information
| Approach | Callbacks | State Machines | Coroutines |
|---|---|---|---|
| Readability | Poor | Medium | Excellent |
| Complexity | High | High | Low |
| Error Handling | Difficult | Complex | Natural |
| Performance | Fast | Fast | Fast |
Waiting for signal...
C++20 modules replace the preprocessor-based #include system with a semantic import mechanism, offering faster compilation and better encapsulation.
// math.cppm (module interface file)
export module math;
export int add(int a, int b) {
return a + b;
}
export int subtract(int a, int b) {
return a - b;
}
// main.cpp
import math;
int main() {
auto result = add(5, 3); // Uses exported add()
return 0;
}
export module geometry;
export namespace geometry {
double circle_area(double radius);
double square_area(double side);
}
export module shapes;
export class Circle {
double radius_;
public:
Circle(double r) : radius_(r) {}
double area() const;
};
// Implementation can be in same file or separate
double Circle::area() const {
return 3.14159 * radius_ * radius_;
}
export module containers;
export template<typename T>
class Stack {
std::vector<T> data_;
public:
void push(const T& value) { data_.push_back(value); }
T pop();
};
Split large modules into partitions:
// math-basics.cppm (partition interface)
export module math:basics;
export int add(int a, int b) {
return a + b;
}
// math-advanced.cppm (partition interface)
export module math:advanced;
export double sqrt(double x) {
// implementation
}
// math.cppm (primary module interface)
export module math;
export import :basics; // Re-export partition
export import :advanced;
Separate interface from implementation:
// widget.cppm (module interface)
export module widget;
export class Widget {
public:
void process();
private:
void internal_helper();
};
// widget.cpp (module implementation)
module widget; // No 'export'
void Widget::process() {
internal_helper();
}
void Widget::internal_helper() {
// Implementation details
}
export module utils;
// Exported: Visible to importers
export void public_function();
// Not exported: Module-internal only
void private_function();
namespace {
// Internal linkage
void internal_function();
}
Import legacy headers:
module; // Global module fragment
#include <vector>
#include <string>
export module mymodule;
export class MyClass {
std::vector<std::string> data_; // Uses std types
};
Hide implementation details completely:
export module widget;
export class Widget {
public:
void process();
};
module :private; // Private fragment
// Implementation hidden from importers
void Widget::process() {
// ...
}
// Helper functions not visible
void helper() {
// ...
}
// With headers: Reparse every #include in every TU
#include <vector> // Parsed in every .cpp file
// With modules: Parse once, import everywhere
import std; // Pre-compiled module
// Header leaks macros
#define MAX(a,b) ((a)>(b)?(a):(b))
// Module doesn't leak implementation details
export module math;
// Macros don't leak to importers
// Headers: Order matters
#include "b.h" // Must come before a.h sometimes
#include "a.h"
// Modules: Order doesn't matter
import a;
import b;
// Module interface
export module database;
export class Connection {
// Only public interface exported
};
// Private implementation not exposed
import std; // Import entire standard library (C++23)
import std.core; // Core utilities
import std.io; // I/O facilities
import std.regex; // Regular expressions
| Aspect | Headers | Modules |
|---|---|---|
| Parsing | Every TU | Once |
| Macros | Leak | Don’t leak |
| Order | Matters | Independent |
| ODR Violations | Possible | Prevented |
| Compile Time | Slow | Fast |
Gradual adoption:
// 1. Start with new code
export module new_feature;
// 2. Wrap existing headers
export module legacy_wrapper;
export {
#include "old_header.h"
}
// 3. Eventually refactor to pure modules
# Compile module interface (generates BMI - Binary Module Interface)
g++ -std=c++20 -c math.cppm -o math.o
# Compile code that imports module
g++ -std=c++20 -c main.cpp -o main.o
# Link
g++ math.o main.o -o program
module math;
export int multiply(int a, int b) {
return a * b;
}C++20 introduces the three-way comparison operator <=> (nicknamed “spaceship”), dramatically simplifying comparison operator implementation.
#include <compare>
class Point {
int x_, y_;
public:
Point(int x, int y) : x_(x), y_(y) {}
// Default three-way comparison
auto operator<=>(const Point&) const = default;
};
Point p1{1, 2}, p2{3, 4};
p1 < p2; // Automatically works
p1 <= p2; // Automatically works
p1 > p2; // Automatically works
p1 >= p2; // Automatically works
p1 == p2; // Requires separate == or also defaulted
p1 != p2; // Generated from ==
The spaceship operator returns one of three ordering types:
Total order with substitutability:
std::strong_ordering::less
std::strong_ordering::equal
std::strong_ordering::greater
std::strong_ordering::equivalent // Same as equal
Use when equal values are truly identical:
class Integer {
int value_;
public:
std::strong_ordering operator<=>(const Integer& other) const {
return value_ <=> other.value_;
}
};
Total order without substitutability:
std::weak_ordering::less
std::weak_ordering::equivalent
std::weak_ordering::greater
Use when equal values may not be substitutable (e.g., case-insensitive strings):
class CaseInsensitiveString {
std::string value_;
public:
std::weak_ordering operator<=>(const CaseInsensitiveString& other) const {
// Compare ignoring case
// "Hello" and "hello" are equivalent but not equal
}
};
May have incomparable values:
std::partial_ordering::less
std::partial_ordering::equivalent
std::partial_ordering::greater
std::partial_ordering::unordered // Not comparable
Use for types like floating-point (NaN is unordered):
double a = std::nan(""), b = 1.0;
auto cmp = a <=> b; // Returns partial_ordering::unordered
The compiler can generate <=> for you:
struct Person {
std::string name;
int age;
auto operator<=>(const Person&) const = default;
// Compares members lexicographically: first name, then age
};
Comparison happens in declaration order:
struct Record {
int priority; // Compared first
std::string name; // Compared second if priority equal
double value; // Compared third if name equal
auto operator<=>(const Record&) const = default;
};
Implement custom logic:
class Version {
int major_, minor_, patch_;
public:
std::strong_ordering operator<=>(const Version& other) const {
if (auto cmp = major_ <=> other.major_; cmp != 0)
return cmp;
if (auto cmp = minor_ <=> other.minor_; cmp != 0)
return cmp;
return patch_ <=> other.patch_;
}
};
For efficiency, you might want to define == separately:
class String {
std::string data_;
public:
// Fast equality check
bool operator==(const String& other) const {
return data_ == other.data_;
}
// Three-way comparison for ordering
std::strong_ordering operator<=>(const String& other) const {
return data_ <=> other.data_;
}
};
Common pattern: default both operators:
struct Point {
int x, y;
bool operator==(const Point&) const = default;
auto operator<=>(const Point&) const = default;
};
Compare different types:
class Temperature {
double celsius_;
public:
explicit Temperature(double c) : celsius_(c) {}
std::partial_ordering operator<=>(double fahrenheit) const {
double c = (fahrenheit - 32) * 5/9;
return celsius_ <=> c;
}
};
Temperature t(100);
t < 212.0; // Compare Celsius with Fahrenheit
With defaulted <=>, you get all six comparison operators:
struct Data {
int value;
auto operator<=>(const Data&) const = default;
};
// Generated automatically:
// <, <=, >, >=
// Also need == for these:
bool operator==(const Data&) const = default;
// Then != is also generated
class Point {
int x_, y_;
public:
bool operator==(const Point& other) const {
return x_ == other.x_ && y_ == other.y_;
}
bool operator!=(const Point& other) const {
return !(*this == other);
}
bool operator<(const Point& other) const {
return (x_ < other.x_) || (x_ == other.x_ && y_ < other.y_);
}
bool operator<=(const Point& other) const {
return !(other < *this);
}
bool operator>(const Point& other) const {
return other < *this;
}
bool operator>=(const Point& other) const {
return !(*this < other);
}
};
class Point {
int x_, y_;
public:
auto operator<=>(const Point&) const = default;
bool operator==(const Point&) const = default;
};
// Done! All six operators generated
Before C++20 idiom:
bool operator<(const Point& other) const {
return std::tie(x_, y_) < std::tie(other.x_, other.y_);
}
C++20 makes this unnecessary:
auto operator<=>(const Point&) const = default;
Waiting for signal...
C++20 introduces designated initializers from C99, allowing you to initialize struct members by name. This improves clarity and maintainability, especially for structs with many members.
struct Config {
int width;
int height;
bool fullscreen;
double scale;
};
// Traditional initialization
Config c1{1920, 1080, true, 2.0}; // What does each value mean?
// C++20 designated initializers
Config c2{
.width = 1920,
.height = 1080,
.fullscreen = true,
.scale = 2.0
}; // Crystal clear!
You can initialize only specific members:
struct Settings {
int timeout = 30; // Default value
bool debug = false;
std::string host = "localhost";
};
Settings s{.debug = true}; // Only set debug, others use defaults
// timeout=30, debug=true, host="localhost"
C++ requires designated initializers to match declaration order:
struct Point {
int x;
int y;
int z;
};
Point p1{.x = 1, .y = 2, .z = 3}; // OK: follows declaration order
Point p2{.x = 1, .z = 3, .y = 2}; // ERROR: out of order!
Point p3{.y = 2, .z = 3}; // OK: can skip x
This differs from C99, which allows any order.
struct Data {
int a, b, c;
};
Data d1{1, .b = 2, .c = 3}; // ERROR: cannot mix
Data d2{.a = 1, 2, 3}; // ERROR: cannot mix
Data d3{.a = 1, .b = 2}; // OK: all designated
Data d4{1, 2, 3}; // OK: all positional
Designated initializers work with nested structures:
struct Address {
std::string street;
std::string city;
int zipcode;
};
struct Person {
std::string name;
int age;
Address address;
};
Person p{
.name = "Alice",
.age = 30,
.address = {
.street = "123 Main St",
.city = "Springfield",
.zipcode = 12345
}
};
struct Palette {
unsigned char colors[3];
std::string name;
};
Palette red{
.colors = {255, 0, 0},
.name = "Red"
};
Designated initializers use aggregate initialization, so they work only with aggregates:
struct Simple {
int x, y;
// No user-defined constructors
};
Simple s{.x = 1, .y = 2}; // OK: aggregate
struct WithConstructor {
int x, y;
WithConstructor(int a, int b) : x(a), y(b) {}
};
WithConstructor w{.x = 1, .y = 2}; // ERROR: not an aggregate
Designated initializers don’t work directly with std::array, but you can use them in enclosing structs:
struct Config {
std::array<int, 3> values;
std::string name;
};
Config cfg{
.values = {1, 2, 3},
.name = "Test"
};
Configuration management becomes much clearer:
struct ServerConfig {
std::string host = "0.0.0.0";
int port = 8080;
int max_connections = 100;
bool enable_tls = false;
std::string cert_path;
int timeout_seconds = 30;
bool verbose = false;
};
// Clear what each setting does
ServerConfig production{
.host = "api.example.com",
.port = 443,
.enable_tls = true,
.cert_path = "/etc/ssl/cert.pem",
.verbose = false
};
ServerConfig development{
.port = 3000,
.verbose = true
};
Particularly useful for options objects:
struct DrawOptions {
int line_width = 1;
unsigned color = 0x000000;
bool anti_alias = true;
double opacity = 1.0;
};
void draw_line(Point start, Point end, DrawOptions opts = {});
// Call with clear intent
draw_line(
{.x = 0, .y = 0},
{.x = 100, .y = 100},
{.color = 0xFF0000, .line_width = 3}
);
struct WindowConfig {
int width, height;
std::string title;
bool resizable, fullscreen;
};
// Hard to read
WindowConfig cfg{800, 600, "My App", true, false};
// Better, but verbose
WindowConfig cfg;
cfg.width = 800;
cfg.height = 600;
cfg.title = "My App";
cfg.resizable = true;
cfg.fullscreen = false;
WindowConfig cfg{
.width = 800,
.height = 600,
.title = "My App",
.resizable = true,
.fullscreen = false
};
struct Constants {
int max_retries;
double timeout;
};
constexpr Constants network_defaults{
.max_retries = 3,
.timeout = 5.0
};
struct Rectangle { int width = 10; int height = 20; std::string color = "red"; }; // Initialize rect with width=50, height=30, keep default color Rectangle rect{};
Waiting for signal...
Modern C++ continuously expands compile-time capabilities. C++20 and C++23 bring significant improvements to constexpr and introduce consteval and constinit.
constexpr int square(int x) {
return x * x;
}
constexpr int result = square(5); // Computed at compile-time
int runtime_val = square(n); // Can still be used at runtime
Allows multiple statements, loops, and local variables:
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int fact5 = factorial(5); // 120, computed at compile-time
auto squared = [](int x) constexpr { return x * x; };
constexpr int val = squared(10); // 100
Virtual functions, try-catch, dynamic allocation, and more:
#include <vector>
#include <algorithm>
constexpr int sum_vector() {
std::vector<int> vec{1, 2, 3, 4, 5}; // Dynamic allocation!
int sum = 0;
for (int x : vec) {
sum += x;
}
return sum;
}
constexpr int result = sum_vector(); // 15, at compile-time
Functions that must execute at compile-time:
consteval int compile_time_only(int x) {
return x * x;
}
constexpr int value1 = compile_time_only(5); // OK: compile-time
int n = 5;
int value2 = compile_time_only(n); // ERROR: not compile-time
Generate compile-time constants:
consteval unsigned hash_string(const char* str) {
unsigned hash = 0;
while (*str) {
hash = hash * 31 + *str++;
}
return hash;
}
switch (event_type) {
case hash_string("click"): // Computed at compile-time
handle_click();
break;
case hash_string("keypress"):
handle_key();
break;
}
constexpr int maybe_compile_time(int x) {
return x * 2;
}
consteval int always_compile_time(int x) {
return x * 2;
}
int runtime_value = 10;
int a = maybe_compile_time(5); // OK: compile-time
int b = maybe_compile_time(runtime_value); // OK: runtime
int c = always_compile_time(5); // OK: compile-time
int d = always_compile_time(runtime_value); // ERROR!
Ensures variables are initialized at compile-time, but allows runtime modification:
constinit int global_counter = 42; // Must be compile-time initialized
void increment() {
++global_counter; // OK: can modify at runtime
}
// constexpr would make it immutable
constexpr int immutable = 42;
// immutable = 43; // ERROR: cannot modify
// constinit ensures initialization, allows modification
constinit int mutable_init = compute_value(); // compute_value() must be constexpr
constinit is primarily for static/thread-local variables:
constinit static int initialized_once = expensive_computation();
// Guarantees no dynamic initialization order issues
// header.h
constinit extern int global_config; // Declaration
// source.cpp
constinit int global_config = compute_config(); // Must be compile-time
// other.cpp
// Safe to use global_config - guaranteed initialized
int value = global_config + 10;
Different code paths for compile-time vs runtime:
constexpr int compute(int x) {
if consteval {
// This branch runs only at compile-time
return expensive_compile_time_computation(x);
} else {
// This branch runs only at runtime
return fast_runtime_computation(x);
}
}
consteval int compile_time_sqrt(int x) {
// Simple algorithm acceptable for compile-time
int result = 0;
while (result * result < x) ++result;
return result;
}
int runtime_sqrt(int x) {
// Optimized runtime algorithm
return std::sqrt(x);
}
constexpr int safe_sqrt(int x) {
if consteval {
return compile_time_sqrt(x);
} else {
return runtime_sqrt(x);
}
}
constexpr std::string build_greeting() {
std::string greeting = "Hello, ";
greeting += "World!";
return greeting;
}
constexpr auto msg = build_greeting(); // Compile-time!
constexpr auto compute_primes(int max) {
std::vector<int> primes;
for (int n = 2; n <= max; ++n) {
bool is_prime = true;
for (int p : primes) {
if (n % p == 0) {
is_prime = false;
break;
}
}
if (is_prime) primes.push_back(n);
}
return primes;
}
constexpr auto primes = compute_primes(20);
// std::array or similar at compile-time
struct Shape {
virtual constexpr double area() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
double radius;
constexpr Circle(double r) : radius(r) {}
constexpr double area() const override {
return 3.14159 * radius * radius;
}
};
constexpr double compute_area() {
Circle c{5.0};
Shape& s = c;
return s.area(); // Virtual call at compile-time!
}
constexpr double area = compute_area(); // 78.5398...
constexpr int safe_divide(int a, int b) {
try {
if (b == 0) throw std::runtime_error("Division by zero");
return a / b;
} catch (...) {
return 0; // Error value
}
}
constexpr int result = safe_divide(10, 2); // 5
Note: Throwing exceptions causes compile-time evaluation to fail, but the structure is allowed.
Detect compile-time evaluation:
#include <type_traits>
constexpr int compute(int x) {
if (std::is_constant_evaluated()) {
// Compile-time: use simple algorithm
return x * x;
} else {
// Runtime: can use optimized version
return fast_multiply(x, x);
}
}
consteval auto get_build_config() {
struct Config {
const char* version;
int optimization_level;
bool debug;
};
return Config{
.version = "1.0.0",
.optimization_level = 2,
.debug = false
};
}
constexpr auto config = get_build_config();
consteval void validate_range(int min, int max) {
if (min >= max) {
throw "Invalid range"; // Compile-time error
}
}
template<int Min, int Max>
struct Range {
Range() {
validate_range(Min, Max); // Compile-time check
}
};
Range<1, 10> valid; // OK
Range<10, 1> invalid; // Compile error!
// Create a function that MUST execute at compile-time int cube(int x) { return x * x * x; } constexpr int result = cube(3); // Must work at compile-time
Waiting for signal...
C++20’s std::format provides Python-style string formatting that’s type-safe, efficient, and more intuitive than iostream or printf.
#include <format>
#include <string>
std::string name = "Alice";
int age = 30;
// C++20 format
std::string msg = std::format("Hello, {}! You are {} years old.", name, age);
// "Hello, Alice! You are 30 years old."
// printf: not type-safe, no std::string support
printf("Hello, %s! You are %d years old.\n", name.c_str(), age);
// iostream: verbose, awkward
std::ostringstream oss;
oss << "Hello, " << name << "! You are " << age << " years old.";
std::string msg = oss.str();
// std::format: clean and type-safe
auto msg = std::format("Hello, {}! You are {} years old.", name, age);
Reference arguments by position:
std::format("{0} {1} {0}", "Hello", "World"); // "Hello World Hello"
std::format("{1} {0}", "World", "Hello"); // "Hello World"
int num = 42;
std::format("{}", num); // "42"
std::format("{:d}", num); // "42" (decimal)
std::format("{:x}", num); // "2a" (hexadecimal lowercase)
std::format("{:X}", num); // "2A" (hexadecimal uppercase)
std::format("{:o}", num); // "52" (octal)
std::format("{:b}", num); // "101010" (binary)
std::format("{:06d}", num); // "000042" (zero-padded, width 6)
std::format("{:+d}", num); // "+42" (show sign)
std::format("{: d}", num); // " 42" (space for positive)
double pi = 3.14159265359;
std::format("{}", pi); // "3.14159265359"
std::format("{:.2f}", pi); // "3.14" (2 decimal places)
std::format("{:.5f}", pi); // "3.14159"
std::format("{:e}", pi); // "3.141593e+00" (scientific)
std::format("{:E}", pi); // "3.141593E+00"
std::format("{:g}", pi); // "3.14159" (general format)
std::format("{:10.2f}", pi); // " 3.14" (width 10)
std::format("{:010.2f}", pi); // "0000003.14" (zero-padded)
std::string text = "Hello";
std::format("{}", text); // "Hello"
std::format("{:10}", text); // "Hello " (width 10, left-aligned)
std::format("{:>10}", text); // " Hello" (right-aligned)
std::format("{:^10}", text); // " Hello " (centered)
std::format("{:*<10}", text); // "Hello*****" (fill with *)
std::format("{:.3}", text); // "Hel" (truncate to 3)
int n = 42;
std::format("{:<5}", n); // "42 " (left-aligned, width 5)
std::format("{:>5}", n); // " 42" (right-aligned)
std::format("{:^5}", n); // " 42 " (centered)
std::format("{:*>5}", n); // "***42" (fill with *, right-aligned)
std::format("{:0>5}", n); // "00042" (zero-padded)
int* ptr = &value;
std::format("{}", ptr); // "0x7ffc12345678"
std::format("{:p}", ptr); // "0x7ffc12345678"
bool flag = true;
std::format("{}", flag); // "true"
std::format("{:s}", flag); // "true" (string representation)
std::format("{:d}", flag); // "1" (numeric representation)
char ch = 'A';
std::format("{}", ch); // "A"
std::format("{:d}", ch); // "65" (ASCII value)
Format strings are checked at compile-time:
int n = 42;
std::format("{}", n); // OK
std::format("{:d}", n); // OK
std::format("{:f}", n); // Compile error: can't format int as float
std::format("{} {}", n); // Compile error: too few arguments
Extend formatting for your types:
struct Point {
int x, y;
};
template<>
struct std::formatter<Point> {
constexpr auto parse(format_parse_context& ctx) {
return ctx.begin();
}
auto format(const Point& p, format_context& ctx) const {
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
Point p{10, 20};
std::format("Point: {}", p); // "Point: (10, 20)"
Support format specifications:
template<>
struct std::formatter<Point> {
char presentation = 'c'; // 'c' for cartesian, 'p' for polar
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && (*it == 'c' || *it == 'p')) {
presentation = *it++;
}
return it;
}
auto format(const Point& p, format_context& ctx) const {
if (presentation == 'p') {
double r = std::sqrt(p.x*p.x + p.y*p.y);
double theta = std::atan2(p.y, p.x);
return std::format_to(ctx.out(), "r={:.2f}, θ={:.2f}", r, theta);
}
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
Point p{3, 4};
std::format("{:c}", p); // "(3, 4)"
std::format("{:p}", p); // "r=5.00, θ=0.93"
Write directly to output iterators for efficiency:
std::string buffer;
std::format_to(std::back_inserter(buffer), "Value: {}", 42);
// buffer == "Value: 42"
std::vector<char> vec;
std::format_to(std::back_inserter(vec), "Hello {}", "World");
Format with size limit:
char buffer[20];
auto result = std::format_to_n(buffer, sizeof(buffer), "Value: {}", 12345);
// result.out points past the last written character
// result.size is the total that would have been written
Calculate size without formatting:
size_t size = std::formatted_size("Value: {}, {}", 42, "test");
std::string buffer;
buffer.reserve(size);
std::format_to(std::back_inserter(buffer), "Value: {}, {}", 42, "test");
std::locale loc("de_DE.UTF-8");
double value = 1234.56;
std::format("{:L}", value); // "1,234.56" (default locale)
std::format(loc, "{:L}", value); // "1.234,56" (German locale)
std::format("{{}}"); // "{}"
std::format("{{{}}} ", 42); // "{42} "
When format string isn’t known at compile-time:
std::string fmt = get_format_from_user(); // Runtime string
std::string result = std::vformat(fmt, std::make_format_args(arg1, arg2));
std::format is typically faster than iostream:
// Iostream: multiple virtual calls, manipulator overhead
std::ostringstream oss;
oss << "Value: " << std::setw(10) << std::setfill('0') << value;
// std::format: single operation, optimized
auto str = std::format("Value: {:010}", value);
// printf
printf("%s: %d items, %.2f total\n", name.c_str(), count, total);
// std::format
std::println("{}: {} items, {:.2f} total", name, count, total);
// or
std::cout << std::format("{}: {} items, {:.2f} total\n", name, count, total);
C++23 adds convenience functions:
std::print("Hello, {}!\n", name); // Print to stdout
std::println("Value: {}", value); // Print with newline
std::print(stderr, "Error: {}\n", msg); // Print to stderr
double price = 19.99; int quantity = 3; // Format as: "Total: $59.97 (3 items)" std::string msg = std::format();
Waiting for signal...
C++23 introduces std::expected<T, E>, a vocabulary type for functions that can return either a value or an error. It provides a type-safe alternative to exceptions and error codes.
std::expected<T, E> holds either a value of type T (success) or an error of type E (failure):
#include <expected>
#include <string>
std::expected<int, std::string> parse_int(const std::string& str) {
try {
return std::stoi(str);
} catch (...) {
return std::unexpected("Invalid integer format");
}
}
auto result = parse_int("42");
if (result) {
std::cout << "Parsed: " << *result << '\n'; // 42
} else {
std::cout << "Error: " << result.error() << '\n';
}
std::expected<double, std::string> divide(double a, double b) {
if (b == 0.0) {
return std::unexpected("Division by zero");
}
return a / b;
}
auto result = divide(10.0, 2.0);
// Method 1: Contextual conversion to bool
if (result) {
double value = *result; // Dereference to get value
}
// Method 2: has_value()
if (result.has_value()) {
double value = result.value(); // Throws if error
}
// Method 3: Check for error
if (!result) {
std::string err = result.error();
}
auto result = divide(10.0, 2.0);
// Dereference (undefined behavior if error)
double v1 = *result;
double v2 = result.operator*();
// value() - throws std::bad_expected_access if error
double v3 = result.value();
// value_or() - returns default if error
double v4 = result.value_or(0.0);
enum class Error {
FileNotFound,
PermissionDenied,
InvalidFormat
};
std::expected<std::string, Error> read_file(const std::string& path) {
if (!file_exists(path)) {
return std::unexpected(Error::FileNotFound);
}
if (!has_permission(path)) {
return std::unexpected(Error::PermissionDenied);
}
// Read and return content
return file_content;
}
struct ParseError {
std::string message;
size_t position;
ParseError(std::string msg, size_t pos)
: message(std::move(msg)), position(pos) {}
};
std::expected<int, ParseError> parse_number(const std::string& str) {
for (size_t i = 0; i < str.length(); ++i) {
if (!std::isdigit(str[i])) {
return std::unexpected(ParseError{
"Invalid character", i
});
}
}
return std::stoi(str);
}
C++23 expected supports monadic operations for chaining:
std::expected<int, std::string> parse_int(const std::string& s);
std::expected<int, std::string> validate_positive(int n);
std::expected<double, std::string> compute_sqrt(int n);
auto result = parse_int("16")
.and_then(validate_positive)
.and_then(compute_sqrt);
if (result) {
std::cout << "Square root: " << *result << '\n'; // 4.0
}
Implementation:
std::expected<int, std::string> validate_positive(int n) {
if (n > 0) {
return n;
}
return std::unexpected("Number must be positive");
}
std::expected<double, std::string> compute_sqrt(int n) {
return std::sqrt(n);
}
auto result = parse_int("invalid")
.or_else([](const std::string& err) -> std::expected<int, std::string> {
std::cerr << "Parse error: " << err << '\n';
return 0; // Return default value
});
auto result = parse_int("42")
.transform([](int n) { return n * 2; }); // std::expected<int, std::string>
if (result) {
std::cout << *result << '\n'; // 84
}
auto result = parse_int("invalid")
.transform_error([](const std::string& err) {
return Error::InvalidFormat; // Convert error type
});
// result is std::expected<int, Error>
enum class FileError {
NotFound,
PermissionDenied,
InvalidFormat,
ProcessingFailed
};
std::expected<std::string, FileError> read_file(const std::string& path);
std::expected<Data, FileError> parse_data(const std::string& content);
std::expected<Result, FileError> process_data(const Data& data);
std::expected<void, FileError> save_result(const Result& result);
// Chain operations
auto outcome = read_file("input.txt")
.and_then(parse_data)
.and_then(process_data)
.and_then([](const Result& r) {
return save_result(r);
});
if (!outcome) {
switch (outcome.error()) {
case FileError::NotFound:
std::cerr << "File not found\n";
break;
case FileError::PermissionDenied:
std::cerr << "Permission denied\n";
break;
// ...
}
}
std::expected<void, E>For operations that don’t return a value:
std::expected<void, std::string> validate_config(const Config& cfg) {
if (cfg.port < 1024) {
return std::unexpected("Port must be >= 1024");
}
if (cfg.host.empty()) {
return std::unexpected("Host cannot be empty");
}
return {}; // Success
}
auto result = validate_config(config);
if (!result) {
std::cerr << "Validation failed: " << result.error() << '\n';
}
// Exceptions: implicit control flow
double divide_throws(double a, double b) {
if (b == 0.0) throw std::runtime_error("Division by zero");
return a / b;
}
try {
auto result = divide_throws(10.0, 0.0);
} catch (const std::exception& e) {
// Handle error
}
// std::expected: explicit error handling
std::expected<double, std::string> divide_expected(double a, double b) {
if (b == 0.0) return std::unexpected("Division by zero");
return a / b;
}
auto result = divide_expected(10.0, 0.0);
if (!result) {
// Handle error explicitly
}
// std::optional: success/failure, but no error information
std::optional<int> parse_int_opt(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt; // No error information
}
}
// std::expected: success/failure with error details
std::expected<int, std::string> parse_int_exp(const std::string& s) {
try {
return std::stoi(s);
} catch (const std::exception& e) {
return std::unexpected(e.what()); // Error details preserved
}
}
// C-style error codes
int parse_int_code(const std::string& s, int* out, std::string* error) {
try {
*out = std::stoi(s);
return 0; // Success
} catch (const std::exception& e) {
*error = e.what();
return -1; // Failure
}
}
// std::expected: type-safe, ergonomic
std::expected<int, std::string> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (const std::exception& e) {
return std::unexpected(e.what());
}
}
Simplify error propagation with monadic operations:
std::expected<int, Error> step1();
std::expected<double, Error> step2(int);
std::expected<std::string, Error> step3(double);
std::expected<std::string, Error> process() {
return step1()
.and_then(step2)
.and_then(step3);
// Error automatically propagates through chain
}
✅ Use when:
❌ Don’t use when:
std::expected<double, std::string> safe_sqrt(double x) { if (x < 0) { return std::("Cannot compute sqrt of negative number"); } return std::sqrt(x); }
Waiting for signal...
CMake has become the de facto standard for C++ build configuration. It generates platform-specific build files and manages dependencies, compiler flags, and project structure.
cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Create executable
add_executable(myapp main.cpp)
Build process:
mkdir build
cd build
cmake ..
cmake --build .
project/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── utils.cpp
│ └── utils.h
├── include/
│ └── mylib/
│ └── api.h
├── tests/
│ └── test_main.cpp
└── build/ (generated)
Root CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(MyProject)
set(CMAKE_CXX_STANDARD 20)
# Include directories
include_directories(include)
# Source files
add_executable(myapp
src/main.cpp
src/utils.cpp
)
# Enable testing
enable_testing()
add_subdirectory(tests)
add_library(mylib STATIC
src/lib.cpp
src/helper.cpp
)
target_include_directories(mylib PUBLIC include)
# Link library to executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
add_library(mylib SHARED
src/lib.cpp
)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)
Modern CMake uses target-based configuration:
add_library(mylib src/lib.cpp)
# Include directories
target_include_directories(mylib
PUBLIC include # Consumers need these
PRIVATE src/internal # Only this target needs these
)
# Compile definitions
target_compile_definitions(mylib
PUBLIC API_VERSION=2
PRIVATE INTERNAL_DEBUG
)
# Compile options
target_compile_options(mylib
PRIVATE -Wall -Wextra -Wpedantic
)
# Link libraries
target_link_libraries(mylib
PUBLIC fmt::fmt # Consumers need this too
PRIVATE spdlog::spdlog # Only this target needs this
)
Visibility:
# Global flags (discouraged)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
# Target-specific flags (preferred)
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
# Set default build type
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# Build type specific flags
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
Build with specific type:
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Release ..
find_package(Boost 1.75 REQUIRED COMPONENTS system filesystem)
add_executable(myapp main.cpp)
target_link_libraries(myapp
Boost::system
Boost::filesystem
)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBXML libxml-2.0 REQUIRED)
target_include_directories(myapp PRIVATE ${LIBXML_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${LIBXML_LIBRARIES})
Download and build dependencies at configure time:
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)
Multiple dependencies:
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.12.0
)
FetchContent_MakeAvailable(googletest spdlog)
Conditional compilation based on configuration:
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
$<$<CONFIG:Release>:RELEASE_MODE>
)
target_link_libraries(myapp PRIVATE
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Linux>:pthread>
)
enable_testing()
add_executable(test_main tests/test_main.cpp)
target_link_libraries(test_main PRIVATE mylib GTest::gtest_main)
add_test(NAME MainTests COMMAND test_main)
Run tests:
cmake --build build
ctest --test-dir build
install(TARGETS myapp mylib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
install(DIRECTORY include/
DESTINATION include
)
Install to custom location:
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
cmake --install build
CMakePresets.json:
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"binaryDir": "build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"binaryDir": "build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "release",
"configurePreset": "release"
}
]
}
Use presets:
cmake --preset debug
cmake --build --preset debug
vcpkg install fmt spdlog
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake ..
conanfile.txt:
[requires]
fmt/9.1.0
spdlog/1.12.0
[generators]
CMakeDeps
CMakeToolchain
conan install . --build=missing
cmake -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake ..
target_* commandscmake_minimum_required appropriatelycmake_minimum_required(VERSION 3.20)
project(ModernApp VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Options
option(BUILD_TESTS "Build tests" ON)
option(ENABLE_WARNINGS "Enable compiler warnings" ON)
# Dependencies
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
# Library
add_library(applib
src/lib.cpp
src/utils.cpp
)
target_include_directories(applib PUBLIC include)
target_link_libraries(applib PUBLIC fmt::fmt)
if(ENABLE_WARNINGS)
target_compile_options(applib PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
endif()
# Executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE applib)
# Tests
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# Installation
install(TARGETS myapp applib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
# Link fmt library to myapp target add_executable(myapp main.cpp) (myapp PRIVATE fmt::fmt)
Google Test (gtest) is the most widely-used C++ testing framework, providing comprehensive features for unit testing, integration testing, and test-driven development.
#include <gtest/gtest.h>
// Simple function to test
int add(int a, int b) {
return a + b;
}
// TEST(TestSuiteName, TestName)
TEST(MathTests, Addition) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_EQ(add(-1, 1), 0);
EXPECT_EQ(add(0, 0), 0);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
TEST(AssertionExample, DemonstratesExpectVsAssert) {
EXPECT_EQ(1, 1); // Passes
EXPECT_EQ(1, 2); // Fails, but continues
EXPECT_EQ(3, 3); // Still runs
}
TEST(AssertionExample, AssertStopsOnFailure) {
ASSERT_EQ(1, 1); // Passes
ASSERT_EQ(1, 2); // Fails, stops test
EXPECT_EQ(3, 3); // Never runs
}
TEST(AssertionTypes, BooleanAssertions) {
EXPECT_TRUE(true);
EXPECT_FALSE(false);
}
TEST(AssertionTypes, ComparisonAssertions) {
EXPECT_EQ(10, 10); // Equal
EXPECT_NE(10, 20); // Not equal
EXPECT_LT(5, 10); // Less than
EXPECT_LE(5, 10); // Less or equal
EXPECT_GT(10, 5); // Greater than
EXPECT_GE(10, 5); // Greater or equal
}
TEST(AssertionTypes, StringAssertions) {
EXPECT_STREQ("hello", "hello"); // C-strings equal
EXPECT_STRNE("hello", "world"); // C-strings not equal
EXPECT_STRCASEEQ("Hello", "hello"); // Case-insensitive
}
TEST(AssertionTypes, FloatingPointAssertions) {
EXPECT_FLOAT_EQ(1.0f, 1.0001f); // Within 4 ULPs
EXPECT_DOUBLE_EQ(1.0, 1.0001); // Within 4 ULPs
EXPECT_NEAR(1.0, 1.1, 0.2); // Within absolute error
}
Reuse setup and teardown code:
class DatabaseTest : public ::testing::Test {
protected:
void SetUp() override {
// Runs before each test
db = std::make_unique<Database>();
db->connect("test.db");
}
void TearDown() override {
// Runs after each test
db->disconnect();
}
std::unique_ptr<Database> db;
};
TEST_F(DatabaseTest, InsertRecord) {
EXPECT_TRUE(db->insert("key", "value"));
EXPECT_EQ(db->get("key"), "value");
}
TEST_F(DatabaseTest, DeleteRecord) {
db->insert("key", "value");
EXPECT_TRUE(db->remove("key"));
EXPECT_FALSE(db->contains("key"));
}
Runs once per test suite:
class ExpensiveSetupTest : public ::testing::Test {
protected:
static void SetUpTestSuite() {
// Runs once before all tests in suite
shared_resource = new Resource();
}
static void TearDownTestSuite() {
// Runs once after all tests in suite
delete shared_resource;
}
static Resource* shared_resource;
};
Resource* ExpensiveSetupTest::shared_resource = nullptr;
Test with multiple inputs:
class PrimeTest : public ::testing::TestWithParam<int> {
};
TEST_P(PrimeTest, IsPrime) {
int n = GetParam();
EXPECT_TRUE(is_prime(n));
}
INSTANTIATE_TEST_SUITE_P(
PrimeNumbers,
PrimeTest,
::testing::Values(2, 3, 5, 7, 11, 13, 17, 19)
);
With multiple parameters:
class AdditionTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {
};
TEST_P(AdditionTest, ValidateAddition) {
auto [a, b, expected] = GetParam();
EXPECT_EQ(add(a, b), expected);
}
INSTANTIATE_TEST_SUITE_P(
AddCases,
AdditionTest,
::testing::Values(
std::make_tuple(1, 2, 3),
std::make_tuple(-1, 1, 0),
std::make_tuple(100, 200, 300)
)
);
Test for expected crashes or aborts:
void fatal_error(bool condition) {
if (condition) {
std::abort();
}
}
TEST(DeathTest, FatalError) {
EXPECT_DEATH(fatal_error(true), "");
EXPECT_EXIT(std::exit(42), ::testing::ExitedWithCode(42), "");
}
TEST(ExceptionTest, ThrowsException) {
EXPECT_THROW(risky_function(), std::runtime_error);
EXPECT_ANY_THROW(another_risky_function());
EXPECT_NO_THROW(safe_function());
}
TEST(ExceptionTest, SpecificException) {
try {
risky_function();
FAIL() << "Expected exception";
} catch (const std::runtime_error& e) {
EXPECT_STREQ(e.what(), "Expected error message");
}
}
#include <gmock/gmock.h>
using ::testing::MatchesRegex;
using ::testing::StartsWith;
using ::testing::EndsWith;
using ::testing::HasSubstr;
TEST(StringMatchers, RegexAndSubstrings) {
std::string text = "Hello, World!";
EXPECT_THAT(text, StartsWith("Hello"));
EXPECT_THAT(text, EndsWith("World!"));
EXPECT_THAT(text, HasSubstr("lo, Wo"));
EXPECT_THAT(text, MatchesRegex("H.*!"));
}
Container matchers:
using ::testing::ElementsAre;
using ::testing::UnorderedElementsAre;
using ::testing::Contains;
using ::testing::SizeIs;
TEST(ContainerMatchers, VectorMatching) {
std::vector<int> vec{1, 2, 3, 4, 5};
EXPECT_THAT(vec, ElementsAre(1, 2, 3, 4, 5));
EXPECT_THAT(vec, Contains(3));
EXPECT_THAT(vec, SizeIs(5));
}
#include <gmock/gmock.h>
// Interface to mock
class Database {
public:
virtual ~Database() = default;
virtual bool insert(const std::string& key, const std::string& value) = 0;
virtual std::string get(const std::string& key) = 0;
};
// Mock implementation
class MockDatabase : public Database {
public:
MOCK_METHOD(bool, insert, (const std::string& key, const std::string& value), (override));
MOCK_METHOD(std::string, get, (const std::string& key), (override));
};
// Class under test
class UserManager {
Database* db_;
public:
UserManager(Database* db) : db_(db) {}
bool addUser(const std::string& name) {
return db_->insert(name, "default_value");
}
};
TEST(UserManagerTest, AddsUser) {
MockDatabase mock_db;
UserManager manager(&mock_db);
// Set expectations
EXPECT_CALL(mock_db, insert("Alice", "default_value"))
.WillOnce(::testing::Return(true));
EXPECT_TRUE(manager.addUser("Alice"));
}
using ::testing::Return;
using ::testing::_;
using ::testing::AtLeast;
TEST(MockTest, Expectations) {
MockDatabase mock_db;
// Expect specific call
EXPECT_CALL(mock_db, get("key"))
.WillOnce(Return("value"));
// Expect any string argument
EXPECT_CALL(mock_db, get(_))
.WillRepeatedly(Return("default"));
// Expect at least 2 calls
EXPECT_CALL(mock_db, insert(_, _))
.Times(AtLeast(2))
.WillRepeatedly(Return(true));
}
namespace { // Anonymous namespace for test-only code
class CalculatorTest : public ::testing::Test {
protected:
Calculator calc;
};
TEST_F(CalculatorTest, Addition) {
EXPECT_EQ(calc.add(2, 3), 5);
}
TEST_F(CalculatorTest, Subtraction) {
EXPECT_EQ(calc.subtract(5, 3), 2);
}
} // namespace
# Run all tests
./test_binary
# Run specific test
./test_binary --gtest_filter=MathTests.Addition
# Run tests matching pattern
./test_binary --gtest_filter=Math*
# Repeat tests
./test_binary --gtest_repeat=10
# Shuffle test order
./test_binary --gtest_shuffle
# Generate XML output
./test_binary --gtest_output=xml:test_results.xml
enable_testing()
find_package(GTest REQUIRED)
add_executable(my_tests test_main.cpp test_math.cpp)
target_link_libraries(my_tests PRIVATE GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(my_tests)
Run with CTest:
ctest --output-on-failure
TEST(StringUtils, Reverse) {
EXPECT_EQ(reverse("hello"), "olleh"); // Test fails - function doesn't exist
}
std::string reverse(const std::string& str) {
return std::string(str.rbegin(), str.rend());
}
std::string reverse(std::string_view str) {
return std::string(str.rbegin(), str.rend());
}
// Create a test fixture for a Stack class class StackTest : public ::testing:: { protected: Stack<int> stack; };
C++ provides fine-grained control over performance. Understanding optimization techniques and measurement tools is essential for writing efficient code.
Never optimize without measuring. Use profilers to identify bottlenecks:
# Compile with debug symbols
g++ -g -O2 program.cpp -o program
# Profile with perf
perf record ./program
perf report
# Profile with Valgrind
valgrind --tool=callgrind ./program
kcachegrind callgrind.out.*
-O0 # No optimization (default, fastest compile)
-O1 # Basic optimization
-O2 # Recommended for release builds
-O3 # Aggressive optimization (may increase code size)
-Os # Optimize for size
-Ofast # -O3 + unsafe math optimizations
Enable link-time optimization:
g++ -O3 -flto program.cpp -o program
// Poor layout: 24 bytes (with padding)
struct Bad {
char a; // 1 byte + 7 padding
double b; // 8 bytes
char c; // 1 byte + 7 padding
};
// Good layout: 16 bytes
struct Good {
double b; // 8 bytes
char a; // 1 byte
char c; // 1 byte + 6 padding
};
static_assert(sizeof(Bad) == 24);
static_assert(sizeof(Good) == 16);
Prefer contiguous memory (vector) over linked structures:
// Cache-unfriendly
std::list<int> list; // Nodes scattered in memory
// Cache-friendly
std::vector<int> vec; // Contiguous memory
// Benchmark shows vector is often 10-100x faster for iteration
// AoS: Poor cache utilization if you only need x coordinates
struct Point { float x, y, z; };
std::vector<Point> points;
for (const auto& p : points) {
process(p.x); // Loads y and z unnecessarily
}
// SoA: Better cache utilization
struct Points {
std::vector<float> x;
std::vector<float> y;
std::vector<float> z;
};
Points points;
for (float x_val : points.x) {
process(x_val); // Only loads x values
}
Compiler automatically elides copies:
std::vector<int> create_vector() {
std::vector<int> vec(1000);
// ...
return vec; // No copy! RVO applied
}
auto v = create_vector(); // vec moved directly into v
std::string build_string() {
std::string result;
result += "Hello";
result += " World";
return result; // NRVO: no copy
}
std::vector<int> source = create_large_vector();
std::vector<int> dest = std::move(source); // Move, not copy
// source is now empty
// Bad: copies vector
void process(std::vector<int> vec) { }
// Good: references vector
void process(const std::vector<int>& vec) { }
// Better: use std::span for read-only access
void process(std::span<const int> data) { }
// Good: RVO applies
std::vector<int> compute() {
std::vector<int> result;
// ...
return result;
}
// Don't do this:
void compute(std::vector<int>& out) { // Output parameter
// ...
}
std::string optimizes for small strings:
std::string small = "Hello"; // No heap allocation (typically < 16 chars)
std::string large = "This is a very long string..."; // Heap allocated
Use string_view for temporary views:
void process(std::string_view sv) { // No copy
// ...
}
std::string str = "Hello";
process(str); // Efficient
process("World"); // No temporary string created
Avoid reallocations:
// Bad: multiple reallocations
std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {
vec.push_back(i); // May reallocate many times
}
// Good: single allocation
std::vector<int> vec;
vec.reserve(1000);
for (int i = 0; i < 1000; ++i) {
vec.push_back(i); // No reallocations
}
// Even better: direct initialization
std::vector<int> vec(1000);
std::iota(vec.begin(), vec.end(), 0);
std::vector<std::string> vec;
// push_back: creates temporary, then moves
vec.push_back(std::string("Hello"));
// emplace_back: constructs in-place
vec.emplace_back("Hello"); // No temporary
// With complex types
std::vector<std::pair<int, std::string>> pairs;
pairs.push_back({1, "one"}); // Creates temporary pair
pairs.emplace_back(1, "one"); // Constructs directly
Small functions benefit from inlining:
inline int square(int x) { return x * x; }
// Better: let compiler decide
constexpr int square(int x) { return x * x; }
Modern compilers inline automatically with optimization enabled.
int process(int x) {
if (x > 0) [[likely]] {
return x * 2;
} else {
return handle_error();
}
}
// Bad: branch in loop
for (int i = 0; i < n; ++i) {
if (condition) {
result += array[i];
}
}
// Better: predication
for (int i = 0; i < n; ++i) {
result += array[i] * condition; // Branchless
}
// O(n²) - slow for large n
std::vector<int> vec;
for (int i = 0; i < n; ++i) {
vec.erase(std::remove(vec.begin(), vec.end(), i), vec.end());
}
// O(n) - much faster
std::unordered_set<int> set(vec.begin(), vec.end());
Choose appropriate containers:
Move work to compile-time:
// Runtime computation
int factorial_runtime(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
// Compile-time computation
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int fact5 = factorial(5); // Computed at compile-time
#include <algorithm>
#include <execution>
std::vector<int> vec(1000000);
// Sequential
std::sort(vec.begin(), vec.end());
// Parallel
std::sort(std::execution::par, vec.begin(), vec.end());
// Parallel + vectorized
std::sort(std::execution::par_unseq, vec.begin(), vec.end());
// Slow: creates many temporary strings
std::string result = str1 + str2 + str3 + str4;
// Fast: single allocation
std::string result;
result.reserve(str1.size() + str2.size() + str3.size() + str4.size());
result += str1;
result += str2;
result += str3;
result += str4;
// Or use std::format (C++20)
auto result = std::format("{}{}{}{}", str1, str2, str3, str4);
// Virtual call: runtime dispatch
struct Base {
virtual void process() { }
};
// Template: compile-time dispatch (faster)
template<typename T>
void process(T& obj) {
obj.process(); // No virtual call
}
// Consider CRTP for static polymorphism
template<typename Derived>
struct Base {
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
For many small allocations:
#include <memory_resource>
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> vec(&pool); // Uses pool allocator
// All allocations come from pool - much faster
Use Google Benchmark:
#include <benchmark/benchmark.h>
static void BM_VectorPush(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> vec;
for (int i = 0; i < state.range(0); ++i) {
vec.push_back(i);
}
}
}
BENCHMARK(BM_VectorPush)->Range(8, 8<<10);
BENCHMARK_MAIN();
std::vector<int> vec; // Reserve space for 1000 elements to avoid reallocations vec.(1000);
Waiting for signal...
C++ provides extensive control over memory management. Understanding allocators, alignment, and memory debugging tools is crucial for high-performance applications.
// Stack allocation
int x = 42; // Automatic storage duration
char buffer[1024]; // Fixed size, fast
// Heap allocation
int* ptr = new int(42); // Manual lifetime
delete ptr;
auto vec = std::make_unique<std::vector<int>>(); // Automatic cleanup
struct alignas(16) Aligned {
double x, y;
};
static_assert(alignof(Aligned) == 16);
static_assert(sizeof(Aligned) == 16); // Padded to alignment
// Cache line alignment
struct alignas(64) CacheAligned {
int data;
// Padded to 64 bytes
};
#include <type_traits>
alignas(16) std::byte buffer[sizeof(MyClass)];
// Construct object in aligned buffer
MyClass* obj = new (buffer) MyClass(args);
// Destroy when done
obj->~MyClass();
// C++17: aligned new
void* ptr = ::operator new(size, std::align_val_t{64});
::operator delete(ptr, std::align_val_t{64});
// Aligned unique_ptr
template<typename T, std::size_t Alignment>
struct AlignedDeleter {
void operator()(T* ptr) {
ptr->~T();
::operator delete(ptr, std::align_val_t{Alignment});
}
};
template<typename T, std::size_t Alignment, typename... Args>
auto make_aligned_unique(Args&&... args) {
void* mem = ::operator new(sizeof(T), std::align_val_t{Alignment});
T* ptr = new (mem) T(std::forward<Args>(args)...);
return std::unique_ptr<T, AlignedDeleter<T, Alignment>>(ptr);
}
template<typename T>
class MyAllocator {
public:
using value_type = T;
MyAllocator() = default;
template<typename U>
MyAllocator(const MyAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
throw std::bad_array_new_length();
}
void* ptr = ::operator new(n * sizeof(T));
return static_cast<T*>(ptr);
}
void deallocate(T* ptr, std::size_t) noexcept {
::operator delete(ptr);
}
};
template<typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) { return true; }
template<typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) { return false; }
// Usage
std::vector<int, MyAllocator<int>> vec;
Allocate from stack buffer:
template<typename T, std::size_t N>
class StackAllocator {
alignas(T) std::byte buffer_[N * sizeof(T)];
std::byte* ptr_ = buffer_;
public:
using value_type = T;
T* allocate(std::size_t n) {
if (ptr_ + n * sizeof(T) > buffer_ + sizeof(buffer_)) {
throw std::bad_alloc();
}
T* result = reinterpret_cast<T*>(ptr_);
ptr_ += n * sizeof(T);
return result;
}
void deallocate(T*, std::size_t) noexcept {
// No-op for stack allocator
}
};
// Fast vector for small sizes
std::vector<int, StackAllocator<int, 100>> small_vec;
template<typename T, std::size_t BlockSize = 4096>
class MemoryPool {
union Node {
T value;
Node* next;
};
struct Block {
std::byte data[BlockSize];
Block* next;
};
Block* blocks_ = nullptr;
Node* free_list_ = nullptr;
public:
~MemoryPool() {
while (blocks_) {
Block* next = blocks_->next;
::operator delete(blocks_);
blocks_ = next;
}
}
T* allocate() {
if (!free_list_) {
allocate_block();
}
Node* node = free_list_;
free_list_ = node->next;
return &node->value;
}
void deallocate(T* ptr) {
Node* node = reinterpret_cast<Node*>(ptr);
node->next = free_list_;
free_list_ = node;
}
private:
void allocate_block() {
Block* block = static_cast<Block*>(::operator new(sizeof(Block)));
block->next = blocks_;
blocks_ = block;
const std::size_t node_count = BlockSize / sizeof(Node);
Node* nodes = reinterpret_cast<Node*>(block->data);
for (std::size_t i = 0; i < node_count; ++i) {
nodes[i].next = free_list_;
free_list_ = &nodes[i];
}
}
};
std::pmr provides runtime polymorphism for allocators:
#include <memory_resource>
// Monotonic allocator: fast, no deallocation
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> vec1(&pool);
std::pmr::string str(&pool);
// All allocations from pool
// Pool memory released when pool destroyed
// Unsynchronized pool: single-threaded
std::pmr::unsynchronized_pool_resource pool;
// Synchronized pool: thread-safe
std::pmr::synchronized_pool_resource thread_safe_pool;
// New/delete resource: default
std::pmr::new_delete_resource();
// Null resource: throws on allocation
std::pmr::null_memory_resource();
class LoggingResource : public std::pmr::memory_resource {
std::pmr::memory_resource* upstream_;
public:
LoggingResource(std::pmr::memory_resource* upstream)
: upstream_(upstream) {}
protected:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
std::cout << "Allocating " << bytes << " bytes\n";
return upstream_->allocate(bytes, alignment);
}
void do_deallocate(void* ptr, std::size_t bytes, std::size_t alignment) override {
std::cout << "Deallocating " << bytes << " bytes\n";
upstream_->deallocate(ptr, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
};
Detects memory errors:
# Compile with ASan
g++ -fsanitize=address -g program.cpp -o program
# Run
./program
Detects:
Example:
int main() {
int* ptr = new int[10];
delete[] ptr;
ptr[0] = 42; // ASan detects: heap-use-after-free
}
# Check for memory leaks
valgrind --leak-check=full ./program
# Memory profiler
valgrind --tool=massif ./program
ms_print massif.out.*
Detects uninitialized memory reads:
clang++ -fsanitize=memory -g program.cpp -o program
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
T* ptr_ = nullptr;
[[no_unique_address]] Deleter deleter_;
public:
unique_ptr() = default;
explicit unique_ptr(T* ptr) : ptr_(ptr) {}
~unique_ptr() {
if (ptr_) deleter_(ptr_);
}
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& other) noexcept
: ptr_(other.ptr_), deleter_(std::move(other.deleter_)) {
other.ptr_ = nullptr;
}
unique_ptr& operator=(unique_ptr&& other) noexcept {
reset(other.release());
deleter_ = std::move(other.deleter_);
return *this;
}
T* release() noexcept {
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void reset(T* ptr = nullptr) noexcept {
T* old_ptr = ptr_;
ptr_ = ptr;
if (old_ptr) deleter_(old_ptr);
}
T* get() const noexcept { return ptr_; }
T& operator*() const { return *ptr_; }
T* operator->() const noexcept { return ptr_; }
explicit operator bool() const noexcept { return ptr_ != nullptr; }
};
// Simplified shared_ptr internals
template<typename T>
class shared_ptr {
T* ptr_ = nullptr;
struct ControlBlock {
std::atomic<long> ref_count{1};
std::atomic<long> weak_count{1};
virtual ~ControlBlock() = default;
virtual void destroy_object() = 0;
virtual void destroy_control_block() = 0;
};
ControlBlock* control_ = nullptr;
public:
shared_ptr(T* ptr) : ptr_(ptr) {
control_ = new ControlBlockImpl(ptr);
}
shared_ptr(const shared_ptr& other)
: ptr_(other.ptr_), control_(other.control_) {
if (control_) {
control_->ref_count.fetch_add(1, std::memory_order_relaxed);
}
}
~shared_ptr() {
if (control_ && control_->ref_count.fetch_sub(1) == 1) {
control_->destroy_object();
if (control_->weak_count.fetch_sub(1) == 1) {
control_->destroy_control_block();
}
}
}
};
#include <atomic>
std::atomic<int> counter{0};
// Relaxed: no synchronization
counter.fetch_add(1, std::memory_order_relaxed);
// Acquire-Release: synchronize with matching operations
counter.store(1, std::memory_order_release);
int value = counter.load(std::memory_order_acquire);
// Sequential consistency: total order (default)
counter.fetch_add(1, std::memory_order_seq_cst);
template<typename F>
class ScopeGuard {
F cleanup_;
bool active_ = true;
public:
explicit ScopeGuard(F cleanup) : cleanup_(std::move(cleanup)) {}
~ScopeGuard() {
if (active_) cleanup_();
}
void dismiss() { active_ = false; }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
template<typename F>
auto make_scope_guard(F cleanup) {
return ScopeGuard<F>(std::move(cleanup));
}
// Usage
void process_file(const char* filename) {
FILE* file = fopen(filename, "r");
auto guard = make_scope_guard([=] { if (file) fclose(file); });
// Use file...
// Automatically closed on scope exit
}
// Force struct to be aligned to 64-byte boundary (cache line) struct (64) CacheAligned { int data[16]; };
Waiting for signal...
Modern C++ provides powerful features that, when used correctly, lead to clean, efficient, and maintainable code. This lesson covers essential idioms and best practices.
The fundamental C++ idiom: tie resource lifetime to object lifetime.
// Bad: Manual resource management
void process_file() {
FILE* file = fopen("data.txt", "r");
// ... process ...
fclose(file); // Easy to forget!
}
// Good: RAII with smart pointers
void process_file() {
auto file = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("data.txt", "r"), fclose
);
// Automatically closed
}
// Better: Use RAII wrappers
class File {
FILE* file_;
public:
explicit File(const char* path) : file_(fopen(path, "r")) {
if (!file_) throw std::runtime_error("Failed to open file");
}
~File() { if (file_) fclose(file_); }
File(const File&) = delete;
File& operator=(const File&) = delete;
FILE* get() const { return file_; }
};
If you don’t need custom resource management, don’t define special members:
// Good: Compiler-generated special members work perfectly
struct Person {
std::string name;
int age;
std::vector<std::string> hobbies;
// Compiler generates correct copy/move/destructor
};
If you define one special member, define all:
class Resource {
int* data_;
public:
// Constructor
Resource(int size) : data_(new int[size]) {}
// Destructor
~Resource() { delete[] data_; }
// Copy constructor
Resource(const Resource& other)
: data_(new int[other.size()]) {
std::copy(other.data_, other.data_ + size(), data_);
}
// Copy assignment
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data_;
data_ = new int[other.size()];
std::copy(other.data_, other.data_ + size(), data_);
}
return *this;
}
// Move constructor
Resource(Resource&& other) noexcept
: data_(other.data_) {
other.data_ = nullptr;
}
// Move assignment
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
}
return *this;
}
};
class Resource {
std::unique_ptr<int[]> data_;
size_t size_;
public:
Resource(size_t size)
: data_(std::make_unique<int[]>(size)), size_(size) {}
// Rule of Zero: compiler-generated special members work!
};
class String {
std::string data_;
public:
// Read-only access: const method
size_t length() const { return data_.length(); }
const char* c_str() const { return data_.c_str(); }
// Modification: non-const method
void append(const std::string& str) { data_ += str; }
// Const and non-const overloads
char& operator[](size_t i) { return data_[i]; }
const char& operator[](size_t i) const { return data_[i]; }
};
// Use const references to avoid copies
void print(const String& str) { // Won't copy
std::cout << str.c_str() << '\n';
}
// Bad: Nullable, unclear ownership
void process(int* value) {
if (value) { // Must check for null
*value += 10;
}
}
// Good: Not nullable, clear value semantics
void process(int& value) {
value += 10; // Always valid
}
// When nullability is needed: std::optional
void process(std::optional<int>& value) {
if (value) {
*value += 10;
}
}
// Direct initialization
int x{42};
std::string name{"Alice"};
std::vector<int> vec{1, 2, 3, 4, 5};
// Avoid narrowing conversions
int i{3.14}; // Error: narrowing
// For copy construction, use auto
auto str = std::string("Hello"); // Clear intent
class Config {
// In-class initializers (C++11)
int timeout = 30;
std::string host = "localhost";
bool debug = false;
public:
// Constructor: only set non-default values
Config(std::string h) : host(std::move(h)) {}
};
// Good: Obvious type
auto count = 42;
auto name = std::string("Alice");
auto ptr = std::make_unique<Widget>();
// Good: Complex types
auto it = vec.begin();
auto result = some_function_with_long_return_type();
// Good: Generic code
template<typename T>
auto process(const T& value) {
auto result = value.compute();
return result;
}
// Bad: Type not obvious
auto x = get_value(); // What type is x?
// Better: Explicit when clarity helps
std::string x = get_value();
std::vector<int> vec{1, 2, 3, 4, 5};
// Read-only: const reference
for (const auto& value : vec) {
std::cout << value << ' ';
}
// Modify: reference
for (auto& value : vec) {
value *= 2;
}
// Copy when needed
for (auto value : vec) {
value += 1; // Modifies copy
}
// Bad: Manual loop
int sum = 0;
for (size_t i = 0; i < vec.size(); ++i) {
sum += vec[i];
}
// Good: Algorithm expresses intent
int sum = std::accumulate(vec.begin(), vec.end(), 0);
// Better: Ranges (C++20)
int sum = std::ranges::fold_left(vec, 0, std::plus{});
More examples:
// Find element
auto it = std::find(vec.begin(), vec.end(), 42);
// Transform
std::vector<int> doubled;
std::transform(vec.begin(), vec.end(),
std::back_inserter(doubled),
[](int x) { return x * 2; });
// Filter
std::vector<int> evens;
std::copy_if(vec.begin(), vec.end(),
std::back_inserter(evens),
[](int x) { return x % 2 == 0; });
// Use exceptions for errors you can't handle locally
void process_file(const std::string& path) {
std::ifstream file(path);
if (!file) {
throw std::runtime_error("Cannot open file: " + path);
}
// Process...
}
// Handle where you can recover
try {
process_file("data.txt");
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
// Fallback action
}
// Use std::expected for errors that are expected
std::expected<int, std::string> parse_int(const std::string& str) {
try {
return std::stoi(str);
} catch (...) {
return std::unexpected("Invalid integer");
}
}
auto result = parse_int(input);
if (result) {
use(*result);
} else {
handle_error(result.error());
}
// Don't do this without profiling:
void process() {
// "Optimization": manually unrolled loop, hard to read
for (size_t i = 0; i < n; i += 4) {
result[i] = data[i] * 2;
result[i+1] = data[i+1] * 2;
result[i+2] = data[i+2] * 2;
result[i+3] = data[i+3] * 2;
}
}
// Instead: Write clear code, let compiler optimize
void process() {
for (size_t i = 0; i < n; ++i) {
result[i] = data[i] * 2;
}
// Modern compilers will vectorize this automatically
}
// Bad: Hard-coded dependency
class UserManager {
Database db; // Tightly coupled
public:
void save_user(const User& user) {
db.insert(user);
}
};
// Good: Inject dependency
class UserManager {
Database& db_; // Dependency injected
public:
UserManager(Database& db) : db_(db) {}
void save_user(const User& user) {
db_.insert(user);
}
};
// Better: Use interface
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual void insert(const User&) = 0;
};
class UserManager {
IDatabase& db_;
public:
UserManager(IDatabase& db) : db_(db) {}
void save_user(const User& user) {
db_.insert(user);
}
};
// Bad: Inheritance for reuse
class Stack : public std::vector<int> {
// Exposes vector interface (push_back, etc.)
};
// Good: Composition
class Stack {
std::vector<int> data_;
public:
void push(int value) { data_.push_back(value); }
int pop() {
int val = data_.back();
data_.pop_back();
return val;
}
// Only expose stack operations
};
// Prefer value semantics
std::vector<std::string> names; // Owns strings
names.push_back("Alice"); // Makes copy/move
// Over pointer semantics
std::vector<std::string*> names; // Doesn't own
names.push_back(new std::string("Alice")); // Manual memory management
// Bad: Primitive obsession
void transfer(int from_account, int to_account, int amount);
transfer(123, 100, 456); // Easy to swap arguments!
// Good: Strong types
struct AccountId {
int value;
explicit AccountId(int v) : value(v) {}
};
struct Amount {
int value;
explicit Amount(int v) : value(v) {}
};
void transfer(AccountId from, AccountId to, Amount amount);
transfer(AccountId{123}, AccountId{456}, Amount{100}); // Type-safe!
// Good: make_unique/make_shared
auto ptr = std::make_unique<Widget>(args);
auto shared = std::make_shared<Widget>(args);
// Custom factory
template<typename T, typename... Args>
auto create(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
auto widget = create<Widget>(arg1, arg2);
// Classes: PascalCase
class UserManager {};
// Functions/variables: snake_case
void process_data();
int user_count;
// Constants: SCREAMING_SNAKE_CASE or kPascalCase
constexpr int MAX_USERS = 100;
constexpr int kMaxUsers = 100;
// Member variables: trailing underscore
class Widget {
int value_;
std::string name_;
};
// Template parameters: PascalCase
template<typename ValueType, typename AllocatorType>
class Container {};
/**
* @brief Computes the factorial of a number.
*
* @param n The number (must be non-negative)
* @return The factorial of n
* @throws std::invalid_argument if n < 0
*/
int factorial(int n) {
if (n < 0) {
throw std::invalid_argument("n must be non-negative");
}
// ...
}
✅ Use RAII for resource management
✅ Prefer smart pointers to raw pointers
✅ Use const correctness
✅ Prefer references to pointers
✅ Initialize variables at declaration
✅ Use auto when type is obvious
✅ Prefer algorithms to raw loops
✅ Use range-based for loops
✅ Follow Rule of Zero when possible
✅ Use exceptions for exceptional cases
✅ Avoid premature optimization
✅ Prefer composition to inheritance
✅ Use strong types for type safety
✅ Document public APIs
✅ Enable compiler warnings (-Wall -Wextra)
// Make this function take a non-nullable reference
void increment(int_ value) {
value += 1;
}Waiting for signal...
C++ has evolved from a “C with Classes” in 1983 to a sophisticated, multi-paradigm language powering everything from embedded systems to game engines, financial systems to web browsers.
The first ISO standard brought:
// C++98 style
std::vector<int> vec;
for (std::vector<int>::iterator it = vec.begin();
it != vec.end(); ++it) {
std::cout << *it << '\n';
}
Revolutionary update with:
// C++11 style
auto vec = std::make_unique<std::vector<int>>();
for (const auto& item : *vec) {
std::cout << item << '\n';
}
Incremental improvements:
auto lambda = [](auto x, auto y) { return x + y; };
Significant additions:
std::map<std::string, int> map = {{"Alice", 30}};
if (auto [it, inserted] = map.insert({"Bob", 25}); inserted) {
std::cout << "Inserted: " << it->first << '\n';
}
Game-changing features:
// C++20: Concepts constrain templates
template<std::integral T>
T add(T a, T b) {
return a + b;
}
// Ranges: composable algorithms
auto result = vec
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 2; });
Recent additions:
// C++23: std::print
std::println("Hello, {}!", "World");
// std::expected
std::expected<int, Error> result = compute();
if (result) {
process(*result);
}
GCC (GNU Compiler Collection)
Clang/LLVM
MSVC (Microsoft Visual C++)
Intel C++ Compiler
CMake: Industry standard, cross-platform
Meson: Fast, user-friendly
Bazel: Scalable, reproducible builds
xmake: Lua-based, modern
vcpkg: Microsoft-backed, cross-platform
Conan: Decentralized, flexible
Hunter: CMake-driven
Google Test: Most widely used
Catch2: Header-only, BDD-style
Doctest: Fast, minimal
Boost: Extensive, battle-tested
Abseil: Google’s C++ library
fmt: Fast formatting
spdlog: Fast logging
nlohmann/json: JSON processing
cpr: HTTP requests
Proposed features:
Safety: Addressing memory safety concerns
Simplicity: Reducing complexity
Performance: Zero-cost abstractions
Tooling: Better IDE support
Modules: Universal adoption
Zero-Overhead Abstraction
// High-level abstractions...
auto result = vec
| std::views::filter(is_valid)
| std::views::transform(compute);
// ...compile to efficient machine code
for (size_t i = 0; i < vec.size(); ++i) {
if (is_valid(vec[i])) {
result.push_back(compute(vec[i]));
}
}
Type Safety
// Compile-time checking prevents errors
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
T add(T a, T b) { // Only accepts numeric types
return a + b;
}
Resource Management
// RAII: no manual cleanup needed
{
auto file = std::ifstream("data.txt");
auto buffer = std::vector<char>(1024);
// Automatic cleanup on scope exit
}
C++ developers are in demand for:
Typical roles:
Beginner
Intermediate
Advanced
Find projects on GitHub:
C++ evolves every three years:
“You don’t pay for what you don’t use” - Bjarne Stroustrup
C++ provides powerful abstractions without runtime cost. You can write high-level code that compiles to efficient machine code.
Multi-Paradigm
Backward Compatibility
C++ maintains compatibility with previous versions, allowing gradual modernization of codebases.
C++ is complex but rewarding. It offers:
The journey from C++98 to C++23 shows continuous improvement while maintaining backward compatibility. Modern C++ (C++11 onward) is a different language from legacy C++.
Key Takeaways:
The C++ community is active and welcoming. Whether you’re building games, financial systems, embedded devices, or exploring systems programming, C++ provides the tools and performance you need.
Your journey doesn’t end here—it’s just beginning. Keep coding, keep learning, and embrace modern C++!
Waiting for signal...
Congratulations on completing this C++ course! You now have a solid foundation in modern C++ programming. Keep practicing, building projects, and exploring the ever-evolving C++ ecosystem. Happy coding! 🚀