Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control flow, modernly

This chapter examines modern control-flow constructs introduced in C++23 and C++26 that let the programmer keep the code that establishes a value adjacent to the test that uses it. We look at init-statements for if, switch, while and for, the ability to place structured bindings directly in a condition, the range-for loop as the preferred iteration form, compile-time conditionals with if constexpr, and the recommendation to replace manual loops with standard algorithms. By using these features the intent of the code stays close to the point of use, reducing accidental reuse and improving readability.

Init-statements

C++23 introduced init‑statements so a temporary variable can be created, tested, and destroyed within the condition of if, while, or for. A simple call yields compact syntax. For more complex setup a helper returning an RAII object can be used directly.

Init‑statements also appear in while and range‑based for. In a while loop the initializer runs once before the first test, allowing resource acquisition followed by exhaustion testing. In a for loop the initializer can bind a temporary range object, ensuring the range lives exactly for the loop’s duration. This keeps lifetimes tightly scoped and prevents accidental reuse outside the loop body.

Example: init‑statement in an if

if (auto line = std::getline(std::cin, s); !line.empty()) {
    std::cout << "first line: " << s << '\n';
}

The variable line exists only while the condition is evaluated and the body runs. No other part of the function can mistakenly read it.

This tight scoping prevents accidental reuse of the temporary buffer later in the function, which is a common source of bugs in legacy code. By limiting the lifetime, the compiler can also apply stack‑slot reuse optimizations, reducing memory pressure in tight loops.

Structured bindings in a condition (C++26)

C++26 extends init-statements by enabling structured bindings directly in the condition. A binding can decompose a tuple-like object and the boolean test can refer to any of the bound names:

if (auto [ok, n] = try_parse(s); ok) {
    std::cout << "got " << n << '\n';
}

try_parse returns a std::pair<bool,int> (or a std::expected<int,std::string_view>). The binding extracts the success flag ok and the parsed value n. The subsequent test ok decides whether the body runs. Before C++26 the language only permitted a single simple declaration inside the condition, so a common pattern was a nested if pyramid:

auto result = try_parse(s);
if (result.ok) {
    int n = result.value;
    // …
}

The new form collapses that pyramid into one line, reducing visual noise and keeping the “parse-then-use” logic together. It is the modern replacement for the nested-if pattern often used with std::expected or std::pair.

Using structured bindings in a condition also makes error handling more direct. When a function returns a std::expected, the success flag and the value can be examined immediately. This enables the error path to be written without an extra temporary variable. This leads to code that reads like a natural language description of the operation, which matches the goal of modern C++ to be expressive and intent-revealing.

#include <iostream>
#include <utility>
#include <string_view>

// Simple parser that returns {true,42} for the literal "42",
// otherwise returns {false,0}.
std::pair<bool,int> try_parse(std::string_view s) {
    if (s == "42") return {true, 42};
    return {false, 0};
}

int main() {
    std::string_view input = "42";
    if (auto [ok, n] = try_parse(input); ok) {
        std::cout << "got " << n << '\n';
    }
    return 0;
}

switch need not switch on an integer

Although the classic switch works well with integral types, modern C++ encourages the use of std::visit for variant-like data. When a developer needs to dispatch based on a value that is not an integer, the visit pattern provides exhaustive handling and integrates with concepts for compile‑time checks.

In practice, a switch on an enum can still be useful when the set of cases is closed and the compiler can emit a jump table. However, for open‑ended sets such as std::variant the visit approach avoids the risk of missing a case and produces clearer error messages.

When performance is critical, a switch with contiguous case values can be faster than a series of if‑else checks because the compiler can generate a direct table lookup. Yet, readability and safety often outweigh micro‑optimisations, especially in high‑level code.

The guidelines suggest preferring visit when dealing with sum types and reserving switch for simple, closed enumerations where the intent is to map each constant to a distinct branch.

std::variant<int,double,std::string> v = 3.14;
std::visit([](auto&& arg){
    std::cout << arg << '\n';
}, v);

For range‑based algorithms replace a switch that branches on element values with a standard algorithm such as std::count_if or std::transform. The algorithm expresses what to compute, not how to iterate.

Ranges-for is the only loop you write

A range-for iterates directly over the elements of a view or container:

for (auto&& x : container) {
    // use x
}

It eliminates the manual index variable, the off-by-one risk, and the need to look up container[i]. The Core Guidelines (ES.71) state: write a range-for whenever you need to touch each element. Index-based loops belong only to cases where the index itself is a required output.

The FizzBuzz program below runs over the view iota(1,21), which generates the integers 1 through 20. No explicit index variable appears because the view owns the counting.

#include <iostream>
#include <ranges>

int main() {
    for (int i : std::views::iota(1, 21)) {
        if (i % 15 == 0) std::cout << "fizzbuzz";
        else if (i % 3 == 0) std::cout << "fizz";
        else if (i % 5 == 0) std::cout << "buzz";
        else std::cout << i;
        if (i != 20) std::cout << ' ';
    }
    std::cout << '\n';
    return 0;
}

if constexpr

C++17 introduced if constexpr, a compile‑time conditional. The false branch is discarded before instantiation, so it need not compile. This lets generic code adapt to template arguments via constant‑expression predicates such as std::is_integral_v<T>. The selected branch can be inlined, eliminating dead code and improving optimisation.

A typical use tests std::ranges::range<T> to choose between printing a scalar value or iterating a range. Concepts can be combined, e.g. requires { typename T::value_type; }, to keep constraints close to the code they govern.

Overall, if constexpr provides clear, type‑safe compile‑time dispatch without separate specialisations.

Raw loops are a smell

When an algorithm expresses the intent (e.g., find the first element greater than 10), writing a manual loop hides that intent. The guideline (ES.70) urges the programmer to replace a hand‑written loop with an appropriate standard algorithm, such as std::find_if, std::count, or std::transform, so that readers instantly recognise the operation. Chapter 12 will enumerate the algorithm zoo and show how each algorithm maps to a common pattern.

Standard algorithms also bring strong exception‑safety guarantees. Because the library implements the iteration and cleanup logic, edge cases such as early exits or thrown exceptions are handled uniformly. This reduces the likelihood of resource leaks compared with manually written loops that must explicitly manage cleanup. Moreover, many algorithms are annotated for vectorisation. The compiler can then generate SIMD instructions automatically when the iterator type supports it. The result is often faster code with the same expressive clarity.

When performance is critical, the programmer can still profile the algorithmic choice. The standard library provides overloads that accept execution policies. These enable parallel execution without changing the high‑level code. This flexibility means that the same source can be tuned for different hardware targets by swapping the policy argument.

Overall, preferring algorithms over hand‑written loops aligns with modern C++ philosophy: write what you want to achieve, let the library handle how to achieve it.

Try this

Rewrite the FizzBuzz program so that the upper bound is supplied by a function parameter and the program prints only the count of numbers that are multiples of both 3 and 5. Use the std::views::filter adaptor to select the qualifying numbers and std::ranges::distance to obtain the count.