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.

CRTP (Curiously Recurring Template Pattern)

CRTP is a C++ pattern where a class inherits from a template instantiated with itself. This enables compile-time polymorphism, allowing the compiler to resolve function calls without virtual dispatch or runtime branching.

1template <typename Derived>
2class Base {
3public:
4    void interface() {
5        // Calls implementation defined in Derived
6        static_cast<Derived*>(this)->implementation();
7    }
8};
9
10class Derived : public Base<Derived> {
11public:
12    void implementation() {
13        // actual logic
14    }
15};

The base class uses static_cast<Derived*>(this)to call methods implemented in the derived class. Since the compiler knows the exact type at compile time, there is no virtual table lookup.

1// What the compiler ACTUALLY generates - conceptually
2class Base_For_Derived { // unique class per Derived
3public:
4    void interface() {
5        static_cast<Derived*>(this)->implementation();
6    }
7};
8
9// compiler knows EXACTLY what Derived is
10// No runtime polymorphism involved.

Virtual vs CRTP

1// Runtime polymorphism (virtual)
2class VirtualAnimal {
3public:
4    virtual void speak() {}
5    int age;
6    // contains hidden vptr
7};
8
9// Compile-time polymorphism (CRTP)
10template <typename Derived>
11class CRTPAnimal {
12public:
13    int age;
14    // no vptr
15};
16
17class Dog : public CRTPAnimal<Dog> {};

Virtual functions require a vtable and introduce an extra pointer (vptr), increasing memory usage and preventing some compiler optimizations. CRTP removes this overhead entirely.

CRTP size comparison

Problem: Runtime Branching

1#include <iostream>
2using namespace std;
3
4enum class CharacterClass { WARRIOR, MAGE, ARCHER };
5
6class DamageCalculator {
7private:
8    CharacterClass cls;
9
10public:
11    DamageCalculator(CharacterClass c) : cls(c) {}
12
13    double calculateDamage(double baseDamage) {
14        // Checked EVERY call
15        if (cls == CharacterClass::WARRIOR) {
16            return baseDamage * 1.50;
17        } else if (cls == CharacterClass::MAGE) {
18            return baseDamage * 1.80;
19        } else if (cls == CharacterClass::ARCHER) {
20            return baseDamage * 1.20;
21        }
22        return 0.0;
23    }
24};

Even if the character type never changes, the program performs conditional checks on every call. In tight loops (e.g. game ticks or trading systems), these branches can hurt performance due to misprediction.

CRTP Solution (Zero Branching)

1#include <iostream>
2using namespace std;
3
4template <typename Derived>
5class DamageCalculator {
6public:
7    double calculateDamage(double baseDamage) {
8        return static_cast<Derived*>(this)->damageImpl(baseDamage);
9    }
10
11    double calculateCritical(double baseDamage) {
12        return static_cast<Derived*>(this)->criticalImpl(baseDamage);
13    }
14};
15
16class WarriorDamage : public DamageCalculator<WarriorDamage> {
17public:
18    double damageImpl(double base) {
19        return base * 1.50;
20    }
21
22    double criticalImpl(double base) {
23        return base * 2.00;
24    }
25};
26
27class MageDamage : public DamageCalculator<MageDamage> {
28public:
29    double damageImpl(double base) {
30        return base * 1.80;
31    }
32
33    double criticalImpl(double base) {
34        return base * 2.50;
35    }
36};
37
38class ArcherDamage : public DamageCalculator<ArcherDamage> {
39public:
40    double damageImpl(double base) {
41        return base * 1.20;
42    }
43
44    double criticalImpl(double base) {
45        return base * 1.75;
46    }
47};
48
49int main() {
50    WarriorDamage warrior;
51
52    for (int i = 0; i < 1000000; i++) {
53        warrior.calculateDamage(100.0);
54    }
55}

The compiler knows the exact type (WarriorDamage) at compile time. This removes all branching and enables function inlining.

What Happens Under the Hood

1// With branching:
2cmp [cls], WARRIOR
3je warrior_branch
4cmp [cls], MAGE
5je mage_branch
6// multiple comparisons and jumps
7
8// With CRTP:
9mulsd xmm0, 1.50
10// direct computation, no branches

CRTP moves decision-making from runtime to compile time. Instead of checking conditions repeatedly, the compiler generates specialized code paths that are faster and more predictable.

Key Takeaways

• CRTP enables compile-time (static) polymorphism
• No vtable or virtual dispatch
• Eliminates runtime branching
• Allows aggressive inlining and optimization
• Common in high-performance systems (e.g. game engines, low-latency trading)

Typedef enum vs Scoped enum

Enumerations (enums) are user-defined types that represent a fixed set of named constants. They improve code readability by replacing "magic numbers" with meaningful names. C and C++ provide several ways to define enums, with modern C++ introducing scoped enumerations that address many of the limitations of traditional enums.

Typedef enum

In C, an enumeration type must normally be referenced using the enum keyword whenever a variable is declared. The typedef keyword can be used together with an enum definition to create an alias for the type, allowing it to be referenced without the enum prefix.

The enumeration itself defines a set of named integral constants, while typedef provides a shorter and more convenient name for the type. This pattern is commonly used in C codebases to improve readability and reduce verbosity.

Without typedef:

1enum Color
2{
3    RED,
4    GREEN,
5    BLUE
6};
7
8enum Color myColor = RED;

With typedef:

1typedef enum
2{
3    RED,
4    GREEN,
5    BLUE
6} Color;
7
8Color myColor = RED;

It is important to note that typedef enum does not change the behavior of the enumeration itself. Enumerators are still represented as integers and can be implicitly converted to integral types. The only difference is the introduction of a more convenient type name.

Traditional (unscoped) enums in C++

Traditional C++ enums behave similarly to C enums. Enumerator names are injected directly into the surrounding scope and can be implicitly converted to integers. While convenient, this can lead to name collisions and unintended comparisons between unrelated enumeration types.

1enum Color
2{
3    red,
4    blue,
5};
6
7enum Fruit
8{
9    banana,
10    apple,
11};
12
13Color color { red };
14Fruit fruit { banana };
15
16if (color == fruit)
17{
18    std::cout << "color and fruit are equal\n";
19}
20else
21{
22    std::cout << "color and fruit are not equal\n";
23}

In the example above, the compiler does not know how to directly compare a Color and a Fruit. Because traditional enums can be implicitly converted to integers, both values are converted to their underlying integer representations before the comparison is performed. Since red and banana both have the value 0, the comparison evaluates to true. Although technically valid, this comparison is semantically meaningless because the two values belong to completely different enumerations.

Scoped enums (enum class)

Scoped enumerations, introduced in C++11 through enum class, were designed to solve the shortcomings of traditional enums. Enumerators remain within the scope of the enumeration, preventing naming conflicts, and values are not implicitly converted to integers.

1enum class Status
2{
3    OK,
4    Error
5};
6
7// Error: no implicit conversion to int
8// int code = Status::OK;
9
10// Correct: explicit conversion
11int code = static_cast<int>(Status::OK);

Because scoped enums do not implicitly convert to integers, accidental comparisons and conversions are prevented at compile time. This provides stronger type safety and makes code easier to reason about.

1enum class Color
2{
3    red,
4    blue,
5};
6
7Color color { Color::blue };
8
9// Error: no implicit conversion to int
10// std::cout << color << '\n';
11
12std::cout << static_cast<int>(color) << '\n';
13
14// C++23
15std::cout << std::to_underlying(color) << '\n';

When working with scoped enums, an explicit conversion is required whenever the underlying integer value is needed. Prior to C++23, this was typically done using static_cast. C++23 introduced std::to_underlying, which provides a more expressive and type-safe way to obtain the underlying integral value.

When should each be used?

In modern C++, enum class should generally be preferred because it provides stronger type safety, avoids namespace pollution, and prevents unintended implicit conversions. Traditional enums are still useful when interoperability with C APIs is required or when implicit integer conversions are intentionally desired. The use of typedef enum is primarily a C-language convention and is rarely needed in modern C++ code.

User-defined conversion operators

User-defined conversion operators allow an object of a class to be converted into another type. They are special member functions that define how a class should behave when a conversion to a different type is requested. This can make custom types integrate more naturally with built-in types and other libraries.

A conversion operator has a unique syntax. Unlike ordinary member functions, it does not specify a return type and does not accept any parameters. Instead, the target type appears directly in the operator's name.

1class MyInt
2{
3    int value;
4
5public:
6    MyInt(int v) : value(v) {}
7
8    // Conversion operator to int
9    operator int() const
10    {
11        return value;
12    }
13};
14
15MyInt obj(42);
16
17int x = obj; // Implicit conversion
18

In the example above, the compiler automatically invokes operator int() when a value of type MyInt is assigned to an int. The object behaves similarly to the underlying primitive type, making the conversion seamless for the caller.

Rules and restrictions

Conversion operators must be defined as non-static member functions. They cannot specify an explicit return type because the target type is already encoded in the operator name itself. Conversion operators also cannot accept parameters. A class may define conversion operators for many different types, including pointers and references. However, conversion to array types or function types is not allowed.

Implicit vs explicit conversions

By default, conversion operators participate in implicit conversions. While convenient, this can sometimes produce unexpected behavior because objects may be converted automatically without the programmer's intent. To address this issue, C++11 introduced the explicit keyword for conversion operators.

1class Distance
2{
3    double meters;
4
5public:
6    explicit operator double() const
7    {
8        return meters;
9    }
10};
11
12Distance d;
13
14// double value = d; // Error: implicit conversion not allowed
15
16double value = static_cast<double>(d); // OK
17

Because the conversion operator is marked with explicit, the compiler will not perform the conversion automatically. Instead, the programmer must explicitly request the conversion using a cast such as static_cast. This makes potentially dangerous conversions more visible and easier to reason about.

Conversion operators and operator overloading

Conversion operators are often used together with operator overloading. Without them, a wrapper type may require explicit accessor functions every time its underlying value is needed. A carefully designed conversion operator can make a class feel more natural to use while still preserving type safety.

Common use cases

One of the most common uses of conversion operators is implementing operator bool(). Types such as file streams and smart pointers use this pattern to indicate whether they are currently in a valid state.

1std::ifstream file("data.txt");
2
3if (file)
4{
5    std::cout << "File opened successfully\n";
6}

Another common use case is wrapper types. Classes that encapsulate primitive values can provide conversion operators to make the wrapper behave similarly to the underlying type.

1class Integer
2{
3    int value;
4
5public:
6    Integer(int v) : value(v) {}
7
8    operator int() const
9    {
10        return value;
11    }
12};
13
14Integer num(10);
15
16int x = num;
17

Although conversion operators can improve usability, they should be used carefully. Excessive implicit conversions can make code harder to understand and may introduce subtle bugs. In modern C++, it is generally recommended to prefer explicit conversion operators unless implicit conversion is clearly safe and intuitive.