Back to articles
Programming Languages

Modern C++ Features Every Systems Programmer Should Know

From smart pointers to concepts and ranges - master the C++ features that make low-level programming safer and more expressive.

kos1
January 18, 2026
15 min read

Introduction

C++ has evolved dramatically since C++11, introducing features that make the language safer, more expressive, and easier to use while maintaining its performance edge. This guide covers the most impactful modern C++ features every systems programmer should know.

Smart Pointers: Goodbye Manual Memory Management

Modern C++ provides three smart pointer types that automatically manage memory lifetime:

unique_ptr - Exclusive Ownership

#include <memory>

// Old way - manual memory management
void old_way() {
    int* ptr = new int(42);
    // ... code that might throw ...
    delete ptr;  // Might never execute!
}

// Modern way - automatic cleanup
void modern_way() {
    auto ptr = std::make_unique<int>(42);
    // Automatically deleted when scope exits
    // Even if an exception is thrown!
}

shared_ptr - Shared Ownership

auto shared = std::make_shared<Resource>();
auto copy = shared;  // Reference count: 2

// Resource freed when last shared_ptr is destroyed

weak_ptr - Breaking Cycles

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Prevents circular reference
};

Move Semantics: Zero-Cost Transfers

Move semantics allow transferring resources without copying, crucial for performance:

class Buffer {
    std::unique_ptr<char[]> data;
    size_t size;

public:
    // Move constructor - steals resources
    Buffer(Buffer&& other) noexcept
        : data(std::move(other.data))
        , size(other.size) {
        other.size = 0;
    }

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        data = std::move(other.data);
        size = other.size;
        other.size = 0;
        return *this;
    }
};

// Usage
Buffer createBuffer() {
    Buffer buf(1024);
    return buf;  // Moved, not copied!
}

Buffer b = createBuffer();  // Zero copies

Lambda Expressions

Lambdas provide inline, anonymous functions with powerful capture semantics:

std::vector<int> nums = {1, 2, 3, 4, 5};

// Capture by value
int multiplier = 3;
std::transform(nums.begin(), nums.end(), nums.begin(),
    [multiplier](int n) { return n * multiplier; });

// Capture by reference
int sum = 0;
std::for_each(nums.begin(), nums.end(),
    [&sum](int n) { sum += n; });

// Generic lambda (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2);       // int
add(1.5, 2.5);   // double

constexpr: Compile-Time Computation

Move calculations from runtime to compile time:

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

// Computed at compile time!
constexpr int fact5 = factorial(5);  // 120

// C++17: constexpr if
template<typename T>
auto process(T value) {
    if constexpr (std::is_integral_v<T>) {
        return value * 2;
    } else {
        return value + value;
    }
}

Structured Bindings (C++17)

Decompose objects into named variables:

std::map<std::string, int> scores;

// Old way
for (const auto& pair : scores) {
    std::cout << pair.first << ": " << pair.second;
}

// Modern way - structured bindings
for (const auto& [name, score] : scores) {
    std::cout << name << ": " << score;
}

// Works with tuples, pairs, arrays, and custom types
auto [x, y, z] = std::make_tuple(1, 2.0, "three");

std::optional, std::variant, std::any

Type-safe alternatives to null pointers and unions:

// optional - nullable value types
std::optional<int> find_index(const std::string& s, char c) {
    if (auto pos = s.find(c); pos != std::string::npos) {
        return pos;
    }
    return std::nullopt;
}

if (auto idx = find_index("hello", 'l')) {
    std::cout << "Found at: " << *idx;
}

// variant - type-safe union
std::variant<int, double, std::string> value;
value = 42;
value = 3.14;
value = "hello";

std::visit([](auto&& v) { std::cout << v; }, value);

Concepts (C++20)

Constrain templates with readable, reusable requirements:

// Define a concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Use the concept
template<Numeric T>
T add(T a, T b) {
    return a + b;
}

// Or with requires clause
template<typename T>
    requires std::is_arithmetic_v<T>
T multiply(T a, T b) {
    return a * b;
}

add(1, 2);        // OK
add("a", "b");    // Compile error with clear message!

Ranges (C++20)

Composable, lazy algorithms for cleaner code:

#include <ranges>

std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Filter and transform with pipes
auto result = nums 
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n; })
    | std::views::take(3);

// result: 4, 16, 36 (lazy evaluation!)

Conclusion

Modern C++ is a different language from the C++ of the 90s. These features enable writing safer, more expressive code without sacrificing performance. Start with smart pointers and move semantics, then gradually adopt newer features as you become comfortable. The compiler is your ally - use -std=c++20 and embrace the evolution.

Tags

#c++#cpp#modern-cpp#systems-programming