# 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` ```cpp 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: ```cpp if (auto [ok, n] = try_parse(s); ok) { std::cout << "got " << n << '\n'; } ``` `try_parse` returns a `std::pair` (or a `std::expected`). 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: ```cpp 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. ```cpp {{#include ../../examples/ch04/ch04_sb_condition.cpp}} ``` ## `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. ```cpp std::variant 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: ```cpp 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. ```cpp {{#include ../../examples/ch04/ch04_fizzbuzz_ranges.cpp}} ``` ## `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`. The selected branch can be inlined, eliminating dead code and improving optimisation. A typical use tests `std::ranges::range` 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.