Modern C++ Features: A Comprehensive Guide

Introduction to Modern C++ (C++11/14/17/20)

This guide documents key features from the Modern C++ Tutorial: Quick Start with C++11/14/17/20.

Chapter 2: Core Language Enhancements

Compile-time Constants with constexpr

The constexpr keyword allows expressions to be evaluated at compile time, enabling optimizations.

constexpr int fibonacci_sequence(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci_sequence(n-1) + fibonacci_sequence(n-2);
}

Enhanced Conditional Statements

Variables declared within conditional statements can be used directly within their scopes.

// Declare iterator directly in if condition
if (const std::vector<int>::iterator position = std::find(data_collection.begin(), data_collection.end(), target_value);
    position != data_collection.end()) {
    *position = replacement_value;
}

Automatic Type Deduction with auto

The auto keyword automatically deduces variable types at declaration time (not applicable to function parameters).

auto numerical_value = 42;              // Deduced as int
auto dynamic_array = new auto(15);     // Deduced as int*

Type Inquiry with decltype

decltype determines the type of an expression or variable, providing type information without auto's initialization requirement.

auto first_number = 10;
auto second_number = 20;
decltype(first_number + second_number) result;

Trailing Return Type Syntax

Function return types can be specified after the parameter list, allowing for more complex type declarations.

template<typename Type1, typename Type2>
auto calculate_sum(Type1 x, Type2 y) -> decltype(x + y) {
    return x + y;
}

Range-based For Loops

Simplified iteration over container elements, similar to other modern languages.

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
    std::vector<int> numbers = {1, 2, 3, 4};
    if (auto iterator = std::find(numbers.begin(), numbers.end(), 3); iterator != numbers.end()) 
        *iterator = 4;
    
    // Read-only iteration
    for (auto element : numbers)
        std::cout << element << std::endl;
        
    // Modifiable iteration
    for (auto &element : numbers) {
        element += 1;
    }
    
    // Read-only iteration after modification
    for (auto element : numbers)
        std::cout << element << std::endl;
}

Strongly-typed Enums

Enum classes prevent namespace pollution and provide type safety for enum values.

enum class color_code : unsigned int {
    red,
    green,
    blue = 100,
    yellow = 100
};

Chapter 3: Advanced Language Features

Lambda Expressions

Anonymous functions that can capture variables from their enclosing scope.

[capture_list](parameters) mutable(exception_specification) -> return_type {
    // function body
}

The capture list allows passing external variables into the lambda. There are two capture methods:

Value Capture: Copies the variable into the lambda.

void demonstrate_value_capture() {
    int value = 1;
    auto capture_by_value = [value] {
        return value;
    };
    value = 100;
    auto stored_value = capture_by_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // At this point, stored_value == 1, while value == 100
    // The lambda preserved a copy of the original value
}

Reference Capture: Creates a reference to the original variable.

void demonstrate_reference_capture() {
    int value = 1;
    auto capture_by_reference = [&value] {
        return value;
    };
    value = 100;
    auto stored_value = capture_by_reference();
    std::cout << "stored_value = " << stored_value << std::endl;
    // At this point, stored_value == 100, same as value
    // The lambda references the original variable
}

Lvalue and Rvalue References

Lvalues (left values) are persistent objects that exist beyond a single expression. For example:

int x = 10 + 5;  // x is an lvalue

Rvalues (right values) are temporary objects that cease to exist after an expression. For example:

std::vector<int> generate_data();
std::vector<int> data = generate_data();  // The return value is temporary

Prvalues (pure rvalues) are either literal values (like 10, true) or temporary objects (like 1+2).

Xvalues (expiring values) are objects that are about to be destroyed but can be moved, introduced in C++11 to support move semantics.

Example demonstrating reference types:

#include <iostream>
#include <string>
void process_reference(std::string& str) {
    std::cout << "Lvalue reference" << std::endl;
}
void process_reference(std::string&& str) {
    std::cout << "Rvalue reference" << std::endl;
}
int main()
{
    std::string left_value = "Hello, ";
    // std::string&& r1 = left_value; // Illegal: rvalue reference cannot bind to lvalue
    std::string&& right_value = std::move(left_value); // Legal: std::move converts lvalue to rvalue
    std::cout << right_value << std::endl; // Hello,
    
    const std::string& extended_left_value = left_value + left_value; // Legal: const lvalue extends lifetime
    // extended_left_value += " World"; // Illegal: const reference cannot be modified
    
    std::string&& extended_right_value = left_value + extended_left_value; // Legal: rvalue extends lifetime
    extended_right_value += " World"; // Legal: non-const reference can modify temporary
    std::cout << extended_right_value << std::endl; // Hello, Hello, World
    
    process_reference(extended_right_value); // Outputs Lvalue reference
    return 0;
}

Move Semantics

Move semantics allow efficient transfer of resources from temporary objects rather than expensive copying.

#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main() {
    std::string original = "Resource-intensive data";
    std::vector<std::string> container;
    
    // Uses push_back(const T&), creating a copy
    container.push_back(original);
    std::cout << "original: " << original << std::endl;
    
    // Uses push_back(T&&), moving resources without copying
    // After this operation, original becomes empty
    container.push_back(std::move(original));
    std::cout << "original after move: " << original << std::endl;
    return 0;
}

Chapter 4: Container Improvements

Fixed-size Arrays (std::array)

Unlike std::vector, std::array has a fixed size and doesn't automatically shrink when elements are removed.

std::array<int, 4> arr = {1, 2, 3, 4};
arr.empty(); // Check if container is empty
arr.size();  // Return number of elements
// Iterator support
for (auto &element : arr)
{
    // Process element
}
// Sort with lambda expression
std::sort(arr.begin(), arr.end(), [](int a, int b) {
    return b < a;
});
// Array size must be compile-time constant
constexpr int size = 4;
std::array<int, size> arr = {1, 2, 3, 4};
// Unlike C-style arrays, std::array doesn't decay to T*
// int *arr_ptr = arr; // Illegal

Unordered Containers

std::unordered_map and std::unordered_set use hash tables instead of binary trees, providing O(1) average complexity for insertions and lookups when element order isn't important.

#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
int main() {
    // Initialize with same order
    std::unordered_map<int, std::string> unordered_container = {
        {1, "First"},
        {3, "Third"},
        {2, "Second"}
    };
    std::map<int, std::string> ordered_container = {
        {1, "First"},
        {3, "Third"},
        {2, "Second"}
    };
    
    // Iterate through both containers
    std::cout << "std::unordered_map" << std::endl;
    for( const auto & entry : unordered_container)
        std::cout << "Key:[" << entry.first << "] Value:[" << entry.second << "]\n";
    std::cout << std::endl;
    std::cout << "std::map" << std::endl;
    for( const auto & entry : ordered_container)
        std::cout << "Key:[" << entry.first << "] Value:[" << entry.second << "]\n";
}

Tuples

Tuples store elements of different types with utilities for creation, access, and unpacking.

#include <tuple>
#include <iostream>
auto create_person(int id)
{
    // Return type inferred as std::tuple<double, char, std::string>
    if (id == 0)
        return std::make_tuple(3.8, 'A', "Alice");
    if (id == 1)
        return std::make_tuple(2.9, 'C', "Bob");
    if (id == 2)
        return std::make_tuple(1.7, 'D', "Charlie");
    return std::make_tuple(0.0, 'D', "Unknown");
    // Returning just 0 would cause inference error
}
int main()
{
    auto person = create_person(0);
    std::cout << "ID: 0, "
    << "GPA: " << std::get<0>(person) << ", "
    << "Grade: " << std::get<1>(person) << ", "
    << "Name: " << std::get<2>(person) << '\n';
    
    double gpa;
    char grade;
    std::string name;
    // Tuple unpacking
    std::tie(gpa, grade, name) = create_person(1);
    std::cout << "ID: 1, "
    << "GPA: " << gpa << ", "
    << "Grade: " << grade << ", "
    << "Name: " << name << '\n';
}

Chapter 5: Smart Pointers

Smart pointers (std::shared_ptr, std::unique_ptr, std::weak_ptr) are defined in the header.

std::shared_ptr

Shared pointers maintain reference counts and automatically delete objects when no references remain.

#include <iostream>
#include <memory>
void increment_value(std::shared_ptr<int> counter)
{
    (*counter)++;
}
int main()
{
    // auto pointer = new int(10); // Illegal: no direct assignment
    // Create a std::shared_ptr
    auto pointer = std::make_shared<int>(10);
    increment_value(pointer);
    std::cout << *pointer << std::endl; // 11
    // The shared_ptr will be automatically destroyed when leaving scope
    return 0;
}

Shared pointer management methods:

auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // Reference count increases
auto pointer3 = pointer; // Reference count increases
int *raw_ptr = pointer.get(); // Doesn't increase reference count
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "after reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "after reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0

std::unique_ptr

Unique pointers enforce exclusive ownership of an object and can be transferred using move semantics.

std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique available since C++14
// std::unique_ptr<int> pointer2 = pointer; // Illegal: copy constructor deleted

#include <iostream>
#include <memory>
struct Data {
    Data() { std::cout << "Data::Data" << std::endl; }
    ~Data() { std::cout << "Data::~Data" << std::endl; }
    void process() { std::cout << "Data::process" << std::endl; }
};
void process_data(const Data &) {
    std::cout << "process_data(const Data&)" << std::endl;
}
int main() {
    std::unique_ptr<Data> p1(std::make_unique<Data>());
    // p1 is not empty, output
    if (p1) p1->process();
    {
        std::unique_ptr<Data> p2(std::move(p1));
        // p2 is not empty, output
        process_data(*p2);
        // p2 is not empty, output
        if(p2) p2->process();
        // p1 is empty, no output
        if(p1) p1->process();
        p1 = std::move(p2);
        // p2 is empty, no output
        if(p2) p2->process();
        std::cout << "p2 is destroyed" << std::endl;
    }
    // p1 is not empty, output
    if (p1) p1->process();
    // Data instance will be destroyed when leaving scope
}

Chapter 7: Concurrency and Parallelism

Threads

std::thread creates new threads, with join() waiting for thread completion.

#include <iostream>
#include <thread>
int main() {
    std::thread worker([](){
        std::cout << "Worker thread executing." << std::endl;
    });
    worker.join();
    return 0;
}

Mutexes and Critical Sections

std::mutex provides basic mutual exclusion, but requires careful handling to avoid deadlocks. std::lock_guard offers RAII-style locking.

#include <iostream>
#include <thread>
int shared_value = 1;
void update_value(int new_value) {
    static std::mutex value_mutex;
    std::lock_guard<std::mutex> lock(value_mutex);
    // Perform critical operation
    shared_value = new_value;
    // Mutex is automatically released when leaving scope
}
int main() {
    std::thread t1(update_value, 2), t2(update_value, 3);
    t1.join();
    t2.join();
    std::cout << shared_value << std::endl;
    return 0;
}

std::unique_lock offers more flexibility with manual lock management.

#include <iostream>
#include <thread>
int shared_value = 1;
void update_value(int new_value) {
    static std::mutex value_mutex;
    std::unique_lock<std::mutex> lock(value_mutex);
    // Perform critical operation
    shared_value = new_value;
    std::cout << shared_value << std::endl;
    // Release lock manually
    lock.unlock();
    // Other threads can access shared_value during this period
    // Acquire lock again for another critical section
    lock.lock();
    shared_value += 1;
    std::cout << shared_value << std::endl;
}
int main() {
    std::thread t1(update_value, 2), t2(update_value, 3);
    t1.join();
    t2.join();
    return 0;
}

Condition Variables

Condition variables enable thread synchronization based on state changes, useful for producer-consumer patterns.

#include <queue>
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream>
#include <condition_variable>
int main() {
    std::queue<int> data_queue;
    std::mutex queue_mutex;
    std::condition_variable data_condition;
    bool notification_flag = false;  // Notification state
    
    // Producer thread
    auto producer = [&]() {
        for (int i = 0; ; i++) {
            std::this_thread::sleep_for(std::chrono::milliseconds(900));
            std::unique_lock<std::mutex> lock(queue_mutex);
            std::cout << "Producing " << i << std::endl;
            data_queue.push(i);
            notification_flag = true;
            data_condition.notify_all(); // Can also use notify_one
        }
    };
    
    // Consumer thread
    auto consumer = [&]() {
        while (true) {
            std::unique_lock<std::mutex> lock(queue_mutex);
            while (!notification_flag) {  // Prevent spurious wakeups
                data_condition.wait(lock);
            }
            // Briefly unlock to allow producer to continue while consumer processes
            lock.unlock();
            std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Consumer slower than producer
            lock.lock();
            while (!data_queue.empty()) {
                std::cout << "Consuming " << data_queue.front() << std::endl;
                data_queue.pop();
            }
            notification_flag = false;
        }
    };
    
    // Run in separate threads
    std::thread p(producer);
    std::thread cs[2];
    for (int i = 0; i < 2; ++i) {
        cs[i] = std::thread(consumer);
    }
    p.join();
    for (int i = 0; i < 2; ++i) {
        cs[i].join();
    }
    return 0;
}

Conclusion

Modern C++ features significantly enhance code expressiveness and performance. The most important concepts to master are smart pointers and lambda expressions, which are prevalent in contemporary C++ codebases.

Tags: C++11 c++14 C++17 C++20 smart-pointers

Posted on Mon, 18 May 2026 16:40:13 +0000 by keyurshah