Modern C++ Handbooks - Advanced Modern C++ Techniques
Modern C++ Handbooks - Advanced Modern C++ Techniques
January 2025
Contents
Contents 2
2
3
1.2.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.3 Type Traits and std::enable if . . . . . . . . . . . . . . . . . . . . . . . 38
1.3.1 Introduction to Type Traits and std::enable if . . . . . . . . . . 38
1.3.2 Type Traits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.3.3 std::enable if . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
1.3.4 Practical Applications . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.3.5 Advanced Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.3.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
1.4 Concepts and Constraints (C++20) . . . . . . . . . . . . . . . . . . . . . . . . 50
1.4.1 Introduction to Concepts and Constraints . . . . . . . . . . . . . . . . 50
1.4.2 What Are Concepts? . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
1.4.3 What Are Constraints? . . . . . . . . . . . . . . . . . . . . . . . . . . 52
1.4.4 Standard Concepts (C++20) . . . . . . . . . . . . . . . . . . . . . . . 53
1.4.5 Combining Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
1.4.6 Practical Applications . . . . . . . . . . . . . . . . . . . . . . . . . . 55
1.4.7 Advanced Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . 58
1.4.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.2.2 std::mutex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
2.2.3 std::lock guard and std::unique lock . . . . . . . . . . . 78
2.2.4 std::atomic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
2.2.5 Practical Applications . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.2.6 Advanced Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . 86
2.2.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
2.3 Coroutines (C++20) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.3.1 Introduction to Coroutines in C++ . . . . . . . . . . . . . . . . . . . . 90
2.3.2 What Are Coroutines? . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.3.3 Coroutine Components . . . . . . . . . . . . . . . . . . . . . . . . . . 92
2.3.4 Writing Coroutines . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
2.3.5 Practical Applications . . . . . . . . . . . . . . . . . . . . . . . . . . 95
2.3.6 Advanced Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . 100
2.3.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Appendices 193
Appendix A: Advanced C++ Features and Techniques . . . . . . . . . . . . . . . 193
Appendix B: Concurrency and Parallelism . . . . . . . . . . . . . . . . . . . . . . 196
Appendix C: Performance Optimization Techniques . . . . . . . . . . . . . . . . 198
Appendix D: Advanced STL and Custom Containers . . . . . . . . . . . . . . . . 199
Appendix E: Cross-Platform Development . . . . . . . . . . . . . . . . . . . . . . 201
Appendix F: Case Studies and Real-World Examples . . . . . . . . . . . . . . . . 202
Appendix G: Further Reading and Resources . . . . . . . . . . . . . . . . . . . . 203
References 204
Modern C++ Handbooks
• Content:
– Introduction to C++:
7
8
– Control Flow:
– Functions:
– Practical Examples:
• Content:
– C++11 Features:
* Structured bindings.
* if and switch with initializers.
* inline variables.
* Fold expressions.
– C++20 Features:
* Ranges library.
* Coroutines.
* Three-way comparison (<=> operator).
– C++23 Features:
• Content:
• Content:
– Containers:
– Algorithms:
* Iterator categories.
* Ranges library (C++20).
– Practical Examples:
* Custom allocators.
* Performance benchmarks.
• Content:
13
* Lock-free programming.
* Custom memory management.
• Content:
– Code Quality:
– Performance Optimization:
– Design Principles:
– Security:
– Practical Examples:
– Deployment (CI/CD):
• Content:
– Scientific Computing:
* Real-time programming.
* Low-level hardware interaction.
– Practical Examples:
* Domain-specific optimizations.
• Content:
16
• Content:
– Template Metaprogramming:
* Custom allocators.
* Memory pools and arenas.
* Garbage collection techniques.
17
– Performance Tuning:
* Cache optimization.
* SIMD (Single Instruction, Multiple Data) programming.
* Profiling and benchmarking tools.
– Advanced Libraries:
• Content:
– Case Studies:
19
20
templates. These concepts are critical for advanced learners and professionals who want to
harness the full power of C++.
A function template is a blueprint for creating functions that can operate on any data type.
Instead of writing multiple overloaded functions for different types (e.g., int, double,
std::string), you can write a single function template that works for all of them. The
compiler generates the appropriate function for each type at compile time.
2. Syntax
The syntax for a function template involves the template keyword, followed by a list of
template parameters enclosed in angle brackets (<>). Each parameter is prefixed with
typename (or class, which is interchangeable in this context).
• T add(T a, T b): This is the function template. The type T can be any data
type, such as int, double, or even user-defined types.
3. Example Usage
21
int main() {
int i = add(3, 4); // T is int
double d = add(3.5, 4.5); // T is double
std::string s = add(std::string("Hello, "),
,→ std::string("World!")); // T is std::string
}
• When add(3.5, 4.5) is called, the compiler generates a version of add where
T is double.
4. Key Points
(a) Code Reusability: A single function template can handle multiple types, reducing
code duplication.
(b) Type Safety: Templates ensure type safety at compile time. For example, you
cannot call add(3, 4.5) because T cannot be both int and double.
(c) Performance: Since templates are resolved at compile time, there is no runtime
overhead compared to using function pointers or virtual functions.
Function templates can be overloaded, just like regular functions. This allows you to
provide specialized implementations for specific types or scenarios.
22
The compiler can automatically deduce the template arguments based on the function
arguments. For example:
A class template is a blueprint for creating classes that can operate on any data type. Like
function templates, class templates allow you to define a class once and use it with
23
multiple types. This is particularly useful for creating generic containers, such as vectors,
stacks, or queues.
2. Syntax
The syntax for a class template is similar to that of a function template. You use the
template keyword followed by a list of template parameters.
3. Example Usage
int main() {
Box<int> intBox(123);
Box<double> doubleBox(456.78);
Box<std::string> stringBox("Hello, World!");
4. Key Points
(a) Generic Containers: Class templates are commonly used to create generic
containers like std::vector, std::list, and std::map.
(b) Multiple Template Parameters: Class templates can have multiple template
parameters.
(a) Default Template Arguments: You can provide default values for template
parameters.
25
template <>
std::string add<std::string>(std::string a, std::string b) {
return a + " " + b; // Custom behavior for strings
}
template <>
class Box<char> {
private:
char value;
public:
Box(char v) : value(v) {}
char getValue() const { return value; }
26
void print() const { std::cout << "Char Box: " << value <<
,→ std::endl; }
};
4. Partial Specialization
Partial specialization allows you to specialize only some of the template parameters.
2. Example
3. Usage
int main() {
Array<int, 10> intArray;
intArray[0] = 123;
std::cout << intArray[0] << std::endl;
}
2. Example
3. Usage
28
int main() {
print(1, 2, 3, "Hello", 4.5); // Output: 1 2 3 Hello 4.5
}
1.1.7 Conclusion
Function and class templates are indispensable tools in modern C++ programming. They enable
you to write generic, reusable, and type-safe code, reducing redundancy and improving
maintainability. By mastering templates, you can unlock the full potential of C++ and create
powerful, flexible, and efficient software systems.
This section has covered the fundamentals of templates, including function and class templates,
template specialization, non-type template parameters, and variadic templates. These concepts
lay the groundwork for more advanced topics, such as template metaprogramming, which will
be explored in later sections of the book.
29
2. Syntax
The syntax for a variadic template involves the typename... or class... keyword,
which declares a template parameter pack. The parameter pack can then be expanded to
represent multiple arguments.
30
#include <iostream>
int main() {
print(1, 2, 3, "Hello", 4.5); // Output: 1 2 3 Hello 4.5
}
• The print function can accept any number of arguments of any type.
• The fold expression (std::cout << ... << args) expands the parameter
pack args and applies the << operator to each argument.
• The expression (std::cout << ... << args) is a fold expression that
expands the parameter pack args.
int main() {
printSize(1, 2, 3); // Output: 3
printSize("Hello", 4.5); // Output: 2
}
32
2. Syntax
There are four types of fold expressions:
int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // Output: 15
}
33
int main() {
std::cout << sumWithInit(10, 1, 2, 3) << std::endl; // Output: 16
}
#include <iostream>
int main() {
print(1, 2, 3, "Hello", 4.5);
}
• The print function processes the first argument and then calls itself with the
remaining arguments.
• The recursion stops when the parameter pack is empty, and the base case void
print() is called.
public:
Tuple() {}
};
int main() {
Tuple<int, double, std::string> t(1, 2.5, "Hello");
}
• The Tuple class template recursively stores each value and the remaining
arguments in a nested structure.
int main() {
forwardToPrint(1, 2, 3, "Hello", 4.5);
}
Variadic templates are used to create heterogeneous data structures like std::tuple
and std::variant. These data structures can store multiple values of different types.
#include <tuple>
#include <string>
int main() {
std::tuple<int, double, std::string> t(1, 2.5, "Hello");
std::cout << std::get<0>(t) << std::endl; // Output: 1
std::cout << std::get<2>(t) << std::endl; // Output: Hello
}
• std::tuple is a variadic class template that can store multiple values of different
types.
Variadic templates enable the creation of type-safe variadic functions, which can accept
any number of arguments of any type while ensuring type safety at compile time.
int main() {
log("Error code: ", 404, ", Message: ", "Not Found");
}
37
• The log function can accept any number of arguments of any type and print them in
a type-safe manner.
4. Practical Applications
(a) Logging and Debugging: Variadic templates can be used to create flexible logging
functions that accept any number of arguments.
(b) Generic Containers: Variadic templates are used to implement containers like
std::tuple, std::any, and std::variant.
(c) Factory Functions: Variadic templates can be used to create factory functions that
construct objects with arbitrary constructor arguments.
(d) Metaprogramming: Variadic templates are essential for advanced
metaprogramming techniques, such as type lists, compile-time computations, and
template metaprogramming.
1.2.7 Conclusion
Variadic templates are a cornerstone of modern C++ programming, enabling the creation of
highly flexible and reusable code. By mastering variadic templates, you can implement
advanced features like fold expressions, recursive template instantiation, perfect forwarding, and
type-safe variadic functions. These techniques are essential for developing robust and efficient
software systems.
This section has provided a comprehensive overview of variadic templates, covering their syntax,
usage, and advanced techniques. With this knowledge, you are well-equipped to tackle complex
programming challenges and leverage the full power of modern C++. Variadic templates are not
just a feature; they are a paradigm shift that opens up new possibilities for generic programming
and metaprogramming in C++.
38
Type traits and std::enable if are foundational tools in modern C++ template
metaprogramming. They allow developers to inspect, manipulate, and conditionally enable or
disable code based on the properties of types at compile time. These tools are essential for
writing generic, reusable, and type-safe code, and they are widely used in the Standard Template
Library (STL) and other advanced C++ libraries.
This section will provide an in-depth exploration of type traits and std::enable if,
covering their syntax, usage, and practical applications. We will also delve into advanced
techniques such as SFINAE (Substitution Failure Is Not An Error), conditional function
overloading, and template specialization. These concepts are critical for advanced learners and
professionals who want to master template metaprogramming and write high-quality,
maintainable C++ code.
1. Definition
Type traits are a set of templates defined in the <type traits> header that allow you
to inspect and manipulate the properties of types at compile time. They are used to
perform type introspection (querying properties of types) and type transformations
(modifying types). Type traits are a cornerstone of template metaprogramming, enabling
compile-time logic based on type properties.
(a) Primary Type Categories: Traits that check if a type belongs to a specific category
(e.g., std::is integral, std::is floating point).
(b) Composite Type Categories: Traits that check if a type belongs to a composite
category (e.g., std::is arithmetic, std::is compound).
(c) Type Properties: Traits that check specific properties of a type (e.g.,
std::is const, std::is pointer).
(d) Type Transformations: Traits that transform one type into another (e.g.,
std::remove const, std::add pointer).
(e) Type Relationships: Traits that check relationships between types (e.g.,
std::is same, std::is base of).
#include <iostream>
#include <type_traits>
int main() {
40
1.3.3 std::enable if
1. Definition
std::enable if is a template defined in the <type traits> header that
conditionally enables or disables a template instantiation based on a compile-time boolean
expression. It is commonly used to enforce constraints on template parameters or to
provide different implementations for different types.
2. Syntax
#include <iostream>
#include <type_traits>
int main() {
print(42); // Output: Integral value: 42
print(3.14); // Output: Floating-point value: 3.14
43
#include <iostream>
#include <type_traits>
int main() {
print(42); // Output: Integral value: 42
44
#include <iostream>
#include <type_traits>
int main() {
process(42); // Output: Processing integral value: 42
process(3.14); // Output: Processing floating-point value: 3.14
45
#include <iostream>
#include <type_traits>
int main() {
int x = 42;
process(&x); // Output: Processing pointer: 42
process(x); // Output: Processing reference: 42
}
3. Template Specialization
std::enable if can be used to specialize templates based on type properties.
46
#include <iostream>
#include <type_traits>
int main() {
MyStruct<double> s1;
s1.print(); // Output: Generic type
MyStruct<int> s2;
s2.print(); // Output: Integral type
}
4. Type-Safe Interfaces
std::enable if can be used to create type-safe interfaces that enforce constraints on
template parameters.
#include <iostream>
#include <type_traits>
int main() {
Container<int> c1(42); // OK
Container<double> c2(3.14); // OK
// Container<std::string> c3("Hello"); // Error: Static assertion
,→ failed
}
You can combine multiple type traits with std::enable if to create complex
compile-time conditions.
#include <iostream>
#include <type_traits>
int main() {
process(42); // Output: Processing arithmetic non-pointer
,→ value: 42
process(3.14); // Output: Processing arithmetic non-pointer
,→ value: 3.14
// int* ptr = nullptr;
// process(ptr); // Error: No matching function
}
#include <iostream>
#include <type_traits>
template <>
struct is_custom_type<int> : std::true_type {};
template <>
struct is_custom_type<double> : std::true_type {};
int main() {
process(42); // Output: Processing custom type: 42
process(3.14); // Output: Processing custom type: 3.14
// process("Hello"); // Error: No matching function
}
1.3.6 Conclusion
Type traits and std::enable if are indispensable tools in modern C++ template
metaprogramming. They enable you to write more generic, flexible, and type-safe code by
inspecting and manipulating types at compile time. By mastering these concepts, you can
implement advanced features like SFINAE, conditional function overloading, and template
specialization.
This section has provided a comprehensive overview of type traits and std::enable if,
covering their syntax, usage, and practical applications. With this knowledge, you are
well-equipped to tackle complex programming challenges and leverage the full power of modern
C++. These techniques are essential for developing robust, efficient, and maintainable software
systems.
50
2. Syntax
A concept is defined using the concept keyword, followed by the concept name and a
boolean expression that specifies the requirements.
51
#include <iostream>
#include <concepts>
int main() {
std::cout << add(3, 4) << std::endl; // Output: 7
std::cout << add(3.5, 4.5) << std::endl; // Output: 8.0
// std::cout << add("Hello", "World") << std::endl; // Error:
,→ "Hello" does not satisfy Addable
}
52
• The Addable concept requires that the + operator is defined for T and that it
returns a value of type T.
• The add function is constrained to types that satisfy the Addable concept.
2. Syntax
Constraints can be applied to template parameters using the requires keyword or
directly in the template parameter list.
#include <iostream>
#include <concepts>
53
int main() {
print(42); // Output: Integral value: 42
// print(3.14); // Error: 3.14 does not satisfy Integral
}
Concept Description
Concept Description
#include <iostream>
#include <concepts>
int main() {
process(42); // Output: Processing integral value: 42
process(3.14); // Output: Processing floating-point value: 3.14
// process("Hello"); // Error: No matching function
}
55
#include <iostream>
#include <concepts>
int main() {
print(42); // Output: Arithmetic value: 42
print(3.14); // Output: Arithmetic value: 3.14
// print("Hello"); // Error: No matching function
}
#include <iostream>
#include <concepts>
int main() {
print(42); // Output: 42
print("Hello"); // Output: Hello
// print(std::vector<int>{1, 2, 3}); // Error: No matching
,→ function
}
2. Algorithm Constraints
Concepts can be used to constrain algorithms to work only with types that meet specific
requirements.
#include <iostream>
#include <concepts>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> intVec = {3, 1, 4, 1, 5};
sortAndPrint(intVec); // Output: 1 1 3 4 5
3. Custom Concepts
You can define custom concepts to encapsulate complex requirements.
#include <iostream>
#include <concepts>
class Number {
int value;
public:
Number(int v) : value(v) {}
Number square() const { return Number(value * value); }
friend std::ostream& operator<<(std::ostream& os, const Number&
,→ num) {
return os << num.value;
}
};
int main() {
Number num(4);
printSquare(num); // Output: 16
}
You can specify nested requirements within a concept to enforce more complex
constraints.
#include <iostream>
#include <concepts>
59
class MyClass {
public:
int foo() { return 42; }
bool bar() { return true; }
};
int main() {
MyClass obj;
process(obj); // Output: Processing: 42
}
2. Parameterized Concepts
Concepts can themselves be parameterized, allowing for even greater flexibility.
#include <iostream>
#include <concepts>
{ t + u } -> std::same_as<T>;
};
int main() {
std::cout << add(3, 4.5) << std::endl; // Output: 7.5
}
#include <iostream>
#include <concepts>
int main() {
std::cout << multiply(3, 4) << std::endl; // Output: 12
// std::cout << multiply(3.5, 4.5) << std::endl; // Error: No
,→ matching function
}
61
1.4.8 Conclusion
Concepts and constraints are transformative features in C++20 that bring clarity, safety, and
expressiveness to template programming. By mastering these tools, you can write more robust,
maintainable, and type-safe code. Concepts allow you to define semantic categories for types,
while constraints enable you to enforce these categories at compile time.
This section has provided a comprehensive overview of concepts and constraints, covering their
syntax, usage, and practical applications. With this knowledge, you are well-equipped to tackle
complex programming challenges and leverage the full power of modern C++. These techniques
are essential for developing high-quality, efficient, and maintainable software systems.
Chapter 2
62
63
2.1.2 std::thread
1. Definition
std::thread is a class in the C++ Standard Library that represents a single thread of
execution. It allows you to create and manage threads, enabling concurrent execution of
tasks.
2. Syntax
#include <thread>
void myFunction() {
// Task to be executed in the thread
}
int main() {
std::thread t(myFunction); // Create a thread that executes
,→ myFunction
t.join(); // Wait for the thread to finish
}
#include <iostream>
#include <thread>
64
void printHello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(printHello); // Create a thread
t.join(); // Wait for the thread to finish
std::cout << "Hello from main!" << std::endl;
}
4. Key Points
(a) Thread Lifecycle: A thread starts execution immediately after creation and runs
until the function it was created with returns.
(b) Joining Threads: Use join() to wait for a thread to finish. Failing to join a thread
can lead to a program crash.
std::thread t(printHello);
t.detach(); // Detach the thread
(a) Thread Arguments: You can pass arguments to the thread function.
65
int main() {
std::thread t(printMessage, "Hello from thread with arguments!");
t.join();
}
2.1.3 std::async
1. Definition
std::async is a higher-level abstraction for asynchronous task execution. It allows
you to run a function asynchronously and obtain the result through a std::future
object. std::async can execute tasks in a separate thread or defer execution until the
result is requested.
2. Syntax
#include <future>
int myFunction() {
return 42;
}
int main() {
std::future<int> result = std::async(myFunction); // Launch
,→ myFunction asynchronously
int value = result.get(); // Get the result
}
66
#include <iostream>
#include <future>
int computeSquare(int x) {
return x * x;
}
int main() {
std::future<int> result = std::async(computeSquare, 5);
std::cout << "Square of 5 is " << result.get() << std::endl;
}
• Output:
Square of 5 is 25
4. Key Points
(a) Returning Values: The result of the asynchronous task is accessed through a
std::future object.
(b) Exception Handling: Exceptions thrown in the asynchronous task are propagated to
the calling thread when get() is called.
int computeSquare(int x) {
if (x < 0) throw std::invalid_argument("Negative input");
return x * x;
}
int main() {
try {
auto result = std::async(computeSquare, -5);
std::cout << "Square is " << result.get() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
#include <iostream>
#include <thread>
#include <vector>
int main() {
const int numThreads = 4;
const int range = 100;
std::vector<std::thread> threads;
std::vector<int> results(numThreads);
int total = 0;
for (int r : results) {
total += r;
}
69
#include <iostream>
#include <future>
#include <fstream>
#include <string>
int main() {
auto future = std::async(std::launch::async, readFile,
,→ "example.txt");
try {
std::string content = future.get();
std::cout << "File content: " << content << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
3. Task-Based Parallelism
70
std::async can be used to implement task-based parallelism, where tasks are executed
concurrently.
#include <iostream>
#include <future>
#include <vector>
int computeSquare(int x) {
return x * x;
}
int main() {
std::vector<std::future<int>> futures;
for (int i = 1; i <= 5; ++i) {
futures.push_back(std::async(std::launch::async,
,→ computeSquare, i));
}
1. Thread Synchronization
Threads often need to synchronize access to shared resources to avoid race conditions.
C++ provides synchronization primitives like std::mutex and std::lock guard.
71
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int sharedData = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++sharedData;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
2. Thread Pools
Thread pools are used to manage a group of threads that execute tasks concurrently. While
C++ does not provide a built-in thread pool, you can implement one using
std::thread and std::queue.
72
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
class ThreadPool {
public:
ThreadPool(size_t numThreads) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex>
,→ lock(queueMutex);
condition.wait(lock, [this] { return
,→ !tasks.empty() || stop; });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
˜ThreadPool() {
{
std::unique_lock<std::mutex> lock(queueMutex);
73
stop = true;
}
condition.notify_all();
for (auto& worker : workers) {
worker.join();
}
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop = false;
};
int main() {
ThreadPool pool(4);
for (int i = 0; i < 8; ++i) {
pool.enqueue([i] {
std::cout << "Task " << i << " executed by thread " <<
,→ std::this_thread::get_id() << std::endl;
});
74
}
}
2.1.6 Conclusion
Threading is a powerful feature in C++ that enables concurrent and parallel execution of tasks.
By mastering std::thread and std::async, you can write efficient, scalable, and
maintainable concurrent programs. These tools, combined with synchronization primitives and
advanced techniques like thread pools, provide a solid foundation for modern C++ concurrency.
This section has provided a comprehensive overview of threading in C++, covering the syntax,
usage, and practical applications of std::thread and std::async. With this knowledge,
you are well-equipped to tackle complex concurrency challenges and leverage the full power of
modern C++. These techniques are essential for developing high-performance, responsive, and
robust software systems.
75
2.2.2 std::mutex
1. Definition
A mutex (short for ”mutual exclusion”) is a synchronization primitive that ensures only
one thread can access a shared resource at a time. In C++, std::mutex is a class in the
<mutex> header that provides this functionality.
2. Syntax
#include <mutex>
std::mutex mtx;
void criticalSection() {
76
• mtx.lock(): Locks the mutex. If the mutex is already locked, the calling thread
will block until the mutex is unlocked.
• mtx.unlock(): Unlocks the mutex, allowing other threads to acquire it.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int sharedData = 0;
void increment() {
mtx.lock();
++sharedData;
mtx.unlock();
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
77
• Output:
Shared data: 2
4. Key Points
(a) Locking and Unlocking: Always ensure that a mutex is unlocked after being locked.
Failing to unlock a mutex can lead to deadlocks.
(b) RAII Wrappers: To avoid manual locking and unlocking, use RAII (Resource
Acquisition Is Initialization) wrappers like std::lock guard or
std::unique lock.
#include <mutex>
std::mutex mtx;
void criticalSection() {
std::lock_guard<std::mutex> lock(mtx); // Automatically locks and
,→ unlocks the mutex
// Access shared resource
}
(a) Deadlocks: A deadlock occurs when two or more threads are waiting for each other
to release locks. To avoid deadlocks, always acquire locks in a consistent order.
78
#include <mutex>
std::mutex mtx;
void criticalSection() {
std::lock_guard<std::mutex> lock(mtx); // Automatically locks and
,→ unlocks the mutex
// Access shared resource
}
2. std::unique lock
std::unique lock is a more flexible RAII wrapper for std::mutex. It allows for
deferred locking, manual unlocking, and transfer of ownership.
#include <mutex>
std::mutex mtx;
void criticalSection() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // Defer
,→ locking
lock.lock(); // Manually lock the mutex
// Access shared resource
lock.unlock(); // Manually unlock the mutex
}
79
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int sharedData = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++sharedData;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
• Output:
Shared data: 2
80
2.2.4 std::atomic
1. Definition
std::atomic is a template class in the <atomic> header that provides atomic
operations on shared variables. Atomic operations are indivisible, meaning they cannot be
interrupted by other threads. This makes std::atomic ideal for simple shared
variables that do not require complex synchronization.
2. Syntax
#include <atomic>
std::atomic<int> atomicData(0);
void increment() {
++atomicData; // Atomic increment
}
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> atomicData(0);
void increment() {
++atomicData;
}
int main() {
81
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
• Output:
Atomic data: 2
4. Key Points
#include <atomic>
std::atomic<int> atomicData(0);
void increment() {
atomicData.fetch_add(1, std::memory_order_relaxed); // Relaxed
,→ memory ordering
}
82
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i) {
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
• Output:
83
Counter: 2000
2. Thread-Safe Queues
std::mutex can be used to implement thread-safe queues.
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
T pop() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return !queue.empty(); });
T value = queue.front();
queue.pop();
return value;
}
private:
std::queue<T> queue;
std::mutex mtx;
84
std::condition_variable cv;
};
int main() {
ThreadSafeQueue<int> queue;
std::thread producer([&queue] {
for (int i = 0; i < 10; ++i) {
queue.push(i);
}
});
std::thread consumer([&queue] {
for (int i = 0; i < 10; ++i) {
std::cout << "Consumed: " << queue.pop() << std::endl;
}
});
producer.join();
consumer.join();
}
• Output:
Consumed: 0
Consumed: 1
Consumed: 2
...
Consumed: 9
3. Double-Checked Locking
85
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
class Singleton {
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
Singleton() {}
static std::atomic<Singleton*> instance;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
int main() {
86
• Output:
Instance 1: 0xaddress
Instance 2: 0xaddress
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
87
void producer() {
for (int i = 0; i < 10; ++i) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(i);
cv.notify_one();
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !queue.empty(); });
int value = queue.front();
queue.pop();
std::cout << "Consumed: " << value << std::endl;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
• Output:
Consumed: 0
Consumed: 1
Consumed: 2
88
...
Consumed: 9
2. Read-Write Locks
Read-write locks allow multiple threads to read a shared resource simultaneously but
require exclusive access for writing. C++17 introduced std::shared mutex for this
purpose.
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex rwMutex;
int sharedData = 0;
void reader() {
std::shared_lock<std::shared_mutex> lock(rwMutex);
std::cout << "Read: " << sharedData << std::endl;
}
void writer() {
std::unique_lock<std::shared_mutex> lock(rwMutex);
++sharedData;
std::cout << "Write: " << sharedData << std::endl;
}
int main() {
std::thread t1(reader);
std::thread t2(writer);
std::thread t3(reader);
89
t1.join();
t2.join();
t3.join();
}
• Output:
Read: 0
Write: 1
Read: 1
2.2.7 Conclusion
Synchronization is a critical aspect of concurrent programming in C++. By mastering
std::mutex, std::atomic, and related tools, you can write safe, efficient, and
maintainable multi-threaded programs. These mechanisms ensure that shared resources are
accessed correctly, preventing race conditions and deadlocks.
This section has provided a comprehensive overview of synchronization in C++, covering the
syntax, usage, and practical applications of std::mutex and std::atomic. With this
knowledge, you are well-equipped to tackle complex concurrency challenges and leverage the
full power of modern C++. These techniques are essential for developing high-performance,
responsive, and robust software systems.
90
A coroutine is a function that can suspend its execution and resume later, allowing for
cooperative multitasking. Coroutines are particularly useful for asynchronous
programming, where tasks need to wait for I/O operations, timers, or other events without
blocking the entire program.
2. Key Characteristics
(a) Suspend and Resume: Coroutines can suspend execution using keywords like
co await and co yield, and resume later from the point of suspension.
91
(b) Stateful: Coroutines maintain their state between suspensions, allowing them to
resume execution with all local variables intact.
(c) Asynchronous: Coroutines are well-suited for asynchronous programming, enabling
non-blocking execution of tasks.
#include <iostream>
#include <coroutine>
struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
MyCoroutine myCoroutine() {
std::cout << "Coroutine started" << std::endl;
co_await std::suspend_always{};
std::cout << "Coroutine resumed" << std::endl;
}
int main() {
auto coro = myCoroutine();
std::cout << "Main function" << std::endl;
}
• Output:
92
Coroutine started
Main function
A coroutine handle is an object that represents the coroutine's state and allows you to
resume or destroy it. It is typically accessed through the promise type of the
coroutine.
2. Promise Type
The promise type is a user-defined type that controls the behavior of the coroutine. It
must define methods like get return object(), initial suspend(),
final suspend(), return void(), and unhandled exception().
3. Awaitable Type
An awaitable type is a type that can be used with the co await keyword. It defines
methods like await ready(), await suspend(), and await resume().
#include <iostream>
#include <coroutine>
struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
MyCoroutine myCoroutine() {
std::cout << "Coroutine started" << std::endl;
co_await std::suspend_always{};
std::cout << "Coroutine resumed" << std::endl;
}
int main() {
auto coro = myCoroutine();
std::cout << "Main function" << std::endl;
}
• Output:
Coroutine started
Main function
To resume a coroutine, you need to store its handle and call the resume() method.
#include <iostream>
#include <coroutine>
struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object() { return
,→ MyCoroutine{std::coroutine_handle<promise_type>::from_promise(*th
,→ }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
std::coroutine_handle<promise_type> handle;
MyCoroutine(std::coroutine_handle<promise_type> h) : handle(h) {}
˜MyCoroutine() { if (handle) handle.destroy(); }
MyCoroutine myCoroutine() {
std::cout << "Coroutine started" << std::endl;
co_await std::suspend_always{};
std::cout << "Coroutine resumed" << std::endl;
}
int main() {
auto coro = myCoroutine();
std::cout << "Main function" << std::endl;
95
coro.resume();
}
• Output:
Coroutine started
Main function
Coroutine resumed
#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>
struct AsyncTask {
struct promise_type {
AsyncTask get_return_object() { return
,→ AsyncTask{std::coroutine_handle<promise_type>::from_promise(*this
,→ }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
96
};
std::coroutine_handle<promise_type> handle;
AsyncTask(std::coroutine_handle<promise_type> h) : handle(h) {}
˜AsyncTask() { if (handle) handle.destroy(); }
AsyncTask asyncIO() {
std::cout << "Starting async I/O" << std::endl;
co_await std::suspend_always{};
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Async I/O completed" << std::endl;
}
int main() {
auto task = asyncIO();
std::cout << "Main function" << std::endl;
task.resume();
}
• Output:
2. Generators
97
#include <iostream>
#include <coroutine>
std::coroutine_handle<promise_type> handle;
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
˜Generator() { if (handle) handle.destroy(); }
T next() {
handle.resume();
return handle.promise().value;
}
};
int main() {
auto gen = generateNumbers(1, 5);
for (int i = 0; i < 5; ++i) {
std::cout << gen.next() << std::endl;
}
}
• Output:
1
2
3
4
5
3. Event Loops
Coroutines can be used to implement event loops, where tasks are executed cooperatively.
#include <iostream>
#include <coroutine>
#include <queue>
#include <functional>
struct EventLoop {
std::queue<std::function<void()>> tasks;
99
void run() {
while (!tasks.empty()) {
auto task = tasks.front();
tasks.pop();
task();
}
}
};
struct Task {
struct promise_type {
Task get_return_object() { return
,→ Task{std::coroutine_handle<promise_type>::from_promise(*this)};
,→ }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) {}
˜Task() { if (handle) handle.destroy(); }
int main() {
EventLoop loop;
auto task = myTask(loop);
loop.schedule([&task] { task.resume(); });
loop.run();
}
• Output:
Task started
Task resumed
You can define custom awaitable types to control the behavior of co await.
#include <iostream>
#include <coroutine>
struct MyAwaitable {
bool await_ready() { return false; }
101
struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
MyCoroutine myCoroutine() {
std::cout << "Coroutine started" << std::endl;
co_await MyAwaitable{};
std::cout << "Coroutine resumed" << std::endl;
}
int main() {
auto coro = myCoroutine();
std::cout << "Main function" << std::endl;
}
• Output:
102
Coroutine started
Suspending coroutine
Resuming coroutine
Coroutine resumed
Main function
2. Error Handling
Coroutines can handle exceptions using the unhandled exception() method in the
promise type.
#include <iostream>
#include <coroutine>
#include <stdexcept>
struct MyCoroutine {
struct promise_type {
MyCoroutine get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::cerr << "Exception caught: "
,→ << std::current_exception() << std::endl; }
};
};
MyCoroutine myCoroutine() {
std::cout << "Coroutine started" << std::endl;
throw std::runtime_error("Something went wrong");
co_await std::suspend_always{};
std::cout << "Coroutine resumed" << std::endl;
}
103
int main() {
auto coro = myCoroutine();
std::cout << "Main function" << std::endl;
}
• Output:
Coroutine started
Exception caught: Something went wrong
Main function
2.3.7 Conclusion
Coroutines are a powerful feature in C++20 that enable asynchronous programming by allowing
functions to suspend and resume execution. By mastering coroutines, you can write efficient,
non-blocking code for tasks like asynchronous I/O, generators, and event loops.
This section has provided a comprehensive overview of coroutines in C++, covering their syntax,
usage, and practical applications. With this knowledge, you are well-equipped to tackle complex
asynchronous programming challenges and leverage the full power of modern C++. These
techniques are essential for developing high-performance, responsive, and robust software
systems.
Chapter 3
Error Handling
Error handling is a critical aspect of software development, ensuring that programs can
gracefully handle unexpected situations and maintain robustness. In C++, exceptions are the
primary mechanism for handling errors, allowing you to separate error-handling code from
normal program logic. The noexcept specifier, introduced in C++11, provides a way to
indicate that a function will not throw exceptions, enabling optimizations and improving code
clarity.
This section will provide an in-depth exploration of exceptions and the noexcept specifier,
covering their syntax, usage, and practical applications. These concepts are essential for
advanced learners and professionals who want to write robust, maintainable, and efficient C++
code.
104
105
3.1.2 Exceptions
Definition An exception is an event that occurs during the execution of a program that
disrupts the normal flow of instructions. Exceptions are typically used to handle errors, such as
invalid input, resource allocation failures, or unexpected conditions.
Syntax Exceptions are thrown using the throw keyword and caught using try and catch
blocks.
#include <iostream>
#include <stdexcept>
int main() {
try {
riskyFunction(-1);
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
}
• Output:
Key Points
1. Throwing Exceptions: Use the throw keyword to throw an exception. The thrown
object is typically an instance of a class derived from std::exception.
2. Catching Exceptions: Use try and catch blocks to handle exceptions. The catch
block specifies the type of exception to handle.
Example: Custom Exception Class You can define custom exception classes by deriving
from std::exception.
#include <iostream>
#include <stdexcept>
void riskyFunction() {
throw MyException();
}
int main() {
try {
riskyFunction();
107
• Output:
Syntax
#include <iostream>
std::cout << "This function does not throw exceptions" << std::endl;
}
int main() {
safeFunction();
}
• Output:
Key Points
#include <iostream>
#include <stdexcept>
class Resource {
public:
Resource() { std::cout << "Resource acquired" << std::endl; }
˜Resource() { std::cout << "Resource released" << std::endl; }
};
void riskyFunction() {
Resource res;
throw std::runtime_error("Something went wrong");
}
int main() {
try {
riskyFunction();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
}
• Output:
Resource acquired
Resource released
Caught exception: Something went wrong
110
2. Error Propagation Exceptions allow errors to propagate up the call stack, enabling
centralized error handling.
#include <iostream>
#include <stdexcept>
void innerFunction() {
throw std::runtime_error("Error in inner function");
}
void outerFunction() {
try {
innerFunction();
} catch (const std::exception& e) {
std::cerr << "Caught exception in outer function: " << e.what() <<
,→ std::endl;
throw; // Re-throw the exception
}
}
int main() {
try {
outerFunction();
} catch (const std::exception& e) {
std::cerr << "Caught exception in main: " << e.what() <<
,→ std::endl;
}
}
• Output:
111
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass() { std::cout << "Constructed" << std::endl; }
˜MyClass() { std::cout << "Destructed" << std::endl; }
MyClass(MyClass&&) noexcept { std::cout << "Move constructed" <<
,→ std::endl; }
MyClass& operator=(MyClass&&) noexcept { std::cout << "Move assigned"
,→ << std::endl; return *this; }
};
int main() {
std::vector<MyClass> vec;
vec.push_back(MyClass()); // Move semantics are used
}
• Output:
Constructed
Move constructed
Destructed
Destructed
112
1. Basic Guarantee: No resources are leaked, and the program remains in a valid state.
2. Strong Guarantee: The operation is atomic; if an exception occurs, the program state is
rolled back to its original state.
#include <iostream>
#include <stdexcept>
class MyClass {
public:
MyClass(int value) : value(new int(value)) {}
˜MyClass() { delete value; }
private:
int* value;
};
int main() {
MyClass a(42);
MyClass b(100);
try {
a = b; // Strong guarantee
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
std::cout << "a: " << a.getValue() << ", b: " << b.getValue() <<
,→ std::endl;
}
• Output:
a: 100, b: 100
2. noexcept and Move Semantics Marking move constructors and move assignment
operators as noexcept enables optimizations in standard containers.
114
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass() { std::cout << "Constructed" << std::endl; }
˜MyClass() { std::cout << "Destructed" << std::endl; }
MyClass(MyClass&&) noexcept { std::cout << "Move constructed" <<
,→ std::endl; }
MyClass& operator=(MyClass&&) noexcept { std::cout << "Move assigned"
,→ << std::endl; return *this; }
};
int main() {
std::vector<MyClass> vec;
vec.push_back(MyClass()); // Move semantics are used
}
• Output:
Constructed
Move constructed
Destructed
Destructed
3.1.6 Conclusion
Exceptions and the noexcept specifier are essential tools for error handling in C++.
Exceptions allow you to handle errors gracefully and propagate them up the call stack, while
115
noexcept enables optimizations and improves code clarity by indicating that a function will
not throw exceptions.
This section has provided a comprehensive overview of exceptions and noexcept, covering
their syntax, usage, and practical applications. With this knowledge, you are well-equipped to
write robust, maintainable, and efficient C++ code. These techniques are essential for developing
high-quality software systems that can handle unexpected situations gracefully.
116
3.2.2 std::optional
Definition std::optional is a template class that represents an optional value. It can
either contain a value of a specified type or be empty. This is particularly useful for functions
that may or may not return a valid result.
#include <optional>
}
}
Syntax
#include <iostream>
#include <optional>
int main() {
auto value = getValue(true);
if (value) {
std::cout << "Value: " << *value << std::endl;
} else {
std::cout << "No value" << std::endl;
}
}
• Output:
Value: 42
Key Points
1. Accessing the Value: Use the * operator or the value() method to access the contained
value. If the optional is empty, calling value() will throw
std::bad optional access.
2. Checking for a Value: Use the has value() method or the implicit conversion to
bool to check if the optional contains a value.
3. Default Value: Use the value or() method to provide a default value if the optional is
empty.
#include <iostream>
#include <optional>
int main() {
auto value = getValue(false);
std::cout << "Value: " << value.value_or(100) << std::endl;
}
119
• Output:
Value: 100
#include <expected>
Syntax
#include <iostream>
#include <expected>
#include <string>
int main() {
auto result = getValue(true);
if (result) {
std::cout << "Value: " << *result << std::endl;
} else {
std::cerr << "Error: " << result.error() << std::endl;
}
}
• Output:
Value: 42
Key Points
1. Accessing the Value: Use the * operator or the value() method to access the contained
121
2. Accessing the Error: Use the error() method to access the error value.
3. Checking for a Value: Use the has value() method or the implicit conversion to
bool to check if the std::expected contains a value.
#include <iostream>
#include <expected>
#include <string>
int main() {
auto result = divide(10, 0);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cerr << result.error() << std::endl;
}
}
• Output:
122
#include <iostream>
#include <optional>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto result = findValue(vec, 3);
if (result) {
std::cout << "Found value: " << *result << std::endl;
} else {
std::cout << "Value not found" << std::endl;
}
}
• Output:
123
Found value: 3
2. Error Handling in Parsing std::expected is ideal for functions that can fail and need
to return an error message, such as parsing input.
#include <iostream>
#include <expected>
#include <string>
int main() {
auto result = parseInteger("abc");
if (result) {
std::cout << "Parsed value: " << *result << std::endl;
} else {
std::cerr << result.error() << std::endl;
}
}
• Output:
124
#include <iostream>
#include <optional>
#include <expected>
#include <string>
int main() {
auto result = complexOperation(true, false);
if (result) {
if (*result) {
std::cout << "Value: " << **result << std::endl;
} else {
std::cerr << "Error: " << result->error() << std::endl;
}
} else {
std::cerr << "No result" << std::endl;
125
}
}
• Output:
#include <iostream>
#include <optional>
int main() {
std::optional<int> value = 42;
auto result = value.and_then(addOne).and_then(multiplyByTwo);
if (result) {
126
• Output:
Result: 86
#include <iostream>
#include <expected>
#include <string>
int main() {
std::expected<int, std::string> value = 42;
auto result = value.and_then(addOne).and_then(multiplyByTwo);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
127
• Output:
Result: 86
2. Custom Error Types You can define custom error types to use with std::expected.
#include <iostream>
#include <expected>
#include <string>
int main() {
auto result = divide(10, 0);
if (result) {
128
• Output:
Error code: 2
3.2.6 Conclusion
std::optional and std::expected are powerful tools for error handling in modern
C++. std::optional provides a way to represent optional values, while
std::expected extends this by allowing the representation of either a value or an error.
These types enable more expressive and efficient error handling, particularly in scenarios where
exceptions are not suitable.
This section has provided a comprehensive overview of std::optional and
std::expected, covering their syntax, usage, and practical applications. With this
knowledge, you are well-equipped to write robust, maintainable, and efficient C++ code. These
techniques are essential for developing high-quality software systems that can handle errors
gracefully and efficiently.
Chapter 4
Advanced Libraries
The C++17 Standard Library introduced the <filesystem> library, which provides a
comprehensive set of tools for working with file systems. This library allows you to perform
operations such as creating, deleting, and traversing directories, querying file properties, and
manipulating file paths. The std::filesystem library is part of the C++ Standard Library
and is designed to be portable across different operating systems, making it an essential tool for
advanced learners and professionals who need to handle file system operations in a modern and
efficient way.
This section will provide an in-depth exploration of the std::filesystem library, covering
its syntax, usage, and practical applications. We will also delve into advanced techniques for
managing file systems and integrating them with other C++ features.
129
130
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path p = "/usr/local/bin";
std::cout << "Path: " << p << std::endl;
std::cout << "Parent path: " << p.parent_path() << std::endl;
std::cout << "Filename: " << p.filename() << std::endl;
std::cout << "Extension: " << p.extension() << std::endl;
}
• Output:
Path: "/usr/local/bin"
Parent path: "/usr/local"
Filename: "bin"
Extension: ""
3. Key Points
131
fs::path p = "/usr/local/../bin";
std::cout << "Normalized path: " << p.lexically_normal() <<
,→ std::endl;
• Output:
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
132
int main() {
fs::path dir = "test_dir";
if (fs::create_directory(dir)) {
std::cout << "Directory created: " << dir << std::endl;
}
if (fs::remove(dir)) {
std::cout << "Directory deleted: " << dir << std::endl;
}
}
• Output:
3. Key Points
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
133
int main() {
fs::path dir = "test_dir/sub_dir";
if (fs::create_directories(dir)) {
std::cout << "Directories created: " << dir << std::endl;
}
if (fs::remove_all("test_dir")) {
std::cout << "Directories deleted: test_dir" << std::endl;
}
}
• Output:
#include <iostream>
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
134
int main() {
fs::path src = "source.txt";
fs::path dst = "destination.txt";
// Clean up
fs::remove(src);
fs::remove("moved.txt");
}
• Output:
3. Key Points
(a) Copying Files: Use copy() to copy files or directories. You can specify options
like copy options::recursive for recursive copying.
135
(b) Moving Files: Use rename() to move files or directories. This operation is
typically faster than copying and deleting.
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path src = "source_dir";
fs::path dst = "destination_dir";
// Clean up
fs::remove_all(src);
fs::remove_all(dst);
}
• Output:
136
You can query file properties such as status, permissions, and size using
std::filesystem::status(), std::filesystem::permissions(), and
std::filesystem::file size().
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path p = "example.txt";
std::ofstream(p.string()) << "Hello, World!";
// Clean up
fs::remove(p);
}
• Output:
File type: 1
File permissions: 436
File size: 13 bytes
3. Key Points
(a) File Status: Use status() to get the status of a file or directory. The status
includes the file type and permissions.
(b) File Permissions: Use permissions() to get or set the permissions of a file or
directory.
(c) File Size: Use file size() to get the size of a file.
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
138
int main() {
fs::path p = "example.txt";
std::ofstream(p.string()) << "Hello, World!";
// Clean up
fs::remove(p);
}
• Output:
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path dir = "test_dir";
fs::create_directories(dir);
std::ofstream(dir / "file1.txt") << "File 1";
std::ofstream(dir / "file2.txt") << "File 2";
// Clean up
fs::remove_all(dir);
}
• Output:
Found: "test_dir/file1.txt"
Found: "test_dir/file2.txt"
#include <iostream>
#include <filesystem>
140
namespace fs = std::filesystem;
int main() {
fs::path dir = "test_dir";
fs::create_directories(dir / "sub_dir");
std::ofstream(dir / "file1.txt") << "File 1";
std::ofstream(dir / "sub_dir/file2.txt") << "File 2";
// Clean up
fs::remove_all(dir);
}
• Output:
Found: "test_dir/file1.txt"
Found: "test_dir/sub_dir/file2.txt"
2. Error Handling
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
try {
fs::path p = "non_existent_file.txt";
fs::file_size(p); // This will throw an exception
} catch (const fs::filesystem_error& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
• Output:
4.1.7 Conclusion
The std::filesystem library is a powerful and versatile tool for working with file systems
in C++. It provides a comprehensive set of functions for manipulating paths, creating and
deleting directories, copying and moving files, and querying file properties. By mastering the
std::filesystem library, you can write efficient, portable, and maintainable code for
handling file system operations.
This section has provided a comprehensive overview of the std::filesystem library,
covering its syntax, usage, and practical applications. With this knowledge, you are
well-equipped to tackle complex file system challenges and leverage the full power of modern
142
C++. These techniques are essential for developing high-quality software systems that interact
with the file system in a robust and efficient manner.
143
#include <iostream>
#include <net>
144
int main() {
net::io_context io_context;
net::ip::tcp::socket socket(io_context);
net::ip::tcp::endpoint
,→ endpoint(net::ip::make_address("127.0.0.1"), 8080);
socket.connect(endpoint);
char reply[1024];
size_t reply_length = net::read(socket, net::buffer(reply,
,→ sizeof(reply)));
socket.close();
}
• Output:
3. Key Points
145
(a) net::io context: The io context class is the central object for managing
I/O operations. It provides the event loop for asynchronous operations.
(b) net::ip::tcp::socket: The socket class represents a TCP socket. It
provides methods for connecting, sending, and receiving data.
(c) net::ip::tcp::endpoint: The endpoint class represents a network
endpoint, consisting of an IP address and a port number.
#include <iostream>
#include <net>
int main() {
net::io_context io_context;
net::ip::tcp::socket socket(io_context);
net::ip::tcp::endpoint
,→ endpoint(net::ip::make_address("127.0.0.1"), 8080);
socket.connect(endpoint);
char reply[1024];
size_t reply_length = net::read(socket, net::buffer(reply,
,→ sizeof(reply)));
socket.close();
}
• Output:
3. Key Points
(b) Sending Data: Use the net::write() function to send data to the server.
(c) Receiving Data: Use the net::read() function to receive data from the server.
You can create a TCP server using the net::ip::tcp::acceptor class to accept
incoming connections and exchange data.
#include <iostream>
#include <net>
int main() {
net::io_context io_context;
net::ip::tcp::acceptor acceptor(io_context,
,→ net::ip::tcp::endpoint(net::ip::tcp::v4(), 8080));
net::ip::tcp::socket socket(io_context);
acceptor.accept(socket);
char request[1024];
size_t request_length = net::read(socket, net::buffer(request,
,→ sizeof(request)));
socket.close();
}
• Output:
6. Key Points
#include <iostream>
#include <net>
int main() {
net::io_context io_context;
net::ip::tcp::socket socket(io_context);
net::ip::tcp::endpoint
,→ endpoint(net::ip::make_address("127.0.0.1"), 8080);
io_context.run();
}
• Output:
150
3. Key Points
#include <iostream>
#include <net>
net::async_write(socket, net::buffer(reply),
[&socket](const std::error_code& ec, std::size_t
,→ /*length*/) {
if (!ec) {
socket.close();
}
});
}
});
}
int main() {
net::io_context io_context;
net::ip::tcp::acceptor acceptor(io_context,
,→ net::ip::tcp::endpoint(net::ip::tcp::v4(), 8080));
net::ip::tcp::socket socket(io_context);
handle_accept(acceptor, socket);
io_context.run();
152
• Output:
6. Key Points
#include <iostream>
#include <net>
char reply[1024];
net::async_read(socket, net::buffer(reply,
,→ sizeof(reply)),
[&socket, reply](const std::error_code& ec,
,→ std::size_t length) {
if (ec) {
std::cerr << "Read error: " << ec.message()
,→ << std::endl;
return;
}
socket.close();
});
});
}
int main() {
net::io_context io_context;
net::ip::tcp::socket socket(io_context);
net::ip::tcp::endpoint
,→ endpoint(net::ip::make_address("127.0.0.1"), 8080);
io_context.run();
}
• Output:
2. Custom Protocols
You can create custom protocols by defining your own protocol classes and using them
with the networking library.
#include <iostream>
#include <net>
class MyProtocol {
public:
using endpoint = net::ip::tcp::endpoint;
using socket = net::ip::tcp::socket;
using acceptor = net::ip::tcp::acceptor;
int main() {
net::io_context io_context;
MyProtocol::acceptor acceptor(io_context,
,→ MyProtocol::endpoint(net::ip::tcp::v4(), 8080));
MyProtocol::socket socket(io_context);
acceptor.accept(socket);
char request[1024];
size_t request_length = net::read(socket, net::buffer(request,
,→ sizeof(request)));
socket.close();
}
• Output:
4.2.6 Conclusion
The C++20 networking library provides a powerful and portable way to handle networking tasks
in C++. By mastering this library, you can write efficient, maintainable, and scalable networked
applications. Whether you are creating a simple client-server application or a complex
distributed system, the networking library offers the tools you need to succeed.
This section has provided a comprehensive overview of the C++20 networking library, covering
its syntax, usage, and practical applications. With this knowledge, you are well-equipped to
tackle complex networking challenges and leverage the full power of modern C++. These
techniques are essential for developing high-quality software systems that communicate
effectively over networks.
Chapter 5
Practical Examples
Advanced C++ programming involves leveraging the full power of the language to create
efficient, maintainable, and scalable applications. This includes techniques such as
multithreading, template metaprogramming, and advanced use of the Standard Library. These
techniques are essential for advanced learners and professionals who want to write
high-performance and robust software systems.
This section will provide an in-depth exploration of advanced C++ programming techniques,
covering multithreaded applications and template metaprogramming. We will provide full
examples and practical applications to illustrate these concepts.
157
158
Introduction to Multithreading
Multithreading is a technique that allows a program to perform multiple tasks concurrently. This
is particularly useful for improving the performance of applications that need to handle multiple
tasks simultaneously, such as web servers, game engines, and scientific simulations.
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<std::thread> threads;
• Output:
159
Key Points
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex mtx;
int main() {
std::vector<std::thread> threads;
• Output:
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
class ThreadPool {
public:
ThreadPool(size_t numThreads) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this] { return !tasks.empty()
,→ || stop; });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
˜ThreadPool() {
{
std::unique_lock<std::mutex> lock(queueMutex);
162
stop = true;
}
condition.notify_all();
for (auto& worker : workers) {
worker.join();
}
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop = false;
};
int main() {
ThreadPool pool(4);
});
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
• Output:
#include <iostream>
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
int main() {
std::cout << "Factorial of 5: " << Factorial<5>::value << std::endl;
}
• Output:
Factorial of 5: 120
Key Points
1. Template Specialization: Use template specialization to define base cases for recursive
templates.
#include <iostream>
#include <type_traits>
int main() {
printType<int>();
printType<double>();
printType<std::string>();
}
• Output:
Integral type
Floating-point type
Other type
#include <iostream>
int main() {
print(1, 2, 3, "Hello", 4.5);
}
• Output:
123Hello4.5
#include <iostream>
#include <thread>
#include <vector>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
char buffer[1024];
ssize_t bytesRead = read(clientSocket, buffer, sizeof(buffer));
if (bytesRead > 0) {
write(clientSocket, "HTTP/1.1 200 OK\r\nContent-Length:
,→ 13\r\n\r\nHello, World!", 46);
}
close(clientSocket);
}
int main() {
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(8080);
std::vector<std::thread> threads;
while (true) {
int clientSocket = accept(serverSocket, nullptr, nullptr);
threads.emplace_back(handleClient, clientSocket);
}
close(serverSocket);
168
• Output:
#include <iostream>
int main() {
169
• Output:
58 64
139 154
5.1.5 Conclusion
Advanced C++ programming techniques such as multithreading and template metaprogramming
are essential for creating high-performance and robust software systems. By mastering these
techniques, you can write efficient, maintainable, and scalable applications that leverage the full
power of modern C++.
This section has provided a comprehensive overview of advanced C++ programming, covering
multithreaded applications and template metaprogramming. With this knowledge, you are
well-equipped to tackle complex programming challenges and develop high-quality software
170
systems. These techniques are essential for advanced learners and professionals who want to
excel in modern C++ development.
Chapter 6
171
172
• Improved Scalability: Since threads do not block, lock-free algorithms can scale
better on multi-core systems.
• Reduced Latency: Without the overhead of acquiring and releasing locks, lock-free
algorithms can achieve lower latency.
• Avoidance of Deadlocks: Lock-free algorithms are immune to deadlocks because
they do not rely on locking mechanisms.
• Progress Guarantees: Lock-free algorithms ensure that at least one thread makes
progress, even if other threads are delayed or fail.
behavior.
Example:
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed); // Atomically
,→ increment counter
2. Memory Ordering
Memory ordering defines the visibility of memory operations across threads. In a
multi-threaded environment, the order in which memory operations are observed can vary
due to compiler optimizations and hardware reordering. C++ provides several memory
ordering options to control this behavior:
• Acquire (memory order acquire): Ensures that subsequent operations are not
reordered before the atomic operation.
• Release (memory order release): Ensures that previous operations are not
reordered after the atomic operation.
• Acquire-Release (memory order acq rel): Combines both acquire and release
semantics.
• Sequentially Consistent (memory order seq cst): The strongest ordering,
providing a total order of all atomic operations.
Example:
std::atomic<bool> flag{false};
int data = 0;
// Thread 1
data = 42;
flag.store(true, std::memory_order_release);
// Thread 2
while (!flag.load(std::memory_order_acquire));
assert(data == 42); // Guaranteed to be true
3. Compare-and-Swap (CAS)
Compare-and-Swap (CAS) is a fundamental operation in lock-free programming. It
atomically compares the value at a memory location with an expected value and, if they
match, updates the memory location to a new value. CAS is typically implemented using
compare exchange strong or compare exchange weak in C++.
Example:
175
std::atomic<int> value{10};
int expected = 10;
while (!value.compare_exchange_weak(expected, 20)) {
// CAS failed, retry
}
4. Hazard Pointers
Hazard pointers are a memory reclamation technique used in lock-free data structures.
They allow threads to mark nodes that are being accessed, preventing them from being
deallocated prematurely. This is particularly important in lock-free algorithms where
nodes may be accessed concurrently by multiple threads.
Read-Modify-Write (RMW) operations are atomic operations that read a value from
memory, modify it, and write it back in a single, indivisible step. Examples include
fetch add, fetch sub, and exchange.
Example:
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed); // Atomically
,→ increment counter
1. Lock-Free Stack
A lock-free stack is typically implemented as a singly-linked list with a head pointer. Push
and pop operations are performed using CAS to update the head pointer atomically.
Example:
std::atomic<Node*> head;
public:
void push(T const& data) {
Node* const new_node = new Node(data);
new_node->next = head.load();
while (!head.compare_exchange_weak(new_node->next,
,→ new_node));
}
std::shared_ptr<T> pop() {
Node* old_head = head.load();
while (old_head && !head.compare_exchange_weak(old_head,
,→ old_head->next));
return old_head ? old_head->data : std::shared_ptr<T>();
}
};
177
2. Lock-Free Queue
A lock-free queue is often implemented as a linked list with head and tail pointers.
Enqueue and dequeue operations must carefully manage the head and tail pointers to
avoid race conditions.
Example:
std::atomic<Node*> head;
std::atomic<Node*> tail;
public:
LockFreeQueue() : head(new Node(T())), tail(head.load()) {}
˜LockFreeQueue() {
while (Node* const old_head = head.load()) {
head.store(old_head->next);
delete old_head;
}
}
std::shared_ptr<T> dequeue() {
while (true) {
Node* old_head = head.load();
Node* old_tail = tail.load();
Node* next = old_head->next.load();
if (old_head == head.load()) {
if (old_head == old_tail) {
if (next == nullptr) {
return std::shared_ptr<T>();
}
tail.compare_exchange_weak(old_tail, next);
} else {
std::shared_ptr<T> res = next->data;
if (head.compare_exchange_weak(old_head, next)) {
delete old_head;
return res;
179
}
}
}
}
}
};
A lock-free hash table can be implemented using an array of lock-free linked lists. Each
bucket in the array is managed independently, allowing concurrent access to different
buckets.
The ABA problem occurs when a value is changed from A to B and back to A, causing a
CAS operation to succeed incorrectly. This can be mitigated using techniques like
double-word CAS or versioning.
2. Memory Reclamation
3. Progress Guarantees
Lock-free algorithms provide progress guarantees, ensuring that at least one thread makes
progress. However, they do not guarantee fairness or starvation-freedom, which can be a
180
6.1.5 Conclusion
Lock-free programming is a powerful technique for building high-performance concurrent
systems. By leveraging atomic operations, memory ordering, and careful design, developers can
create data structures that scale well on modern multi-core processors. However, the complexity
and subtlety of lock-free algorithms require a deep understanding of concurrency, memory
models, and hardware behavior. As such, lock-free programming is best suited for advanced
learners and professionals who are willing to invest the time to master these concepts.
181
The default memory allocator provided by the C++ Standard Library is designed for
general-purpose use and may not be optimal for specific workloads. Custom memory
allocators can be tailored to the allocation patterns of an application, reducing overhead
and improving performance. For example, a custom allocator can pre-allocate memory in
large blocks, minimizing the number of system calls and reducing allocation latency.
2. Reduced Fragmentation
Memory fragmentation occurs when free memory is divided into small, non-contiguous
blocks, making it difficult to allocate large contiguous chunks. Custom memory allocators
can employ strategies like memory pooling or slab allocation to minimize fragmentation,
182
4. Deterministic Behavior
A memory pool pre-allocates a large block of memory and divides it into fixed-size
chunks. Allocations and deallocations are performed by managing these chunks, avoiding
the overhead of general-purpose allocators.
Example:
183
class MemoryPool {
private:
struct Block {
Block* next;
};
Block* freeList;
std::vector<char> memory;
public:
MemoryPool(size_t blockSize, size_t blockCount)
: memory(blockSize * blockCount) {
freeList = reinterpret_cast<Block*>(memory.data());
Block* current = freeList;
for (size_t i = 1; i < blockCount; ++i) {
current->next = reinterpret_cast<Block*>(memory.data() +
,→ i * blockSize);
current = current->next;
}
current->next = nullptr;
}
void* allocate() {
if (!freeList) throw std::bad_alloc();
Block* block = freeList;
freeList = freeList->next;
return block;
}
freeList = block;
}
};
2. Slab Allocation
Slab allocation is a technique where memory is divided into ”slabs,” each containing
objects of a fixed size. This approach is particularly efficient for allocating small objects
of the same type.
Example:
class SlabAllocator {
private:
std::vector<std::vector<char>> slabs;
size_t objectSize;
size_t slabSize;
public:
SlabAllocator(size_t objectSize, size_t slabSize)
: objectSize(objectSize), slabSize(slabSize) {}
void* allocate() {
for (auto& slab : slabs) {
if (slab.size() + objectSize <= slab.capacity()) {
void* ptr = slab.data() + slab.size();
slab.resize(slab.size() + objectSize);
return ptr;
}
}
slabs.emplace_back(slabSize);
return allocate();
185
3. Arenas
An arena allocator manages a large block of memory and allocates from it sequentially.
This is useful for scenarios where objects have similar lifetimes and can be deallocated in
bulk.
Example:
class Arena {
private:
std::vector<char> memory;
size_t offset;
public:
Arena(size_t size) : memory(size), offset(0) {}
void reset() {
offset = 0;
}
};
class LockFreeMemoryPool {
private:
struct Block {
std::atomic<Block*> next;
};
187
std::atomic<Block*> freeList;
public:
LockFreeMemoryPool(size_t blockSize, size_t blockCount) {
std::vector<char> memory(blockSize * blockCount);
Block* head = reinterpret_cast<Block*>(memory.data());
freeList.store(head);
void* allocate() {
Block* block = freeList.load();
while (block && !freeList.compare_exchange_weak(block,
,→ block->next.load()));
return block;
}
1. Hazard Pointers
Hazard pointers are a memory reclamation technique where threads mark nodes they are
accessing, preventing them from being deallocated prematurely.
Example:
std::atomic<void*> hazardPointers[MAX_THREADS];
2. Epoch-Based Reclamation
189
Epoch-based reclamation divides time into epochs and defers memory reclamation until
no threads are in an older epoch. This ensures that memory is only deallocated when it is
safe to do so.
Example:
class EpochManager {
private:
std::atomic<size_t> currentEpoch{0};
std::vector<std::atomic<size_t>> threadEpochs;
std::vector<std::vector<void*>> retiredLists;
public:
EpochManager(size_t numThreads) : threadEpochs(numThreads),
,→ retiredLists(numThreads) {}
void reclaim() {
size_t oldestEpoch = currentEpoch.load();
for (auto& epoch : threadEpochs) {
oldestEpoch = std::min(oldestEpoch, epoch.load());
}
for (auto& list : retiredLists) {
for (auto it = list.begin(); it != list.end();) {
if (threadEpochs[threadId].load() < oldestEpoch) {
delete *it;
190
it = list.erase(it);
} else {
++it;
}
}
}
currentEpoch.fetch_add(1);
}
};
3. Reference Counting
Reference counting tracks the number of references to a memory block and deallocates it
when the count reaches zero. This technique can be combined with atomic operations to
work in concurrent environments.
Example:
public:
void addRef() {
refCount.fetch_add(1);
}
void release() {
if (refCount.fetch_sub(1) == 1) {
delete static_cast<T*>(this);
}
191
}
};
Custom memory allocators must ensure proper alignment of allocated memory to avoid
performance penalties or crashes. Padding may be required to align memory blocks
correctly.
Custom memory allocators can make debugging and profiling more challenging. Tools
like Valgrind or AddressSanitizer can help identify memory-related issues.
Custom memory allocators can be integrated with the C++ Standard Library by providing
custom allocators for containers like std::vector or std::map.
Example:
CustomAllocator() = default;
T* allocate(size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
6.2.5 Conclusion
Custom memory management is a powerful tool for advanced C++ programmers, enabling them
to optimize performance, reduce fragmentation, and handle memory safely in concurrent
environments. By understanding and applying techniques like memory pools, slab allocation,
hazard pointers, and lock-free allocators, developers can build high-performance systems that
meet the demands of modern applications.
Appendices
Example:
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// Move resources from 'other'
}
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
193
194
2. Variadic Templates:
Example:
Example:
195
Example:
1. Thread Management:
Example:
std::mutex mtx;
void threadFunc() {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Thread ID: " << std::this_thread::get_id() << '\n';
}
Example:
197
std::atomic<int> counter{0};
void increment() {
counter.fetch_add(1, std::memory_order_relaxed);
}
3. Parallel Algorithms:
Example:
Example:
2. Cache-Friendly Programming:
4. Compiler Optimizations:
1. Custom Allocators:
Example:
2. Custom Iterators:
1. Platform-Specific Code:
2. Build Systems:
2. Game Development:
3. Embedded Systems:
1. Books:
2. Online Resources:
• CppReference (https://en.cppreference.com/).
• Stack Overflow and C++ forums.
Books
Books are an invaluable resource for mastering advanced C++ concepts. The following books
are highly recommended for readers who want to explore topics in greater depth:
• A detailed reference for the C++ Standard Library, including containers, algorithms,
and utilities.
204
205
• A seminal paper on lock-free data structures, including linked lists and skip lists.
• Discusses memory reclamation techniques like hazard pointers.
Online Resources
Online resources provide up-to-date information, tutorials, and community support for C++
programmers. The following websites and forums are highly recommended:
1. CppReference (https://en.cppreference.com/)
• A comprehensive online reference for the C++ Standard Library and language
features.
• Great for quick learning and staying updated with the latest trends.
• An online tool for exploring compiler output and understanding how C++ code is
translated into assembly.
1. CMake (https://cmake.org/)
• Simplifies the process of compiling and linking code across different platforms.
• Includes tools like clang-format for code formatting and clang-tidy for
static analysis.
3. Valgrind (https://valgrind.org/)
• A tool for detecting memory leaks, memory corruption, and threading issues.
• Essential for debugging and profiling C++ programs.
1. CppCon (https://cppcon.org/)
209
• Provides resources for learning and staying connected with the C++ community.
• The official standard for C++20, including new features like concepts, ranges, and
coroutines.
• A set of guidelines for writing modern, safe, and efficient C++ code.
• Maintained by Bjarne Stroustrup and Herb Sutter.
1. Pluralsight (https://www.pluralsight.com/)
2. Udemy (https://www.udemy.com/)
3. Leanpub (https://leanpub.com/)