Learning C++ part 4

Table of Contents

Iterators

Iterators provide a uniform abstraction for traversing elements in a container. Different iterator categories define what operations are supported, such as reading values, writing values, or moving forward and backward through a sequence. Each iterator category builds upon the capabilities of the previous one.

Input Iterators

Access: Read-only access to elements (*it behaves as an rvalue).

Traversal: Can only move forward using ++it.

Passes: Single-pass. Once incremented, the previous position cannot be reliably revisited.

Example: std::istream_iterator

Output Iterators

Access: Write-only access to elements (*it behaves as an lvalue).

Traversal: Can only move forward using ++it.

Passes: Single-pass. Values written cannot be read back through the iterator.

Example: std::ostream_iterator

Forward Iterators

Access: Support both reading and writing (*it can act as both rvalue and lvalue).

Traversal: Can move forward using ++it.

Passes: Multi-pass. Multiple copies of the iterator can traverse the same range independently.

Containers: Used by containers such as std::forward_list.

Bidirectional Iterators

Access: All capabilities of forward iterators (read/write, multi-pass).

Traversal: Can move forward ( ++it) and backward ( --it).

Containers: Used by containers such as std::list, std::set, and std::map.

Random Access Iterators

Access: All capabilities of bidirectional iterators.

Traversal: Support constant-time jumps using pointer arithmetic such as it + n, it - n, it += n, and it -= n.

Comparison: Support full ordering comparisons (<, >, <=, >=).

Containers: Used by containers like std::vector, std::deque, and std::array.

Note: Raw pointers are also considered random access iterators.

1#include <vector>
2#include <iostream>
3
4int main() {
5    std::vector<int> v = {1, 2, 3, 4};
6
7    for (auto it = v.begin(); it != v.end(); ++it) {
8        std::cout << *it << " ";
9    }
10}

mutable keyword

In C++, the mutable keyword in a lambda expression allows modification of variables that were captured by value. By default, lambdas treat captured-by-value variables as read-only.

Under the hood, every lambda is compiled into an unnamed class (called aclosure type) with data members representing the captured variables.

Without mutable:
The compiler generates the lambda's operator()as a const member function. This means captured-by-value variables cannot be modified inside the lambda, because they are treated as const.

With mutable:
Adding mutable removes theconst qualifier fromoperator(). This allows the lambda to modify its internal copies of captured variables.

Important: These modifications only affect the lambda’s internal copy. The original variables in the outer scope remain unchanged.

mutable vs. capture by reference:
If you need to modify the original variable, capture it by reference using&x. In that case,mutable is not needed, since you are directly modifying the external variable.

1#include <iostream>
2
3int main() {
4    int n = 0;
5
6    // Mutable lambda modifies its own copy of n
7    auto incrementer = [n]() mutable {
8        std::cout << "Inner n: " << ++n << std::endl;
9    };
10
11    incrementer(); // Inner n: 1
12    incrementer(); // Inner n: 2
13    incrementer(); // Inner n: 3
14
15    std::cout << "Outer n: " << n << std::endl; // Outer n: 0
16
17    return 0;
18}

Key takeaway:mutable allows a lambda to maintain and modify its own internal state when capturing variables by value, without affecting the original variables.

Singleton

1void functionality() {
2    static MyClass instance;
3}

In C++11 and later, function-local static variables are initialized in a thread-safe manner. If multiple threads reach the declaration at the same time, the language guarantees that only one thread performs the initialization, while the others block until it is complete.

After initialization finishes, all threads will observe a fully constructed object. However, this guarantee only applies to the initialization phase — it does not make subsequent access to the object thread-safe. Any further synchronization must be handled manually if the object is mutable.

1#include <atomic>
2#include <mutex>
3#include <new>
4
5static std::atomic<bool> _is_initialized{false};
6static std::mutex _init_mutex;
7alignas(MyClass) static unsigned char _instance_buffer[sizeof(MyClass)];
8
9void functionality() {
10    if (!_is_initialized.load(std::memory_order_acquire)) {
11        std::lock_guard<std::mutex> lock(_init_mutex);
12
13        if (!_is_initialized.load(std::memory_order_relaxed)) {
14            new (_instance_buffer) MyClass(); // placement new
15            _is_initialized.store(true, std::memory_order_release);
16        }
17    }
18
19    MyClass* ptr = reinterpret_cast<MyClass*>(_instance_buffer);
20}

Double-checked locking (fast path): The first check avoids acquiring the mutex once initialization is complete. This makes subsequent calls very fast, requiring only a single atomic load.

Memory ordering:

Fast path (double-checked locking): The first check !_is_initialized.load(std::memory_order_acquire) acts as a fast path. If the object is already initialized, the function skips acquiring the mutex entirely. This makes subsequent calls extremely fast, requiring only a single atomic load.

Acquire semantics (first load): Using memory_order_acquire ensures that if the flag is observed as true, then all memory writes that happened before the corresponding release (i.e., the full construction of the object) are also visible to the current thread.

Relaxed ordering (second load): The second check inside the mutex uses memory_order_relaxed because the mutex already guarantees mutual exclusion. Since only one thread can enter this critical section at a time, no additional memory ordering is required.

Release semantics (store): Usingmemory_order_release when setting the flag guarantees that the construction ofMyClass is fully completed before the flag becomes visible to other threads. Any thread that later observes the flag as true(via acquire) will see a fully initialized object.

friend function

A friend function in C++ is a non-member function that is granted access to the private and protected members of a class. It is declared inside the class using the friend keyword, but it is not a member of the class and therefore does not have a this pointer.

A common use case is operator overloading, especially for operators like <<, >>, +, and==, where the left-hand operand is not the class itself.

1class Vector {
2public:
3    double x, y;
4
5    // ❌ Member function — leads to awkward usage
6    std::ostream& operator<<(std::ostream& os) {
7        os << x << ", " << y;
8        return os;
9    }
10};

The issue with making operator<< a member function is that member operators require the left operand to be the calling object. This would force usage like v << std::cout, which is unnatural.

In practice, we want std::cout << p. Since we cannot modify std::ostream, we implement the operator as a non-member function and declare it as a friend to allow access to private members.

1class Point {
2private:
3    int x, y;
4
5public:
6    Point(int x, int y) : x(x), y(y) {}
7
8    // ✅ Friend function — enables: std::cout << p
9    friend std::ostream& operator<<(std::ostream& out, const Point& p) {
10        out << "(" << p.x << ", " << p.y << ")";
11        return out;
12    }
13};
14
15int main() {
16    Point p(3, 4);
17    std::cout << p << std::endl;  // Output: (3, 4)
18}

The expression std::cout << p is simply syntactic sugar for:

1operator<<(std::cout, p);

This works because the compiler finds a matching function with the signature operator<<(std::ostream&, const Point&). The return type is std::ostream&, which allows chaining:

1operator<<(operator<<(std::cout, p), std::endl);

Key properties of friend functions: They are not member functions and do not have a this pointer. They cannot be called using . or -> on an object. They can be defined inside the class body (which makes them implicitly inline) or outside like a regular function. Even when defined inside the class, they belong to the enclosing namespace rather than the class itself.

Argument-Dependent Lookup (ADL): Friend functions defined inside a class are often “hidden” from normal name lookup. However, they are still found by ADL when at least one argument is of the associated class type. This is why std::cout << p works without explicitly qualifying the function name.

In summary, friend functions provide a controlled way to grant external functions access to a class's internals. They are especially useful for implementing non-member operators while maintaining natural syntax and preserving encapsulation.

member function

Every non-static member function in C++ implicitly receives a hidden pointer called this, which refers to the object that invoked the function. For a call like obj.method(arg), the compiler conceptually translates it to ClassName::method(&obj, arg).

For a class X, the type of this is X* const, meaning it is a constant pointer to the object. If the member function is declared as const, the type becomes const X* const, which prevents modification of the object's data members through that function.

The this pointer is commonly used to disambiguate between member variables and parameters when they share the same name, such as this->name = name;.

It is also frequently used to return the current object from a function (via *this) to enable method chaining, for example obj.doA().doB();.

In operator overloading, particularly assignment operators,this is used to detect self-assignment, such asif (this != &other), which helps avoid unnecessary work or potential bugs.

Additionally, this can be passed to other functions or stored in data structures when an object needs to reference itself, enabling patterns such as callbacks or self-referential designs.