Advance C++

STL (Standard Template Library)

Multithreading is a feature in C++ that allows multiple threads to run concurrently, making better use of the CPU. Each thread is a separate flow of execution within a process, which allows multiple parts of a program to run in parallel.

Multithreading support was added in C++11, and before that, programmers had to use the POSIX threads (pthreads) library. C++11 introduced std::thread, which made multithreading much easier and portable across different platforms. The std::thread class and related utilities are provided in the <thread> header.

Syntax for Creating a Thread:

std::thread thread_object(callable);

Here, std::thread represents a single thread in C++. To start a new thread, we create a std::thread object and pass a callable to its constructor. A callable can be:

1. A Function Pointer
2. A Lambda Expression
3. A Function Object (Functor)
4. A Non-Static Member Function
5. A Static Member Function

Once the callable is passed, the thread will execute the corresponding code.

Launching a Thread Using a Function Pointer

A function pointer can be passed to a std::thread constructor to launch a thread:

void my_function(int value)
{
    for (int i = 0; i < value; i++) {
        std::cout << "Thread using function pointer\n";
    }
}

// Creating and launching the thread
std::thread my_thread(my_function, 5);
Launching a Thread Using a Lambda Expression

A lambda expression is a convenient way to define a callable on the fly. Here’s how to use it to launch a thread:

auto my_lambda = [](int value) {
    for (int i = 0; i < value; i++) {
        std::cout << "Thread using lambda expression\n";
    }
};

// Launching a thread using the lambda expression
std::thread my_thread(my_lambda, 5);
Launching a Thread Using a Function Object (Functor)

A function object (functor) is a class with an overloaded () operator. Here’s an example:

class Functor {
public:
    void operator()(int value) {
        for (int i = 0; i < value; i++) {
            std::cout << "Thread using functor\n";
        }
    }
};

// Launching a thread using a function object
std::thread my_thread(Functor(), 5);

Output:

a < b  : 0
a > b  : 1
a <= b: 0
a >= b: 1
a == b: 0
a != b : 1
Launching a Thread Using a Non-Static Member Function

Non-static member functions require an instance of the class to be called. Here’s how to use a non-static member function in a thread:

class MyClass {
public:
    void my_method(int value) {
        for (int i = 0; i < value; i++) {
            std::cout << "Thread using non-static member function\n";
        }
    }
};

// Creating an instance of the class
MyClass my_obj;

// Launching the thread
std::thread my_thread(&MyClass::my_method, &my_obj, 5);
Launching a Thread Using a Static Member Function

Static member functions do not require an instance of the class and can be directly passed to a thread:

class MyClass {
public:
    static void my_static_method(int value) {
        for (int i = 0; i < value; i++) {
            std::cout << "Thread using static member function\n";
        }
    }
};

// Launching the thread using the static member function
std::thread my_thread(&MyClass::my_static_method, 5);
Waiting for Threads to Finish

Once a thread is launched, we may need to wait for it to finish before proceeding. The join() function blocks the calling thread until the specified thread completes execution.

int main() {
    std::thread t1(my_function, 5);
    t1.join();  // Wait for t1 to finish

    // Proceed with other tasks after t1 finishes
}
Complete C++ Program for Multithreading

Below is a complete C++ program that demonstrates launching threads using different callables, including a function pointer, lambda expression, functor, and member functions:

#include <iostream>
#include <thread>

// Function to be used as a function pointer
void function_pointer(int value) {
    for (int i = 0; i < value; i++) {
        std::cout << "Thread using function pointer\n";
    }
}

// Functor (Function Object)
class Functor {
public:
    void operator()(int value) {
        for (int i = 0; i < value; i++) {
            std::cout << "Thread using functor\n";
        }
    }
};

// Class with member functions
class MyClass {
public:
    void non_static_function() {
        std::cout << "Thread using non-static member function\n";
    }
    static void static_function() {
        std::cout << "Thread using static member function\n";
    }
};

int main() {
    std::cout << "Launching threads...\n";

    // Launch thread using function pointer
    std::thread t1(function_pointer, 3);

    // Launch thread using functor
    Functor functor;
    std::thread t2(functor, 3);

    // Launch thread using lambda expression
    auto lambda = [](int value) {
        for (int i = 0; i < value; i++) {
            std::cout << "Thread using lambda expression\n";
        }
    };
    std::thread t3(lambda, 3);

    // Launch thread using non-static member function
    MyClass obj;
    std::thread t4(&MyClass::non_static_function, &obj);

    // Launch thread using static member function
    std::thread t5(&MyClass::static_function);

    // Wait for all threads to finish
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();

    std::cout << "All threads finished.\n";

    return 0;
}

Output:

Launching threads...
Thread using function pointer
Thread using function pointer
Thread using function pointer
Thread using lambda expression
Thread using lambda expression
Thread using lambda expression
Thread using functor
Thread using functor
Thread using functor
Thread using non-static member function
Thread using static member function
All threads finished.

Pointers in C++

#include <iostream>
using namespace std;

class Demo {
private:
    int data;
};

void memoryLeak() {
    Demo* p = new Demo();
}

int main() {
    while (true) {
        memoryLeak();
    }
    return 0;
}

Explanation:
In the memoryLeak function, a pointer to a dynamically created Demo object is created, but the memory allocated by new is never deallocated using delete, leading to a memory leak as the program keeps allocating memory without freeing it.

Smart Pointers

Smart pointers in C++ automatically manage memory allocation and deallocation, avoiding manual delete calls and preventing memory leaks. Unlike normal pointers, smart pointers automatically free the memory when they go out of scope. Smart pointers overload operators like * and -> to behave similarly to normal pointers but with additional memory management features.

Example of a Smart Pointer:

#include <iostream>
using namespace std;

class SmartPtr {
    int* ptr;
public:
    explicit SmartPtr(int* p = nullptr) { ptr = p; }
    ~SmartPtr() { delete ptr; }
    int& operator*() { return *ptr; }
};

int main() {
    SmartPtr sp(new int());
    *sp = 30;
    cout << *sp << endl;  // Outputs 30
    return 0;
}

Explanation:
The destructor of the SmartPtr class automatically frees the memory when the object goes out of scope, preventing memory leaks.

Differences Between Normal Pointers and Smart Pointers
PointerSmart Pointer
A pointer holds a memory address and data type info.Smart pointers are objects that wrap a pointer.
A normal pointer does not deallocate memory when it goes out of scope.Automatically deallocates memory when out of scope.
Manual memory management is required.Handles memory management automatically.

Generic Smart Pointer Using Templates:

#include <iostream>
using namespace std;

template <class T>
class SmartPtr {
    T* ptr;
public:
    explicit SmartPtr(T* p = nullptr) { ptr = p; }
    ~SmartPtr() { delete ptr; }
    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }
};

int main() {
    SmartPtr<int> sp(new int());
    *sp = 40;
    cout << *sp << endl;  // Outputs 40
    return 0;
}
Types of Smart Pointers

C++ provides several types of smart pointers that are available in the standard library:

1. unique_ptr: Manages a single object and ensures that only one unique_ptr instance can point to a particular object.
2. shared_ptr: Allows multiple shared_ptr objects to share ownership of the same object, managing reference counting.
3. weak_ptr: A non-owning smart pointer that is used in conjunction with shared_ptr to avoid circular references.

Example: Using unique_ptr

Area: 48
Area (after transfer): 48

Explanation:
The ownership of the object is transferred from up1 to up2 using std::move. After the transfer, up1 becomes null, and up2 owns the object.

Example: Using shared_ptr

#include <iostream>
#include <memory>
using namespace std;

class Circle {
    int radius;
public:
    Circle(int r) : radius(r) {}
    int circumference() { return 2 * 3.14 * radius; }
};

int main() {
    shared_ptr<Circle> sp1(new Circle(7));
    cout << "Circumference: " << sp1->circumference() << endl;

    shared_ptr<Circle> sp2 = sp1;
    cout << "Reference count: " << sp1.use_count() << endl;

    return 0;
}

Output:

Circumference: 43.96
Reference count: 2

Explanation:
Both sp1 and sp2 share ownership of the Circle object, and the reference count is managed automatically.

Example: Using weak_ptr

#include <iostream>
#include <memory>
using namespace std;

class Box {
    int size;
public:
    Box(int s) : size(s) {}
    int getSize() { return size; }
};

int main() {
    shared_ptr<Box> sp1(new Box(15));
    weak_ptr<Box> wp1(sp1);  // weak_ptr does not increase reference count

    cout << "Box size: " << sp1->getSize() << endl;
    cout << "Reference count: " << sp1.use_count() << endl;

    return 0;
}

Output:

Box size: 15
Reference count: 1

auto_ptr vs unique_ptr vs shared_ptr vs weak_ptr in C++

Smart pointers in C++ are special objects that manage memory and resources automatically. They are part of the C++ Standard Library and are defined in the <memory> header. The key types of smart pointers are:

1. auto_ptr (deprecated in C++11)
2. unique_ptr
3. shared_ptr
4. weak_ptr

These smart pointers are used to manage dynamic memory and other resources, ensuring proper cleanup and preventing issues such as memory leaks and dangling pointers.

1. auto_ptr :auto_ptr was a smart pointer in C++ before C++11 but was deprecated because of its limitations. It manages the memory of dynamically allocated objects and automatically deletes the object when the auto_ptr goes out of scope. However, auto_ptr follows a transfer-of-ownership model, meaning only one pointer can own an object at a time. Copying or assigning an auto_ptr transfers ownership, making the original pointer empty.

Example:

(10 * 5) + (8 / 2) = 50 + 4 = 54

Example 1: C Program to Calculate the Area and Perimeter of a Rectangle

#include <iostream>
#include <memory>
using namespace std;

class MyClass {
public:
    void display() { cout << "MyClass::display()" << endl; }
};

int main() {
    auto_ptr<MyClass> ptr1(new MyClass);
    ptr1->display();

    // Transfer ownership to ptr2
    auto_ptr<MyClass> ptr2(ptr1);
    ptr2->display();

    // ptr1 is now empty
    cout << "ptr1: " << ptr1.get() << endl;
    cout << "ptr2: " << ptr2.get()

Output:

Area = 21
Perimeter = 20

Why is auto_ptr deprecated?

  • Ownership Transfer:When copying or assigning an auto_ptr, ownership is transferred, and the source pointer becomes null. This behavior made auto_ptr unsuitable for use in STL containers, which require copy semantics.

  • Lack of Reference Counting: auto_ptr does not support shared ownership, so it cannot be used in scenarios where multiple pointers need to reference the same object.

2. unique_ptr:unique_ptr was introduced in C++11 to replace auto_ptr. It provides exclusive ownership of a dynamically allocated object and ensures that only one unique_ptr can manage a resource at a time. It prevents copying but supports transferring ownership using the std::move() function.

Example:

#include <iostream>
#include <memory>
using namespace std;

class MyClass {
public:
    void display() { cout << "MyClass::display()" << endl; }
};

int main() {
    unique_ptr<MyClass> ptr1(new MyClass);
    ptr1->display();

    // Transfer ownership to ptr2
    unique_ptr<MyClass> ptr2 = move(ptr1);
    ptr2->display();

    // ptr1 is now empty
    cout << "ptr1: " << ptr1.get() << endl;
    cout << "ptr2: " << ptr2.get() << endl;

    return 0;
}

Output:

MyClass::display()
MyClass::display()
ptr1: 0
ptr2: <address>
Key Features of unique_ptr:
  • Exclusive Ownership: Only one unique_ptr can own a resource at a time.
  • Move Semantics: Ownership can be transferred using std::move().
  • Resource Management: When the unique_ptr goes out of scope, the resource is automatically freed.

3. shared_ptr :shared_ptr provides shared ownership of a dynamically allocated object. It uses a reference counting mechanism to keep track of how many pointers are pointing to the object. The object is only destroyed when the reference count reaches zero, meaning all shared_ptrs referencing the object have been deleted.

Example:

#include <iostream>
#include <memory>
using namespace std;

class MyClass {
public:
    void display() { cout << "MyClass::display()" << endl; }
};

int main() {
    shared_ptr<MyClass> ptr1(new MyClass);
    ptr1->display();

    // Share ownership with ptr2
    shared_ptr<MyClass> ptr2(ptr1);
    cout << "ptr1 count: " << ptr1.use_count() << endl;
    cout << "ptr2 count: " << ptr2.use_count() << endl;

    // Reset ptr1
    ptr1.reset();
    cout << "ptr1: " << ptr1.get() << endl;
    cout << "ptr2 count after ptr1 reset: " << ptr2.use_count() << endl;

    return 0;
}

Output:

MyClass::display()
ptr1 count: 2
ptr2 count: 2
ptr1: 0
ptr2 count after ptr1 reset: 1

this Pointer in C++

In C++, the this pointer refers to the current object of a class and is implicitly passed to all non-static member function calls. It is a hidden argument that provides access to the calling object inside the member function.

Type of this Pointer

The type of the this pointer depends on whether the member function is const, volatile, or both. The this pointer type will either be const ExampleClass* or ExampleClass* based on whether the member function is const or not.

1) Const ExampleClass: When a member function is declared as const, the type of the this pointer inside that function becomes const ExampleClass* const. This ensures that the function cannot modify the object that called it.

Example:

#include <iostream>
using namespace std;

class Demo {
public:
    void show() const {
        // 'this' is implicitly passed as a hidden argument
        // The type of 'this' is 'const Demo* const'
        cout << "Const member function called" << endl;
    }
};

int main() {
    Demo obj;
    obj.show();
    return 0;
}

2) Non-Const ExampleClass: If the member function is not const, the this pointer is of type ExampleClass* const. This means that the function can modify the state of the object it is called on.

Example:

#include <iostream>
using namespace std;

class Demo {
public:
    void show() {
        // 'this' is implicitly passed as a hidden argument
        // The type of 'this' is 'Demo* const'
        cout << "Non-const member function called" << endl;
    }
};

int main() {
    Demo obj;
    obj.show();
    return 0;
}

3) Volatile ExampleClass: When a member function is declared as volatile, the type of the this pointer becomes volatile ExampleClass* const. This means that the function can work with objects that are volatile (i.e., objects that can be modified outside the program’s control).

Example:

#include <iostream>
using namespace std;

class Demo {
public:
    void show() volatile {
        // 'this' is implicitly passed as a hidden argument
        // The type of 'this' is 'volatile Demo* const'
        cout << "Volatile member function called" << endl;
    }
};

int main() {
    volatile Demo obj;
    obj.show();
    return 0;
}

4) Const Volatile ExampleClass: If a member function is declared as both const and volatile, the type of the this pointer becomes const volatile ExampleClass* const.

Example:

#include <iostream>
using namespace std;

class Demo {
public:
    void show() const volatile {
        // 'this' is implicitly passed as a hidden argument
        // The type of 'this' is 'const volatile Demo* const'
        cout << "Const volatile member function called" << endl;
    }
};

int main() {
    const volatile Demo obj;
    obj.show();
    return 0;
}

“delete this” in C++

Using delete this in C++

The delete operator should ideally not be used on the this pointer, as it can lead to undefined behavior if not handled carefully. However, if it is used, the following considerations must be taken into account:

1) The object must be created using new: The delete operator only works for objects that have been dynamically allocated using the new operator. If an object is created on the stack or as a local variable (i.e., without new), using delete this will result in undefined behavior.

Example:

#include <iostream>
using namespace std;

class MyClass {
public:
    void destroy() {
        delete this;  // Deletes the current object
    }
};

int main() {
    // Valid: Object created using new
    MyClass* obj = new MyClass;
    obj->destroy();
    obj = nullptr;  // Ensure pointer is set to null after deletion

    // Invalid: Undefined behavior, object created on the stack
    MyClass obj2;
    obj2.destroy();  // This will cause undefined behavior

    return 0;
}

In the valid case, the object is dynamically allocated, and using delete this will correctly free the memory. In the invalid case, the object is created locally, and deleting a non-dynamic object leads to undefined behavior.

2) Accessing members after delete this leads to undefined behavior : Once delete this is called, the object is destroyed, and any attempt to access its members after deletion results in undefined behavior. The program might appear to work, but accessing members of a deleted object is dangerous and unreliable.

Example:

#include <iostream>
using namespace std;

class MyClass {
    int value;
public:
    MyClass() : value(42) {}

    void destroy() {
        delete this;

        // Invalid: Undefined behavior
        cout << value << endl;  // This might work but is unsafe
    }
};

int main() {
    MyClass* obj = new MyClass;
    obj->destroy();  // Calls delete this and tries to access a deleted object
    return 0;
}

Output:

42  // This is unpredictable and could vary depending on the system

Passing a Function as a Parameter in C++

In C++, functions can be passed as parameters in various ways. This technique is useful, for instance, when passing custom comparator functions in algorithms like std::sort(). There are three primary ways to pass a function as an argument:

1. Passing a function pointer
2. Using std::function<>
3. Using lambdas

1. Passing a Function Pointer : A function can be passed to another function by passing its address, which can be done through a pointer.

Example:

#include <iostream>
using namespace std;

// Function to add two numbers
int add(int x, int y) { return x + y; }

// Function to multiply two numbers
int multiply(int x, int y) { return x * y; }

// Function that takes a pointer to another function
int execute(int x, int y, int (*func)(int, int)) {
    return func(x, y);
}

int main() {
    // Pass pointers to the 'add' and 'multiply' functions
    cout << "Addition of 15 and 5: " << execute(15, 5, &add) << '\n';
    cout << "Multiplication of 15 and 5: " << execute(15, 5, &multiply) << '\n';

    return 0;
}

Output:

Addition of 15 and 5: 20
Multiplication of 15 and 5: 75

2. Using std::function<> : From C++11, the std::function<> template class allows passing functions as objects. A std::function<> object can be created using the following format:

std::function<return_type(arg1_type, arg2_type...)> obj_name;

You can then call the function object like this:

return_type result = obj_name(arg1, arg2);

Example:

#include <functional>
#include <iostream>
using namespace std;

// Define add and multiply functions
int add(int x, int y) { return x + y; }
int multiply(int x, int y) { return x * y; }

// Function that accepts an object of type std::function<>
int execute(int x, int y, function<int(int, int)> func) {
    return func(x, y);
}

int main() {
    // Pass the function as a parameter using its name
    cout << "Addition of 15 and 5: " << execute(15, 5, add) << '\n';
    cout << "Multiplication of 15 and 5: " << execute(15, 5, multiply) << '\n';

    return 0;
}

Output:

Addition of 15 and 5: 20
Multiplication of 15 and 5: 75

3. Using Lambdas : Lambdas in C++ provide a way to create anonymous function objects in place. This is particularly useful when you need a function for a specific task and don’t want to define it elsewhere.

Example:

#include <functional>
#include <iostream>
using namespace std;

// Function that accepts a lambda as a parameter
int execute(int x, int y, function<int(int, int)> func) {
    return func(x, y);
}

int main() {
    // Lambda for addition
    int result1 = execute(15, 5, [](int x, int y) { return x + y; });
    cout << "Addition of 15 and 5: " << result1 << '\n';

    // Lambda for multiplication
    int result2 = execute(15, 5, [](int x, int y) { return x * y; });
    cout << "Multiplication of 15 and 5: " << result2 << '\n';

    return 0;
}

Output:

Addition of 15 and 5: 20
Multiplication of 15 and 5: 75

Signals in C++

Signals are interrupts that prompt an operating system (OS) to halt its current task and give attention to the task that triggered the interrupt. These signals can pause or interrupt processes running on the OS. Similarly, C++ provides several signals that can be caught and handled within a program. Below is a list of common signals and their associated operations in C++.

SignalOperation
SIGINTProduces a receipt for an active signal
SIGTERMSends a termination request to the program
SIGBUSIndicates a bus error (e.g., accessing an invalid address)
SIGILLDetects an illegal instruction
SIGALRMTriggered by the alarm() function when the timer expires
SIGABRTSignals abnormal termination of a program
SIGSTOPCannot be blocked, handled, or ignored; stops a process
SIGSEGVIndicates invalid access to memory (segmentation fault)
SIGFPESignals erroneous arithmetic operations like division by zero
SIGUSR1, SIGUSR2User-defined signals

signal() Function: The signal() function, provided by the signal library, is used to catch and handle unexpected signals or interrupts in a C++ program.

Syntax:

signal(registered_signal, signal_handler);
  • The first argument is an integer that represents the signal number.
  • The second argument is a pointer to the function that will handle the signal.

The signal must be registered with a handler function before it can be caught. The handler function should have a return type of void.

Example:

#include <csignal>
#include <iostream>
using namespace std;

void handle_signal(int signal_num) {
    cout << "Received interrupt signal (" << signal_num << ").\n";
    exit(signal_num);  // Terminate the program
}

int main() {
    // Register SIGABRT and set the handler
    signal(SIGABRT, handle_signal);

    while (true) {
        cout << "Running program..." << endl;
    }

    return 0;
}

Output:

Running program...
Running program...
Running program...

When you press Ctrl+C, which generates an interrupt signal (e.g., SIGABRT), the program will terminate and print:

Received interrupt signal (22).

raise() Function : The raise() function is used to generate signals in a program.

Syntax:

raise(signal);

It takes a signal from the predefined list as its argument.

Example:

// C program to demonstrate the use of relational operators
#include <stdio.h>

int main() {
    int num1 = 12, num2 = 8;

    // greater than
    if (num1 > num2)
        printf("num1 is greater than num2\n");
    else
        printf("num1 is less than or equal to num2\n");

    // greater than or equal to
    if (num1 >= num2)
        printf("num1 is greater than or equal to num2\n");
    else
        printf("num1 is less than num2\n");

    // less than
    if (num1 < num2)
        printf("num1 is less than num2\n");
    else
        printf("num1 is greater than or equal to num2\n");

    // less than or equal to
    if (num1 <= num2)
        printf("num1 is less than or equal to num2\n");
    else
        printf("num1 is greater than num2\n");

    // equal to
    if (num1 == num2)
        printf("num1 is equal to num2\n");
    else
        printf("num1 and num2 are not equal\n");

    // not equal to
    if (num1 != num2)
        printf("num1 is not equal to num2\n");
    else
        printf("num1 is equal to num2\n");

    return 0;
}

Output:

Running program...
Running program...
Running program...
Caught signal (11).