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:

  1. Constructor: initializes an object that can take parameters. If not defined, the compiler will generate a default one.
  2. 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.
  3. 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.
  4. 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 to c(); 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.

ContainerNotesAccess EfficiencyInsert/Delete Efficiency
VectorContiguous storage; reallocates when capacity exceeded.O(1)O(n) at middle or start
ListDouble-linked list; extra memory for pointers. Good for sorting.O(n)O(1)
Forward ListSingle-linked list; less memory than list.O(n)O(1)
ArrayFixed-size; contiguous storage.O(1)Not applicable
MapStores key-value pairs; implemented as a binary search tree.O(log n)O(log n)
SetStores unique elements; like map but only keys.O(log n)O(log n)
Unordered MapStores 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/IssueDescription
Raw PointersTraditional way of dynamic memory allocation using new/delete. Prone to memory leaks, segmentation faults, and unclear ownership.
Smart PointersIntroduced to automate memory management and prevent common issues associated with raw pointers. Defined in <memory> header.
std::unique_ptrRepresents exclusive ownership of a dynamically allocated object. Not copyable, only moveable. Automatically deallocates memory when the pointer goes out of scope.
std::shared_ptrAllows 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_ptrComplements 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 references foo. - [foo]: Only captures foo by copying it. - [this]: Captures this 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; } (Imagine counter 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 functionReturn valueDescription
get_id()thread::idReturns an unique identifier object
detach()voidAllows the thread to run independently from the others
join()voidBlocks waiting for the thread to complete
joinable()boolCheck if the thread is joinable
hardware_concurrency0unsignedAn 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.

FunctionReturn valueDescription
yield()voidSuspend the current thread, allowing
other threads to be scheduled to run
sleep_for()voidSleep for a certain amount of time
sleep_until()voidSleep 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 functionDescription
lockLocks the mutex. If already locked, the thread blocks.
try_lockTry to lock the mutex if not already locked
try_lock_forTry to lock the timed_mutex for a specified duration
try_lock_untilTry to lock the timed_mutex until a time point
unlockUnlock the mutex
releaseRelease 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 and shared_lock when reading with shared_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 functionDescription
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 boolean quit to true.

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 of quit into shouldQuit.

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.