Advanced C++ Cheatsheet
An interactive guide to advanced C++ concepts, data structures, and design patterns.
0. C++ Fundamentals
This section covers the foundational concepts of C++ programming, essential for building any application.
Includes
Standard libraries provide pre-written code for common tasks. Use #include <library_name> to bring them into your program.
<iostream>: For input/output operations (std::cout,std::cin).<vector>: For dynamic arrays (std::vector).<string>: For string manipulation (std::string).<cmath>: For common mathematical functions (sqrt,pow).<algorithm>: For a wide range of algorithms (std::sort,std::min,std::max).<map>: For associative arrays (key-value pairs,std::map).<set>: For unique, sorted collections (std::set).<memory>: For smart pointers (std::unique_ptr,std::shared_ptr).<fstream>: For file input/output (std::ifstream,std::ofstream).<sstream>: For string stream manipulation (std::stringstream).<stdexcept>: For standard exception classes (std::runtime_error).
Basic Data Types
Fundamental types for storing different kinds of data.
int: Whole numbers (integers), e.g.,5,10,0.float: Real numbers with decimal points (single precision), e.g.,3.14f,-0.5f.double: Real numbers with decimal points (double precision), e.g.,3.14159,-0.01.char: Single characters, e.g.,'a','B','7','?'.bool: Boolean values:trueorfalse.std::string: Sequences of characters (text), e.g.,"hello","World"(requires#include <string>).
Variables
Named memory locations to store data.
int age = 30; // Declare an integer variable 'age' and initialize it to 30.
double price = 19.99; // Declare a double variable 'price' and initialize it to 19.99.
std::string name = "Alice"; // Declare a string variable 'name' and initialize it to "Alice".
bool isActive = true; // Declare a boolean variable 'isActive' and initialize it to true.
const double PI = 3.14159; // Declare a constant double 'PI'. Its value cannot be changed.
Input/Output
Interacting with the user via the console.
std::cout: Standard output stream (console).std::cin: Standard input stream (keyboard).std::endl: Inserts a newline character and flushes the output buffer.std::getline(std::cin, var): Reads an entire line of input, including spaces.
#include <iostream>
#include <string> // For std::string and std::getline
int age;
std::string fullName;
std::cout << "Hello, World!" << std::endl; // Print "Hello, World!"
std::cout << "Enter your age: ";
std::cin >> age; // Read an integer
std::cout << "Enter your full name: ";
// std::cin >> std::ws skips any leading whitespace (like the newline left by previous std::cin)
std::getline(std::cin >> std::ws, fullName); // Read the full line of input into the 'name' string.
Operators
Symbols that perform operations on variables and values.
- **Arithmetic**:
+,-,*,/,%(modulo/remainder) - **Comparison**:
==(equal to),!=(not equal to),<(less than),>(greater than),<=(less than or equal to),>=(greater than or equal to) - **Logical**:
&&(AND),||(OR),!(NOT) - **Assignment**:
=(assign value),+=(add and assign),-=(subtract and assign), etc. - **Bitwise**:
&(AND),|(OR),^(XOR),~(NOT),<<(left shift),>>(right shift) - **Increment/Decrement**:
++(increase by 1),--(decrease by 1)
Control Flow
Statements that control the order in which instructions are executed.
If-Else
int age = 15;
if (age >= 18) {
std::cout << "You are an adult." << std::endl;
} else if (age >= 13) {
std::cout << "You are a teenager." << std::endl;
} else {
std::cout << "You are a child." << std::endl;
}
Switch
int day = 3;
switch (day) {
case 1:
std::cout << "Monday" << std::endl;
break;
case 2:
std::cout << "Tuesday" << std::endl;
break;
default:
std::cout << "Other day" << std::endl;
}
For Loop
for (int i = 0; i < 5; ++i) {
std::cout << "Iteration: " << i << std::endl;
}
Range-based For Loop
#include <vector>
std::vector<int> numbers = {10, 20, 30};
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
While Loop
int count = 0;
while (count < 3) {
std::cout << "Count: " << count << std::endl;
count++;
}
Do-While Loop
int j = 0;
do {
std::cout << "J: " << j << std::endl;
j++;
} while (j < 0); // Condition checked after first execution
Functions
Reusable blocks of code that perform a specific task.
Function Declaration (Prototype)
int add(int a, int b); // Takes two ints, returns an int
void greet(const std::string& name); // Takes a const reference to string, returns nothing
Function Definition
int add(int a, int b) {
return a + b;
}
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
Main Function
The entry point of every C++ program.
int main_example() { // Renamed main to avoid conflict in a single HTML file
int sum = add(5, 7); // Call 'add' function
std::cout << "Sum: " << sum << std::endl;
greet("Bob"); // Call 'greet' function
return 0; // Indicate successful program execution
}
Arrays
Fixed-size collections of elements of the same type.
int my_array[5] = {1, 2, 3, 4, 5}; // Declare and initialize an array of 5 integers
std::cout << "First element: " << my_array[0] << std::endl; // Access using 0-based index
my_array[2] = 99; // Change element value
Pointers
Variables that store the memory address of another variable.
int x = 10;
int* ptr = &x; // Declare pointer 'ptr' and store address of 'x'
std::cout << "Value of x: " << x << std::endl;
std::cout << "Address of x: " << &x << std::endl;
std::cout << "Value stored in ptr (address of x): " << ptr << std::endl;
std::cout << "Value pointed to by ptr: " << *ptr << std::endl; // Dereference ptr to get value
*ptr = 20; // Change value at address pointed to by ptr (changes x)
std::cout << "New value of x: " << x << std::endl;
References
Aliases (alternative names) for existing variables.
int y = 50;
int& ref = y; // Declare reference 'ref' as an alias for 'y'
std::cout << "Value of y: " << y << std::endl;
std::cout << "Value of ref: " << ref << std::endl;
ref = 60; // Changing 'ref' also changes 'y'
std::cout << "New value of y: " << y << std::endl;
Structures
User-defined data types that group variables of different types together.
struct Point {
int x;
int y;
};
Point p1; // Declare a variable 'p1' of type Point
p1.x = 10; // Access the 'x' member of p1
p1.y = 20; // Access the 'y' member of p1
std::cout << "Point coordinates: " << p1.x << ", " << p1.y << std::endl;
Classes (Object-Oriented Programming)
Blueprints for creating objects, encapsulating data (members) and functions (methods).
#include <string> // Required for std::string
#include <iostream> // Required for std::cout
class Dog {
public: // Members accessible from outside the class
std::string name;
int age;
// Constructor: A special function called when an object is created
Dog(std::string n, int a) : name(n), age(a) {} // Initializer list
// Member function: A function that belongs to the class
void bark() {
std::cout << name << " says Woof!" << std::endl;
}
private: // Members only accessible within the class
std::string breed;
};
// Example usage (typically in main or another function)
// Dog my_dog("Buddy", 3);
// my_dog.bark();
// std::cout << "Dog's name: " << my_dog.name << std::endl;
// my_dog.breed = "Golden Retriever"; // Error: 'breed' is private
Templates (Basic)
Allow writing functions or classes that work with different data types.
#include <iostream> // Required for std::cout
template <typename T> // Declare a template with type parameter 'T'
T maximum(T a, T b) { // Function takes two arguments of type T
return (a > b) ? a : b; // Ternary operator: return a if a > b; else return b
}
// Example usage (typically in main or another function)
// int maxInt = maximum(5, 10);
// double maxDouble = maximum(3.14, 2.71);
// std::cout << "Max int: " << maxInt << std::endl;
// std::cout << "Max double: " << maxDouble << std::endl;
Standard Template Library (STL) Basics
Fundamental containers for storing and managing data.
Vectors
#include <vector>
#include <algorithm> // For std::sort
#include <iostream> // For std::cout
std::vector<int> vec = {5, 1, 4, 2, 8};
std::sort(vec.begin(), vec.end()); // Sort the elements
std::cout << "Sorted vector: ";
for (int val : vec) {
std::cout << val << " ";
}
std::cout << std::endl;
Maps
#include <map>
#include <string> // For std::string
#include <iostream> // For std::cout
std::map<std::string, int> scores;
scores["Alice"] = 95; // Insert or update key-value pair
scores["Bob"] = 88;
std::cout << "Alice's score: " << scores["Alice"] << std::endl;
for (const auto& pair : scores) { // Iterate through the map
std::cout << pair.first << ": " << pair.second << std::endl;
}
Sets
#include <set>
#include <iostream> // For std::cout
std::set<int> unique_nums;
unique_nums.insert(5);
unique_nums.insert(1);
unique_nums.insert(5); // Inserting a duplicate has no effect
unique_nums.insert(1);
std::cout << "Unique numbers: ";
for (int num : unique_nums) {
std::cout << num << " ";
}
std::cout << std::endl;
File I/O
Reading from and writing to files.
#include <fstream>
#include <iostream>
#include <string>
// Writing to a file
void writeToFile() {
std::ofstream outfile("example.txt"); // Create an output file stream
if (outfile.is_open()) {
outfile << "This is line 1.\n";
outfile << "This is line 2.\n";
outfile.close();
} else {
std::cerr << "Error opening file for writing." << std::endl;
}
}
// Reading from a file
void readFromFile() {
std::ifstream infile("example.txt"); // Create an input file stream
std::string line;
if (infile.is_open()) {
while (std::getline(infile, line)) { // Read line by line
std::cout << "Read line: " << line << std::endl;
}
infile.close();
} else {
std::cerr << "Error opening file for reading." << std::endl;
}
}
// Example usage:
// writeToFile();
// readFromFile();
Exception Handling (Basic)
Dealing with errors that occur during program execution.
#include <stdexcept> // Required for std::runtime_error
#include <iostream>
double divide(double a, double b) {
if (b == 0) {
throw std::runtime_error("Division by zero!"); // Throw an exception
}
return a / b;
}
int main_exceptions() { // Renamed main to avoid conflict
try {
double result = divide(10, 2);
std::cout << "Result: " << result << std::endl;
result = divide(10, 0); // This will throw an exception
std::cout << "This line won't be reached." << std::endl;
} catch (const std::runtime_error& e) { // Catch a specific exception
std::cerr << "Caught exception: " << e.what() << std::endl;
} catch (...) { // Catch any other type of exception
std::cerr << "Caught an unknown exception." << std::endl;
}
return 0;
}
Preprocessor Directives
Instructions processed by the compiler before compilation.
#include <filename>: Includes the contents of another header file.#define MACRO_NAME value: Defines a macro.#ifdef MACRO_NAME/#endif: Checks if a macro is defined for conditional compilation.
#include <iostream>
#define MAX_SIZE 100
#ifdef DEBUG
std::cout << "Debug mode enabled." << std::endl;
#endif
// Usage of MAX_SIZE
// int arr[MAX_SIZE];
1. Smart Pointers
Smart pointers are objects that act like pointers but also manage the memory of the object they point to, ensuring proper deallocation and preventing memory leaks.
std::unique_ptr
- Exclusive ownership. Cannot be copied, but can be moved.
- Automatically deallocates memory when it goes out of scope.
- Use
std::make_uniquefor safe construction.
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
// std::unique_ptr<int> ptr2 = ptr1; // Error: cannot copy
std::unique_ptr<int> ptr3 = std::move(ptr1); // OK: moves ownership
std::shared_ptr
- Shared ownership. Multiple
shared_ptrs can point to the same object. - Uses a reference count; object is deallocated when the last
shared_ptrgoes out of scope. - Use
std::make_sharedfor safe and efficient construction.
std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
std::shared_ptr<int> ptr2 = ptr1; // OK: copies ownership, increments ref count
// ptr1.use_count() and ptr2.use_count() will both be 2
std::weak_ptr
- Non-owning "weak" reference to an object managed by
std::shared_ptr. - Does not increment the reference count.
- Used to break circular references between
shared_ptrs. - Must be converted to
std::shared_ptrvialock()before use.
std::shared_ptr<int> s_ptr = std::make_shared<int>(30);
std::weak_ptr<int> w_ptr = s_ptr;
if (std::shared_ptr<int> locked_s_ptr = w_ptr.lock()) {
// Use locked_s_ptr
std::cout << *locked_s_ptr << std::endl;
} else {
std::cout << "Object no longer exists." << std::endl;
}
2. Move Semantics
Move semantics allow resources (like dynamically allocated memory) to be transferred from one object to another efficiently, avoiding costly deep copies.
Rvalue References (&&)
- References to temporary objects (rvalues).
- Enable move constructors and move assignment operators.
void func(int& lvalue_ref) { /* ... */ } // Lvalue reference
void func(int&& rvalue_ref) { /* ... */ } // Rvalue reference
int x = 10;
func(x); // Calls lvalue_ref version
func(20); // Calls rvalue_ref version (20 is a temporary)
func(std::move(x)); // Calls rvalue_ref version (std::move casts x to rvalue)
std::move
- A function that casts its argument to an rvalue reference.
- Does not move anything itself; it only enables subsequent move operations.
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // v1 is now in a valid but unspecified state (likely empty)
Perfect Forwarding (std::forward)
- Used with universal references (template parameter
T&&) to preserve the value category (lvalue or rvalue) of an argument when forwarding it to another function.
template<typename T>
void wrapper(T&& arg) {
// std::forward<T>(arg) preserves the original value category of arg
some_other_func(std::forward<T>(arg));
}
3. Templates
Templates allow writing generic code that works with different data types.
Function Templates
template<typename T>
T add(T a, T b) {
return a + b;
}
// Usage: add(5, 10); add(5.5, 10.1);
Class Templates
template<typename T>
class MyContainer {
public:
T value;
MyContainer(T val) : value(val) {}
};
// Usage: MyContainer<int> int_cont(10); MyContainer<double> double_cont(10.5);
Variadic Templates
- Templates that can take a variable number of arguments.
- Uses parameter pack (
...) to represent a list of arguments.
template<typename T>
void print(T arg) {
std::cout << arg << std::endl;
}
template<typename T, typename... Args>
void print(T first_arg, Args... rest_of_args) {
std::cout << first_arg << ", ";
print(rest_of_args...); // Recursive call to expand the pack
}
// Usage: print(1, "hello", 3.14);
Template Metaprogramming (TMP)
- Performing computations at compile time using templates.
- Often involves recursion and template specialization.
- Example: Factorial calculation at compile time.
template<int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0> {
static const int value = 1;
};
// Usage: std::cout << Factorial<5>::value << std::endl; // Computes 120 at compile time
4. Lambda Expressions
Anonymous functions that can be defined inline and capture variables from their surrounding scope.
Syntax: [capture_list](parameters) -> return_type { body }
capture_list:[]: No variables captured.[var]: Capturevarby value.[&var]: Capturevarby reference.[=]: Capture all used variables by value.[&]: Capture all used variables by reference.[this]: Capturethispointer by value.[=, &var]: Capture all by value exceptvarby reference.[&, var]: Capture all by reference exceptvarby value.
return_type: Optional, can be deduced by the compiler.
Examples:
int x = 10;
auto add_x = [&](int a) { return a + x; }; // Captures x by reference
std::cout << add_x(5) << std::endl; // Output: 15
std::vector<int> nums = {1, 5, 2, 8, 3};
std::sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; }); // Custom sort
5. Concurrency (C++11 onwards)
C++ provides standard library support for multithreading.
std::thread
- Represents a single thread of execution.
void task() {
std::cout << "Task running in a thread." << std::endl;
}
std::thread t(task);
t.join(); // Wait for the thread to finish
// t.detach(); // Let the thread run independently
Mutexes (std::mutex)
- Used to protect shared data from race conditions.
lock()andunlock()for manual locking.std::lock_guardandstd::unique_lockfor RAII-style locking.
std::mutex mtx;
int shared_data = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx); // Locks mutex on construction, unlocks on destruction
shared_data++;
}
Condition Variables (std::condition_variable)
- Used for thread synchronization, allowing threads to wait until a certain condition is met.
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return data_ready; }); // Waits until data_ready is true
std::cout << "Data consumed." << std::endl;
}
void producer() {
std::lock_guard<std::mutex> lock(mtx);
data_ready = true;
cv.notify_one(); // Notifies one waiting thread
// cv.notify_all(); // Notifies all waiting threads
}
Futures (std::future, std::async)
std::asynclaunches an asynchronous task and returns astd::future.std::futurecan be used to retrieve the result of the asynchronous task.
int calculate_sum(int a, int b) {
return a + b;
}
std::future<int> fut = std::async(std::launch::async, calculate_sum, 10, 20);
int result = fut.get(); // Blocks until result is available
6. STL Advanced
Custom Allocators
- Provide custom memory management strategies for STL containers.
- Useful for performance optimization or specific memory requirements (e.g., fixed-size blocks).
Advanced Algorithms
std::for_each,std::transform,std::accumulate,std::partition,std::remove_if,std::unique, etc.- Often used with iterators and lambda expressions.
7. Exception Handling
RAII (Resource Acquisition Is Initialization)
- A C++ programming idiom that ties the lifetime of a resource to the lifetime of an object.
- Resources (memory, file handles, mutexes) are acquired in the constructor and released in the destructor.
- Guarantees resource release even if exceptions occur. Smart pointers are a prime example of RAII.
class FileHandler {
FILE* file;
public:
FileHandler(const char* filename, const char* mode) {
file = fopen(filename, mode);
if (!file) throw std::runtime_error("Failed to open file.");
}
~FileHandler() {
if (file) fclose(file);
}
// ... other file operations
};
noexcept
- Specifies that a function does not throw exceptions.
noexceptfunctions can be optimized more aggressively by the compiler.- If a
noexceptfunction does throw,std::terminateis called.
void func_no_throw() noexcept {
// This function promises not to throw.
}
8. C++11/14/17/20 Key Features (Brief)
- C++11:
auto,decltype, range-based for loops,nullptr,enum class,override,final,std::chrono,std::tuple,std::array,std::function. - C++14: Generic lambdas (
autoin lambda parameters),constexprfunctions improved, variable templates. - C++17: Structured bindings,
if constexpr,std::optional,std::variant,std::any,std::string_view, parallel STL algorithms. - C++20: Concepts, Modules, Coroutines, Ranges,
std::jthread,std::span.
9. Polymorphism and Virtual Functions
Virtual Functions
- Enabled by the
virtualkeyword in a base class function. - Allows dynamic dispatch (runtime polymorphism) where the actual function called depends on the type of the object pointed to, not the type of the pointer/reference.
- Requires a vtable (virtual table) lookup at runtime.
class Base {
public:
virtual void greet() { std::cout << "Hello from Base!" << std::endl; }
};
class Derived : public Base {
public:
void greet() override { std::cout << "Hello from Derived!" << std::endl; } // 'override' is good practice
};
Base* b = new Derived();
b->greet(); // Calls Derived::greet()
delete b;
Pure Virtual Functions
- A virtual function declared with
= 0. - Makes a class abstract; cannot be instantiated directly.
- Derived classes must implement pure virtual functions unless they are also abstract.
class Shape { // Abstract base class
public:
virtual double area() = 0; // Pure virtual function
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
double radius;
Circle(double r) : radius(r) {}
double area() override { return 3.14159 * radius * radius; }
};
CRTP (Curiously Recurring Template Pattern)
- A compile-time polymorphism technique.
- A class
BasetakesDerivedas a template argument, andDerivedinherits fromBase<Derived>. - Allows static dispatch (no vtable overhead) while achieving polymorphic-like behavior.
template<typename Derived>
class BaseCRTP {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class MyDerived : public BaseCRTP<MyDerived> {
public:
void implementation() {
std::cout << "MyDerived implementation." << std::endl;
}
};
// Usage: MyDerived obj; obj.interface();
10. Memory Management
Custom new and delete Operators
- Can be overloaded globally or for specific classes to customize memory allocation/deallocation.
- Global overload:
void* operator new(std::size_t size) - Class-specific overload:
void* ClassName::operator new(std::size_t size)
Placement New
- Constructs an object at a pre-allocated memory location.
- Does not allocate memory, only calls the constructor.
char buffer[sizeof(MyClass)]; // Assuming MyClass is defined
MyClass* obj = new (buffer) MyClass(); // Constructs MyClass in 'buffer'
// Don't use 'delete obj'; explicitly call destructor: obj->~MyClass();
Understanding and Avoiding Memory Leaks
A memory leak occurs when a program allocates memory dynamically but fails to deallocate it when it's no longer needed. This leads to a gradual consumption of available memory, potentially causing the program or even the entire system to slow down or crash.
How Memory Leaks Happen
- Forgetting to
deleteallocated memory: The most common cause. Ifnewis used to allocate memory, a correspondingdeletemust be called to free it.// Example of a memory leak void leakyFunction() { int* data = new int[100]; // Memory allocated // ... use data ... // No 'delete[] data;' here, so memory is leaked when function exits } - Losing pointer to allocated memory: If the only pointer to dynamically allocated memory goes out of scope, is reassigned, or is otherwise lost before
deleteis called, the memory becomes unreachable and cannot be freed.void anotherLeakyFunction() { int* p = new int(5); // Memory allocated p = new int(10); // 'p' now points to new memory; the old memory (where 5 was) is leaked delete p; // Only the memory for 10 is freed } - Exceptions: If an exception occurs between a
newand its correspondingdelete, and there's no proper exception handling (like RAII), thedeletemight be skipped, leading to a leak.void leakyFunctionWithException() { int* resource = new int[100]; // Allocate resource // ... some operations ... if (true) { // Simulate an error/exception condition throw std::runtime_error("Something went wrong!"); } delete[] resource; // This line is never reached if an exception is thrown }
How to Avoid Memory Leaks
The primary way to avoid memory leaks in modern C++ is to embrace RAII (Resource Acquisition Is Initialization) and use smart pointers.
- Use Smart Pointers (
std::unique_ptr,std::shared_ptr): This is the most effective and recommended approach. Smart pointers automatically manage memory deallocation.std::unique_ptrfor exclusive ownership: When a resource has a single, clear owner.// Avoiding leak with std::unique_ptr void safeFunctionUnique() { std::unique_ptr<int[]> data = std::make_unique<int[]>(100); // Memory allocated and managed // ... use data ... // No explicit delete needed; memory is freed automatically when 'data' goes out of scope }std::shared_ptrfor shared ownership: When multiple parts of your code need to share ownership of a resource.// Assuming MyObject is defined // Avoiding leak with std::shared_ptr void safeFunctionShared() { std::shared_ptr<MyObject> obj = std::make_shared<MyObject>(); // Memory allocated and managed // ... pass obj around ... // Memory is freed when the last shared_ptr goes out of scope }
- RAII (Resource Acquisition Is Initialization): This principle ensures that resources are acquired in a constructor and released in a destructor. Smart pointers are the prime example, but you can also apply it to other resources like file handles, mutexes, etc. (as shown in the
FileHandlerexample in the "Exception Handling" section). - Pair
newwithdeleteandnew[]withdelete[]: If you must use raw pointers and manual memory management (e.g., in low-level code or when interfacing with C libraries), always remember to pairnewwithdeleteandnew[]withdelete[]. This is error-prone and should be avoided in favor of smart pointers whenever possible.// Manual memory management (use with extreme caution) void manualMemoryManagement() { int* singleInt = new int(42); // ... delete singleInt; // Correctly frees single int int* intArray = new int[50]; // ... delete[] intArray; // Correctly frees array }
By consistently using smart pointers and adhering to the RAII principle, you can significantly reduce the risk of memory leaks in your C++ applications.
11. Inbuilt Data Structures
The C++ Standard Library provides a rich set of container data structures. Understanding their characteristics is crucial for efficient programming.
std::vector
- Description: A dynamic array that can resize itself. Elements are stored contiguously in memory.
#include <vector>
#include <iostream>
std::vector<int> vec = {10, 20, 30};
vec.push_back(40); // Add element
std::cout << vec[0] << std::endl; // Access element- Performance: Access (by index): $O(1)$, Insertion/Deletion (end): $O(1)$ amortized, Insertion/Deletion (middle/beginning): $O(N)$
- Thread Safety: Not thread-safe for concurrent modifications. Requires external synchronization.
std::list
- Description: A doubly-linked list. Elements are not stored contiguously, allowing efficient insertion and deletion anywhere.
#include <list>
#include <iostream>
std::list<int> lst = {10, 20, 30};
lst.push_back(40);
lst.push_front(5);
lst.remove(20); // Remove by value
for (int x : lst) {
std::cout << x << " ";
}
std::cout << std::endl;- Performance: Access (by index): $O(N)$, Insertion/Deletion (anywhere, with iterator): $O(1)$, Insertion/Deletion (without iterator, by value): $O(N)$
- Thread Safety: Not thread-safe for concurrent modifications. Requires external synchronization.
std::deque (Double-Ended Queue)
- Description: A double-ended queue. Similar to
std::vectorbut optimized for insertions and deletions at both ends. Elements are not necessarily contiguous.
#include <deque>
#include <iostream>
std::deque<int> dq = {10, 20, 30};
dq.push_front(5);
dq.push_back(40);
std::cout << dq.front() << ", " << dq.back() << std::endl;- Performance: Access (by index): $O(1)$, Insertion/Deletion (beginning/end): $O(1)$ amortized, Insertion/Deletion (middle): $O(N)$
- Thread Safety: Not thread-safe for concurrent modifications. Requires external synchronization.
std::set
- Description: An associative container that stores unique elements in a sorted order. Implemented as a self-balancing binary search tree (typically a Red-Black Tree).
#include <set>
#include <iostream>
std::set<int> s = {30, 10, 20};
s.insert(40);
s.insert(10); // Duplicate, ignored
for (int x : s) {
std::cout << x << " "; // Output: 10 20 30 40
}
std::cout << std::endl;- Performance: Insertion/Deletion/Search: $O(\log N)$
- Thread Safety: Not thread-safe for concurrent modifications. Requires external synchronization.
std::map
- Description: An associative container that stores key-value pairs in a sorted order based on keys. Keys must be unique. Implemented as a self-balancing binary search tree (typically a Red-Black Tree).
#include <map>
#include <iostream>
std::map<std::string, int> ages;
ages["Alice"] = 30;
ages["Bob"] = 25;
std::cout << "Alice's age: " << ages["Alice"] << std::endl;
ages["Bob"] = 26; // Update value- Performance: Insertion/Deletion/Search: $O(\log N)$
- Thread Safety: Not thread-safe for concurrent modifications. Requires external synchronization.
std::unordered_set
- Description: An associative container that stores unique elements in no particular order. Elements are organized into hash buckets. Provides average constant-time complexity.
#include <unordered_set>
#include <iostream>
std::unordered_set<int> us = {30, 10, 20};
us.insert(40);
us.insert(10); // Duplicate, ignored
if (us.count(20)) {
std::cout << "20 is in the set." << std::endl;
}- Performance: Insertion/Deletion/Search: $O(1)$ on average, $O(N)$ worst case (due to hash collisions)
- Thread Safety: Not thread-safe for concurrent modifications. Requires external synchronization.
std::unordered_map
- Description: An associative container that stores key-value pairs in no particular order. Keys must be unique. Elements are organized into hash buckets. Provides average constant-time complexity.
#include <unordered_map>
#include <iostream>
std::unordered_map<std::string, int> scores;
scores["John"] = 95;
scores["Jane"] = 88;
std::cout << "John's score: " << scores["John"] << std::endl;- Performance: Insertion/Deletion/Search: $O(1)$ on average, $O(N)$ worst case (due to hash collisions)
- Thread Safety: Not thread-safe for concurrent modifications. Requires external synchronization.
std::queue
- Description: A container adaptor that provides a FIFO (First-In, First-Out) data structure. By default, it's implemented using
std::deque.
#include <queue>
#include <iostream>
std::queue<int> q;
q.push(10);
q.push(20);
std::cout << q.front() << std::endl; // Output: 10
q.pop();
std::cout << q.front() << std::endl; // Output: 20- Performance: Push/Pop/Front/Back: $O(1)$
- Thread Safety: Not inherently thread-safe. Requires external synchronization.
std::stack
- Description: A container adaptor that provides a LIFO (Last-In, First-Out) data structure. By default, it's implemented using
std::deque.
#include <stack>
#include <iostream>
std::stack<int> s;
s.push(10);
s.push(20);
std::cout << s.top() << std::endl; // Output: 20
s.pop();
std::cout << s.top() << std::endl; // Output: 10- Performance: Push/Pop/Top: $O(1)$
- Thread Safety: Not inherently thread-safe. Requires external synchronization.
std::priority_queue
- Description: A container adaptor that provides a max-heap (by default). Elements are retrieved in order of priority (largest element first). Implemented using
std::vectorand heap algorithms.
#include <queue> // For std::priority_queue
#include <iostream>
std::priority_queue<int> pq;
pq.push(30);
pq.push(10);
pq.push(50);
std::cout << pq.top() << std::endl; // Output: 50
pq.pop();
std::cout << pq.top() << std::endl; // Output: 30- Performance: Push/Pop: $O(\log N)$, Top: $O(1)$
- Thread Safety: Not inherently thread-safe. Requires external synchronization.
Important Note on Thread Safety for STL Containers:
None of the standard C++ containers are inherently thread-safe for concurrent modifications. If multiple threads access and at least one modifies the container, you must use external synchronization mechanisms (like std::mutex) to protect the container and prevent race conditions. Concurrent read-only access by multiple threads is generally safe.
Comparison of Inbuilt Data Structures
| Data Structure | Description | Access (Index) | Insertion (Avg) | Deletion (Avg) | Search (Avg) | Thread Safety (Concurrent Mod.) |
|---|---|---|---|---|---|---|
std::vector | Dynamic array, contiguous memory | $O(1)$ | $O(1)$ amortized (end), $O(N)$ (middle) | $O(N)$ | $O(N)$ | No (requires external sync) |
std::list | Doubly-linked list, non-contiguous | $O(N)$ | $O(1)$ (with iterator) | $O(1)$ (with iterator) | $O(N)$ | No (requires external sync) |
std::deque | Double-ended queue, non-contiguous blocks | $O(1)$ | $O(1)$ amortized (ends), $O(N)$ (middle) | $O(1)$ amortized (ends), $O(N)$ (middle) | $O(N)$ | No (requires external sync) |
std::set | Unique sorted elements (Red-Black Tree) | N/A | $O(\log N)$ | $O(\log N)$ | $O(\log N)$ | No (requires external sync) |
std::map | Sorted key-value pairs (Red-Black Tree) | $O(\log N)$ (key access) | $O(\log N)$ | $O(\log N)$ | $O(\log N)$ | No (requires external sync) |
std::unordered_set | Unique unsorted elements (Hash Table) | N/A | $O(1)$ (avg), $O(N)$ (worst) | $O(1)$ (avg), $O(N)$ (worst) | $O(1)$ (avg), $O(N)$ (worst) | No (requires external sync) |
std::unordered_map | Unsorted key-value pairs (Hash Table) | $O(1)$ (avg, key access), $O(N)$ (worst) | $O(1)$ (avg), $O(N)$ (worst) | $O(1)$ (avg), $O(N)$ (worst) | $O(1)$ (avg), $O(N)$ (worst) | No (requires external sync) |
std::queue | FIFO adaptor (typically std::deque) | N/A (front/back $O(1)$) | $O(1)$ | $O(1)$ | N/A | No (requires external sync) |
std::stack | LIFO adaptor (typically std::deque) | N/A (top $O(1)$) | $O(1)$ | $O(1)$ | N/A | No (requires external sync) |
std::priority_queue | Max-heap adaptor (typically std::vector) | N/A (top $O(1)$) | $O(\log N)$ | $O(\log N)$ | N/A | No (requires external sync) |
12. Design Patterns
Advanced C++ often involves implementing various design patterns. Each pattern below is a collapsible section.
Singleton ▾
Ensures a class has only one instance and provides a global point of access to it.
class Singleton {
private:
Singleton() { /* Private constructor */ }
// Prevent copy and assignment
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // Guaranteed to be initialized once
return instance;
}
void showMessage() {
std::cout << "Hello from Singleton!" << std::endl;
}
};
// Usage: Singleton::getInstance().showMessage();
Factory Method / Abstract Factory ▾
Provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
// Product Interface
class Product {
public:
virtual ~Product() = default;
virtual std::string getName() const = 0;
};
// Concrete Products
class ConcreteProductA : public Product {
public:
std::string getName() const override { return "Product A"; }
};
// Concrete Product
class ConcreteProductB : public Product {
public:
std::string getName() const override { return "Product B"; }
};
// Creator Interface (Factory)
class Creator {
public:
virtual ~Creator() = default;
virtual Product* createProduct() const = 0;
std::string someOperation() const {
Product* product = this->createProduct();
std::string result = "Creator: The product is " + product->getName();
delete product;
return result;
}
};
// Concrete Creators
class ConcreteCreatorA : public Creator {
public:
Product* createProduct() const override { return new ConcreteProductA(); }
};
class ConcreteCreatorB : public Creator {
public:
Product* createProduct() const override { return new ConcreteProductB(); }
};
/*
// Usage:
Creator* creatorA = new ConcreteCreatorA();
std::cout << creatorA->someOperation() << std::endl; // Output: Creator: The product is Product A
delete creatorA;
*/
Observer ▾
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
#include <vector>
#include <list>
#include <algorithm>
#include <string> // Required for std::string
#include <iostream> // Required for std::cout
// Observer Interface
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& message) = 0;
};
// Subject (Observable)
class Subject {
private:
std::list<Observer*> observers;
public:
void attach(Observer* observer) {
observers.push_back(observer);
}
void detach(Observer* observer) {
observers.remove(observer);
}
void notify(const std::string& message) {
for (Observer* observer : observers) {
observer->update(message);
}
}
};
// Concrete Observer
class ConcreteObserver : public Observer {
private:
std::string name;
public:
ConcreteObserver(const std::string& n) : name(n) {}
void update(const std::string& message) override {
std::cout << name << " received update: " << message << std::endl;
}
};
/*
// Usage:
Subject subject;
ConcreteObserver obs1("Observer 1");
ConcreteObserver obs2("Observer 2");
subject.attach(&obs1);
subject.attach(&obs2);
subject.notify("A new event occurred!"); // Both observers get notified
subject.detach(&obs1);
subject.notify("Another event!"); // Only Observer 2 gets notified
*/
Strategy ▾
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
// Strategy Interface
class PaymentStrategy {
public:
virtual ~PaymentStrategy() = default;
virtual void pay(int amount) const = 0;
};
// Concrete Strategies
class CreditCardPayment : public PaymentStrategy {
public:
void pay(int amount) const override {
std::cout << "Paying " << amount << " using Credit Card." << std::endl;
}
};
// Concrete Strategy
class PayPalPayment : public PaymentStrategy {
public:
void pay(int amount) const override {
std::cout << "Paying " << amount << " using PayPal." << std::endl;
}
};
// Context
class ShoppingCart {
private:
PaymentStrategy* paymentStrategy;
public:
ShoppingCart(PaymentStrategy* strategy) : paymentStrategy(strategy) {}
~ShoppingCart() { delete paymentStrategy; } // Ownership assumed
void setPaymentStrategy(PaymentStrategy* strategy) {
delete paymentStrategy; // Clean up old strategy
paymentStrategy = strategy;
}
void checkout(int amount) const {
if(paymentStrategy) paymentStrategy->pay(amount);
}
};
/*
// Usage:
ShoppingCart cart(new CreditCardPayment());
cart.checkout(100); // Output: Paying 100 using Credit Card.
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(50); // Output: Paying 50 using PayPal.
*/
Decorator ▾
Attaches additional responsibilities to an object dynamically.
// Component Interface
class Coffee {
public:
virtual ~Coffee() = default;
virtual double getCost() const = 0;
virtual std::string getIngredients() const = 0;
};
// Concrete Component
class SimpleCoffee : public Coffee {
public:
double getCost() const override { return 5.0; }
std::string getIngredients() const override { return "Coffee"; }
};
// Decorator Base Class
class CoffeeDecorator : public Coffee {
protected:
Coffee* decoratedCoffee; // Changed from Coffee* to std::unique_ptr for RAII
public:
CoffeeDecorator(Coffee* coffee) : decoratedCoffee(coffee) {}
// Destructor now handles decoratedCoffee if it was owned, or unique_ptr handles it.
// If decoratedCoffee is not owned (e.g. passed as raw ptr and managed elsewhere),
// then the destructor might not need to delete it.
// For simplicity here, assuming CoffeeDecorator takes ownership if a raw pointer is passed.
// Better: pass std::unique_ptr to constructor.
~CoffeeDecorator() { delete decoratedCoffee; }
double getCost() const override { return decoratedCoffee->getCost(); }
std::string getIngredients() const override { return decoratedCoffee->getIngredients(); }
};
// Concrete Decorators
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
double getCost() const override { return decoratedCoffee->getCost() + 1.5; }
std::string getIngredients() const override {
return decoratedCoffee->getIngredients() + ", Milk";
}
};
class SugarDecorator : public CoffeeDecorator {
public:
SugarDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
double getCost() const override { return decoratedCoffee->getCost() + 0.5; }
std::string getIngredients() const override {
return decoratedCoffee->getIngredients() + ", Sugar";
}
};
/*
// Usage:
Coffee* myCoffee = new SimpleCoffee();
std::cout << "Cost: " << myCoffee->getCost() << ", Ingredients: " << myCoffee->getIngredients() << std::endl;
myCoffee = new MilkDecorator(myCoffee); // Add milk
std::cout << "Cost: " << myCoffee->getCost() << ", Ingredients: " << myCoffee->getIngredients() << std::endl;
myCoffee = new SugarDecorator(myCoffee); // Add sugar
std::cout << "Cost: " << myCoffee->getCost() << ", Ingredients: " << myCoffee->getIngredients() << std::endl;
delete myCoffee; // Clean up all decorators and the base coffee
*/
Visitor ▾
Represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
// Forward declarations
class ConcreteElementA;
class ConcreteElementB;
// Visitor Interface
class Visitor {
public:
virtual ~Visitor() = default;
virtual void visit(ConcreteElementA* element) = 0;
virtual void visit(ConcreteElementB* element) = 0;
};
// Element Interface
class Element {
public:
virtual ~Element() = default;
virtual void accept(Visitor* visitor) = 0;
};
// Concrete Elements
class ConcreteElementA : public Element {
public:
void accept(Visitor* visitor) override {
visitor->visit(this);
}
std::string operationA() const { return "ConcreteElementA"; }
};
class ConcreteElementB : public Element {
public:
void accept(Visitor* visitor) override {
visitor->visit(this);
}
std::string operationB() const { return "ConcreteElementB"; }
};
// Concrete Visitor
class ConcreteVisitor1 : public Visitor {
public:
void visit(ConcreteElementA* element) override {
std::cout << "Visitor 1 processing " << element->operationA() << std::endl;
}
void visit(ConcreteElementB* element) override {
std::cout << "Visitor 1 processing " << element->operationB() << std::endl;
}
};
// Concrete Visitor
class ConcreteVisitor2 : public Visitor {
public:
void visit(ConcreteElementA* element) override {
std::cout << "Visitor 2 processing " << element->operationA() << " differently." << std::endl;
}
void visit(ConcreteElementB* element) override {
std::cout << "Visitor 2 processing " << element->operationB() << " differently." << std::endl;
}
};
/*
// Usage:
std::vector<Element*> elements = {new ConcreteElementA(), new ConcreteElementB()};
ConcreteVisitor1 visitor1;
for (Element* elem : elements) {
elem->accept(&visitor1);
}
ConcreteVisitor2 visitor2;
for (Element* elem : elements) {
elem->accept(&visitor2);
}
for (Element* elem : elements) {
delete elem;
}
*/