Notifications

No notifications

/Phase 4

Modern C++ — auto, lambdas, optional, ranges, concepts

C++11 → C++23 in One Topic 🚀

Modern C++ removes huge classes of bugs and noise. If your code looks like 1998, you're working too hard.

auto — Let the Compiler Type for You

auto i      = 42;                    // int
auto pi     = 3.14;                  // double
auto name   = std::string("Asha");   // string (NOT const char*)
auto it     = v.begin();             // ::iterator (long type!)

for (const auto& [k, v] : map) /* ... */; // structured bindings

> Use auto when the type is obvious from the right side or genuinely cumbersome to spell. Don't hide important types from readers.

Range-Based for

for (int x : v)            cout << x;     // copy
for (int& x : v)            x *= 2;        // mutate
for (const auto& x : v)     /* ... */;     // const-ref (default for non-trivials)

Lambdas

auto add = [](int a, int b) { return a + b; };
add(2, 3);

int n = 10; auto by_n = [n](int x) { return x % n == 0; }; // capture by value

auto count = 0; for_each(v.begin(), v.end(), [&count](int x) { if (x > 0) ++count; }); // capture by reference

std::optional — "Maybe a Value"

optional<int> parse_int(string_view s) {
    try { return stoi(string(s)); }
    catch (...) { return nullopt; }
}

if (auto x = parse_int("42")) cout << *x; else cout << "no number";

int v = parse_int("oops").value_or(-1);

On this page

Detailed Theory

Modern C++ (C++11 and later) layered in features that look small but transform daily code. Here's the survival pack.

auto, Type Deduction & decltype

auto x = 42;                     // int
auto y = 3.14f;                  // float
auto s = "hello"s;               // std::string (with literal suffix)

decltype(x) y = x; // y has same type as x decltype(auto) z = func(); // preserve reference-ness exactly

> Rule: auto is great for *iterators*, *lambdas*, *complex template results*. Resist for short numeric/string types if it hides intent.

Range-Based for & Structured Bindings (C++17)

map<string, int> m{{"a", 1}, {"b", 2}};
for (const auto& [k, v] : m) cout << k << "=" << v << "\n";

auto [q, r] = div(17, 5); // q = 3, r = 2 auto [it, inserted] = set.insert(x); // pair returned

Lambda Expressions

[capture-list](params) -> return_type { body }

CaptureMeaning
[]None
[x]x by value
[&x]x by reference
[=]All used by value
[&]All used by reference
[this]Member access
[x, &y]mixed

sort(v.begin(), v.end(),
     [](const auto& a, const auto& b) { return a.score > b.score; });

auto make_counter = []() { int n = 0; return [n]() mutable { return ++n; }; }; auto next = make_counter(); next(); next(); next(); // 1, 2, 3

C++14 added generic lambdas (auto params); C++20 added template lambdas ().

nullptr — Not NULL, Not 0

int* p = nullptr;            // ✅
if (p) /* never */;
// NULL is just (int)0 — causes overload-resolution bugs.

Smart Pointers (recap)

PointerUse
unique_ptrSingle owner
shared_ptrMulti-owner ref-counted
weak_ptrNon-owning observer (break cycles)

Always make_unique / make_shared rather than new.

Move Semantics, std::move, Rvalue References

C++11 introduced move semantics so we can transfer ownership of large resources without copying.

vector<int> a(1'000'000);
vector<int> b = std::move(a);       // O(1) — steals a's buffer

string greet(string name) { return "Hello, " + std::move(name); }

After std::move(a), the moved-from object is in a *valid but unspecified* state — you can assign to it or destroy it, but don't read it.

class Buffer {
    vector<int> data;
public:
    Buffer(Buffer&& other) noexcept : data(std::move(other.data)) {}
    Buffer& operator=(Buffer&&) noexcept = default;
};

> Compiler synthesises good defaults — Rule of Zero remains the goal.

std::optional (C++17)

A type that either holds a T or holds nothing — replaces sentinel values like -1 / nullptr / empty string.

optional<int> find_age(string_view name);

if (auto age = find_age("Asha")) cout << *age; else cout << "unknown";

int safe = find_age("Bob").value_or(0);

std::variant (C++17)

Tagged union — type-safe one of these types:

variant<int, string, double> v = 42;
v = "hello";                      // now a string

visit([](const auto& x){ cout << x; }, v);

if (holds_alternative<int>(v)) /* ... */;

std::string_view (C++17)

A non-owning view into a string — no allocation, no copy. Use it for parameters when you only need to read:

size_t count_vowels(string_view s) {
    size_t n = 0;
    for (char c : s) if (string_view{"aeiou"}.contains(c)) ++n;
    return n;
}

count_vowels("hello"); // works on literals count_vowels(my_string); // works on string

> Don't store a string_view longer than the string it points to — that's dangling.

std::span (C++20)

Like string_view, but for arbitrary contiguous ranges (vector, array, raw arrays). Use it for any function that reads/writes a sequence.

int sum(span<const int> s) {
    int total = 0;
    for (int x : s) total += x;
    return total;
}

vector<int> v{1,2,3}; int arr[] = {4,5,6}; sum(v); // ok sum(arr); // ok

C++20 Concepts

Already met — let you constrain template parameters readably.

#include <concepts>

template <std::integral T> T half(T x) { return x / 2; }

template <typename T> concept Hashable = requires(T t) { std::hash<T>{}(t); };

Ranges (C++20)

#include <ranges>
namespace rv = std::views;
namespace ra = std::ranges;

auto pipe = v | rv::filter([](int x){ return x % 2 == 0; }) | rv::transform([](int x){ return x * x; }) | rv::take(3);

ra::sort(v); ra::find(v, 5);

std::format (C++20)

Type-safe modern formatting:

#include <format>
cout << format("Name: {}, Age: {}\n", name, age);
cout << format("{:8.2f}\n", 3.14159);

Designated Initialisers (C++20)

struct Config { int port = 8080; bool debug = false; };
Config c{ .port = 3000, .debug = true };

Modules (C++20)

A replacement for header files. Faster compiles, real encapsulation. Compiler/build-tool support is still maturing — adopt cautiously.

// math.cppm
export module math;
export int add(int a, int b) { return a + b; }

// main.cpp import math; int main() { return add(2, 3); }

if constexpr (C++17) — Compile-Time Branch

template <typename T>
auto getValue(T x) {
    if constexpr (std::is_pointer_v<T>) return *x;
    else                                 return x;
}

The dead branch is not even compiled, which means it can use type-specific operations.

constexpr, consteval, constinit

KeywordMeaning
constexpr"Can be evaluated at compile time"
consteval (C++20)"MUST be evaluated at compile time"
constinit (C++20)"Static initialisation must be a compile-time constant"

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int f5 = factorial(5);   // computed at compile time → 120

[[attributes]]

Pure compiler hints. Common ones:

[[nodiscard]] int compute();         // warn if return value ignored
[[maybe_unused]] int x = 5;          // suppress unused warning
[[noreturn]] void fatal();           // function never returns
[[likely]] / [[unlikely]]            // branch-prediction hint (C++20)
[[fallthrough]];                     // intentional switch fallthrough

Cheat-Sheet

FeatureOne-liner
Type deductionauto x = expr;
Range loopfor (auto& x : v)
Structured bindingauto [k, v] = pair;
Lambda[](int x){ return x*2; }
Movestd::move(obj)
Maybe valuestd::optional
One of typesstd::variant
Read-only string paramstd::string_view
Read-only sequence paramstd::span
Constrain templatetemplate
Pipelinev \views::filter \views::transform
Formatstd::format("hi {}", name)
Compile-timeconstexpr, consteval

You're now writing C++ that any modern team would accept. The last topic curates places to practice.