Cpp
Classes
A class in C++ is defined using a specific syntax and is typically placed in a header file (.h
or .hh
) that needs to be imported in implementation files (.cpp
or .cc
).
Each class has special member functions:
- Constructor: initializes an object that can take parameters. If not defined, the compiler will generate a default one.
- Copy constructor: initializes an object using another object of the same class as an argument. If not defined, the compiler will generate a default one.
- Move constructor: initializes an object from an value. Unlike a copy constructor, which creates a new object as a copy of an existing object, a move constructor transfers resources from a source object to a new object, effectively “moving” the data.
- Destructor: called implicitly when the object goes out of scope. If not defined, the compiler will generate a default one. It does not take any parameters and includes cleanup/resource release code.
To instantiate an object of a class:
- Use
Car c;
which is equivalent toc();
for constructors with no parameters. - Use
Car c(<params-list>);
for constructors with parameters. - Use
Car c{<params-list>};
as a more modern approach where the compiler checks for and disallows narrowing conversions. Example:
int x{7.9}; // Error: narrowing conversion from 'double' to 'int'
char c{999}; // Error: narrowing conversion might occur
Encapsulation, Inheritance and polymorphism
Object-oriented programming defines three fundamental properties for a class type data: encapsulation, inheritance, and polymorphism. In the context of C++, these properties are implemented in specific ways.
Encapsulation means to properly restrict the access to some of the members We can set a member as:
- public: can be accessed by code outside of the object
- private
- protected: permits to derived classes to access protected members of the base class
#include <iostream>
class Vehicle {
public:
// Constructor with an initializer list
Vehicle() : nr_wheels(4), x(0) {}
virtual void move_one_step_forward() {
this->x += 1;
}
int num_of_wheels() const {
return nr_wheels;
}
protected:
int nr_wheels;
int x; // Position
};
class Car : public Vehicle {
public:
void move_one_step_forward() override {
this->x += 100; // Moves the car 100 units forward
}
};
int main() {
Car my_car;
// Move car one step forward
my_car.move_one_step_forward();
// Print number of wheels
std::cout << "New position: " << my_car.num_of_wheels() << std::endl;
return 0;
}
Some member functions may be pure virtual, which means that derived classes must provide their own implementation. An abstract class refers to a type of class whose member functions are all pure virtual. This is useful when defining a general interface but leaving the implementation to the specific case.
Pure virtual member function definitions must include the virtual keyword as a qualifier and an “=0” termination.
Operators
In addition to member data and functions, we can define and implement different operators in C++.
- The copy assignment operator is basically used for updating an object so it matches another object. By default, the compiler provides us with an implementation of this operator.
- The move assignment operator “moves” the state of the right-hand object into the assigned object, leaving the right-hand object empty. e.
The Rule of Five in modern C++ states that we should define or delete the following five member functions for classes that deal with resource management:
- the copy constructor
- the move constructor
- the copy assignment operator
- the move assignment operator
- the destructor.
Namespaces
Namespaces in C++ are used to prevent name conflicts by creating “named scopes” for the declaration of symbols, such as functions and classes. Symbols with the same name from different namespaces do not introduce conflicts.
The built-in namespace std includes the definitions of most of the C++ library symbols. Additionally, it is possible to define nested namespaces.
Templates
Templates in C++ are a useful feature for implementing functions and classes that can be used with different data types. They allow for code reuse and avoid writing the same code for each specific data type.
The data type for a template is determined during compilation, and the compiler generates the appropriate code to handle different data types. This means that we can have a single implementation that works for multiple cases.
Template code is typically placed in header files, making it easy to use and reuse in different parts of a program.
However, using templates has some drawbacks. Compiling code with templates can be slower and result in larger executable files. Modifying a template class often requires recompiling a significant portion of the project. Debugging can also become more complex due to lengthy error messages from the compiler.
template<typename T>
T duplicate(T param) {
return param*2;
}
//Calling template function
void testTemplate() {
cout<<duplicate(2)<<endl; //T=int
cout<<duplicate(2.5)<<endl; //T=double
}
Standard Template Library (STL)
The std namespace is probably the most important namespace in the library that encapsulates all the classes and functions defined by the STL library.
The STL (Standard Template Library) is an important part of the C++
language. It includes the definition of a set of template-based containers.
Container | Notes | Access Efficiency | Insert/Delete Efficiency |
---|---|---|---|
Vector | Contiguous storage; reallocates when capacity exceeded. | O(1) | O(n) at middle or start |
List | Double-linked list; extra memory for pointers. Good for sorting. | O(n) | O(1) |
Forward List | Single-linked list; less memory than list . | O(n) | O(1) |
Array | Fixed-size; contiguous storage. | O(1) | Not applicable |
Map | Stores key-value pairs; implemented as a binary search tree. | O(log n) | O(log n) |
Set | Stores unique elements; like map but only keys. | O(log n) | O(log n) |
Unordered Map | Stores key-value pairs; uses hashing. Faster than map /set . | O(1) or O(n) | O(1) or O(n) |
Containers are used to build collections of objects. When choosing a container, it is important to consider design aspects such as:
- Read access patterns
- How frequently we need to access objects? Do we need a direct access or can we tolerate an iteration over the entire collection?
- Write access patterns
- Does the content of the collection change frequently? (many add/remove operations)
- Memory occupancy
- Is the additional overhead introduced by the container an issue?
Smart pointers
Feature/Issue | Description |
---|---|
Raw Pointers | Traditional way of dynamic memory allocation using new /delete . Prone to memory leaks, segmentation faults, and unclear ownership. |
Smart Pointers | Introduced to automate memory management and prevent common issues associated with raw pointers. Defined in <memory> header. |
std::unique_ptr | Represents exclusive ownership of a dynamically allocated object. Not copyable, only moveable. Automatically deallocates memory when the pointer goes out of scope. |
std::shared_ptr | Allows for shared ownership of an object through reference counting. Memory is automatically freed when the last shared pointer to an object is destroyed or reset. |
std::weak_ptr | Complements std::shared_ptr by providing a non-owning “weak” reference to an object managed by shared_ptr , preventing cyclic references (cyclic references are a problem because they keep alive indefinitely each other). |
Example of shared_ptr
:
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
};
int main() {
std::shared_ptr<Resource> res1 = std::make_shared<Resource>(); // Resource acquired
std::shared_ptr<Resource> res2 = res1; // Both res1 and res2 now own the Resource
std::cout << "res1 use count: " << res1.use_count() << '\n'; // Outputs 2
res2.reset(); // res2 releases ownership; Resource is still alive
std::cout << "res1 use count: " << res1.use_count() << '\n'; // Outputs 1
// res1 goes out of scope here, automatically deleting the Resource
return 0; // Resource released
}
Functor or Function object
A function object (functor) is an object hat can be called as if it were a function. This is achieved by defining an operator()
within the class. Functors are useful because, unlike regular functions, they can have state.
#include <iostream>
class Increment {
private:
int num;
public:
Increment(int n) : num(n) {} // Constructor initializes the value to add
// The functor's call operator
int operator()(int i) const {
return num + i; // Adds the stored value to the given parameter
}
};
int main() {
Increment incFive(5); // Create a functor that adds 5
Increment incTen(10); // Create another functor that adds 10
std::cout << "Increment 5 to 3: " << incFive(3) << std::endl; // Outputs: 8
std::cout << "Increment 10 to 3: " << incTen(3) << std::endl; // Outputs: 13
return 0;
}
Bind expressions
std::bind
(found in the <functional>
header) that lets you prepare a function call in advance. It’s like setting up a function with some of its parameters pre-filled.
You can specify some arguments now and leave placeholders for others to be filled in later when the function is actually called.
int main() {
// Creating a function that always multiplies its argument by 2
auto multiply_by_2 = std::bind(multiply, 2, std::placeholders::_1);
// Now, you can use 'multiply_by_2' like a regular function
std::cout << "3 multiplied by 2 is " << multiply_by_2(3) << std::endl; // Outputs: 6
return 0;
}
Lambda expressions
Lambda expressions in C++11 let you write quick, unnamed (anonymous) functions right where you need them.
- Basic Structure:
[] () {}
is the simplest form of a lambda. - Capturing Variables: The part inside
[]
is about grabbing variables from the surrounding area so you can use them in the lambda. -[]
: Captures nothing. -[&]
: Captures everything around by reference. -[=]
: Captures everything around by making copies. -[=, &foo]
: Copies everything but referencesfoo
. -[foo]
: Only capturesfoo
by copying it. -[this]
: Capturesthis
pointer, so you can use the class’s member variables and functions. - Return Type: Usually, C++ can figure out what a lambda returns, so you don’t have to spell it out.
Examples:
- Capture Nothing:
[]() { std::cout << "Hello, World!"; }
- Capture by Reference:
[&]() { ++counter; }
(Imaginecounter
is a variable defined outside the lambda) - Capture by Value:
[=]() { return value + 1; }
(Here,value
is a variable from outside)
Lambdas are handy for quick operations, like when you’re using functions that need other functions as parameters, such as std::sort
or std::for_each
.
Modern C++ threading
C++11
introduced the class <thread>
:
Member function | Return value | Description |
---|---|---|
get_id() | thread::id | Returns an unique identifier object |
detach() | void | Allows the thread to run independently from the others |
join() | void | Blocks waiting for the thread to complete |
joinable() | bool | Check if the thread is joinable |
hardware_concurrency0 | unsigned | An hint on the HW thread contexts (often, number of CPU cores) |
operator= | Move assignment |
C++11
defines also the namespace std::this_thread
to group a set of functions to access the current thread.
Function | Return value | Description |
---|---|---|
yield() | void | Suspend the current thread, allowing other threads to be scheduled to run |
sleep_for() | void | Sleep for a certain amount of time |
sleep_until() | void | Sleep until a given timepoint |
Mutex
A mutex
is a common tool used for synchronizing access to data in order to ensure that only one thread can access it at a time.
To use a mutex, you can utilize the member function:
Member function | Description |
---|---|
lock | Locks the mutex. If already locked, the thread blocks. |
try_lock | Try to lock the mutex if not already locked |
try_lock_for | Try to lock the timed_mutex for a specified duration |
try_lock_until | Try to lock the timed_mutex until a time point |
unlock | Unlock the mutex |
release | Release ownership the mutex, without unlocking |
operator= | Unlocks the owned mutex and acquire another |
A key concept ts the RAII (Resource Acquisition Is Initialization): it ensures resources are acquired on initialization and released on destruction, applicable to all mutex types mentioned. Here more advanced mutex wrappers which follows the RAII pattern:
lock_guard
:- Automatically acquires a mutex when constructed and releases it when the scope ends, ensuring minimal overhead.
#include <mutex>
std::mutex myMutex;
void safeFunction() {
std::lock_guard<std::mutex> lock(myMutex); // The door is locked
// Critical section: only you can access the shared resource here
} // The door automatically unlocks here when `lock` goes out of scope
unique_lock
:- Supports deferred locking, transferring lock ownership, and manual lock/unlock operations.
- Has more overhead compared to
lock_guard
due to its additional features.
#include <mutex>
std::mutex myMutex;
void flexibleFunction() {
{
std::unique_lock<std::mutex> lock(myMutex);
// Do some work...
}
//outside scope, so lock automatically unlocked
{
std::unique_lock<std::mutex> lock(myMutex);
// Do some work...
lock.unlock(); // You can manually unlock
// Do some work without the lock...
}
}
The additional {
and }
create a new scope, which is used here to define the critical section more precisely. By limiting the scope of the lock to just the necessary operations on the shared resource, the code reduces the likelihood of contention and ensures that other threads can access the shared resource sooner, improving the overall efficiency of the multithreaded program.
scoped_lock
is similar to lock_guard
, but capable of locking multiple mutexes at once.
- Helps avoid deadlocks by managing multiple locks simultaneously.
- Adheres to the RAII pattern, automatically releasing all acquired mutexes when the scope ends.
#include <mutex>
std::mutex mutex1, mutex2;
void multiLockFunction() {
std::scoped_lock lock(mutex1, mutex2); // Both doors lock together
// Critical section: safe access to resources protected by both mutex1 and mutex2
} // Both doors automatically unlock here
shared_lock
allows multiple threads to hold a “read” lock (shared ownership) simultaneously, but only one thread can hold a “write” lock (exclusive ownership) at a time.- You use
unique_lock
when writing andshared_lock
when reading withshared_mutex
Condition variables
Condition variables are used when a thread needs to wait (block) for a specific condition to become true. Often used in conjunction with a mutex to synchronize the access to a resource and wait for specific conditions on that resource.
Member function | Description |
---|---|
wait(unique_lock<mutex> &) | Blocks the thread until another thread wakes it up. The Lockable object is unlocked for the duration of the wait |
wait_for( unique_lock<mutex>&) const chrono::duration<..> t) | Blocks the thread until another thread wakes it up, or a time span has passed. |
notify_one() | Wake up one of the waiting threads. |
notify_all() | Wake up all the waiting threads. If no thread is waiting do nothing. |
Spurious wakeups occur when a thread wakes up from waiting, even without a proper signal.
To handle spurious wakeups, it is recommended to use a while
loop instead of an if
statement. If we use an if
statement, there is a chance that the thread may proceed even after a spurious wakeup, leading to incorrect behavior.
while (queue.size() < minFill)
cv.wait(lock);
Design patterns for multithreaded programming
Now let’s see most used design patterns in multithreaded programming context using
Producer/Consumer
The Producer-Consumer pattern is a classic example of a multi-threading design pattern used to coordinate work between these two types of threads.
A SynchronizedQueue
is an implementation detail in this pattern: a thread-safe buffer between producers and consumers (it can be a FIFO data structure). Obviously to to protect the access to the queue, a Mutex (std::mutex
) is used.
A condition Variable (std::condition_variable
) is crucial for coordinating between producers and consumers without occupying the CPU.
Active Object
This pattern allows for threads to be treated as objects with their own methods, making it easier to manage. This approach is more organized and secure than using global variables for thread communication.
class ActiveObject {
public:
ActiveObject() : quit(false) {
// Constructor just starts the thread
t = std::thread(&ActiveObject::run, this);
}
virtual ~ActiveObject() {
// Destructor signals the thread to stop and waits for it to finish
quit = true;
if (t.joinable()) {
t.join();
}
}
private:
virtual void run() = 0; // Pure virtual function to be implemented by derived classes
protected:
std::atomic<bool> quit; // Atomic flag to safely signal the thread to stop
std::thread t; // The encapsulated thread
};
std::atomic<bool>
std::atomic<bool>
is often used to ensure thread-safe signaling:.
You can initialize it just like a regular boolean and both use explicit assignments or store
and load
methods
quit = true; // or
quit.store(true);
bool shouldQuit = quit; //or
bool shouldQuit = quit.load()
store
and load
are member functions provided by the std::atomic
template class in C++, which are used to safely set and retrieve the value of an atomic variable in a multithreaded environment.
store
Method:
The store
method sets the value of the atomic variable. It ensures that the write operation is atomic, meaning it cannot be interrupted or divided into smaller operations that other threads can observe partway through. This atomicity guarantees that other threads always see the variable as either fully updated or not updated at all, with no intermediate states.
- Syntax:
atomic_variable.store(desired_value, memory_order);
- Example:
quit.store(true);
sets the atomic booleanquit
totrue
.
The memory_order
argument is optional and specifies the memory ordering semantics for the operation. If omitted, it defaults to memory_order_seq_cst
, which provides sequential consistency, the strongest memory ordering.
load
Method:
The load
method retrieves the current value of the atomic variable, ensuring that the read operation is atomic. This means that the value read is consistent and not a partial value that could result from simultaneous write operations in other threads.
- Syntax:
value = atomic_variable.load(memory_order);
- Example:
bool shouldQuit = quit.load();
reads the value ofquit
intoshouldQuit
.
Like store
, load
also accepts an optional memory_order
argument that dictates the memory ordering semantics of the read operation. If not specified, it defaults to memory_order_seq_cst
.
Use in Multithreading:
store
and load
are essential for thread-safe operations on shared variables in concurrent code, preventing data races and ensuring visibility of changes across threads. They are particularly useful for flags and counters that are accessed and modified by multiple threads, such as loop control variables or state indicators.
For the displayInit()
function in an embedded context, using a spinlock might not be necessary unless you’re dealing with a multi-threaded environment where other threads might modify the LCD controller registers concurrently. In many embedded applications, especially in initialization routines, simple polling is sufficient and commonly used due to its simplicity and the single-threaded nature of most initialization sequences.
Reactor
The Reactor pattern allows for the decoupling of task creation and execution by packaging a function and its arguments to be called later. This pattern utilizes:
- Active objects and synchronized queues: These components are combined to manage tasks and ensure thread-safe communication.
- Task queue: the central place where tasks are stored, waiting to be executed by the reactor thread.
- Execution order: tasks are typically executed in a FIFO order.
ThreadPool Pattern Explained
While the Reactor pattern is suited for scenarios where tasks can be processed sequentially, the ThreadPool pattern optimizes performance employing multiple executor threads:
- Multiple Worker Threads: These threads concurrently pick up and execute tasks from one or more shared task queues.
- The number of worker threads (ideally the number of available CPU cores) and task allocation are design considerations.
Both the Reactor and ThreadPool patterns are foundational in concurrent programming, providing mechanisms to handle tasks efficiently in multi-threaded environments.