Flusso di Controllo e Cicli
Padroneggia tutti i costrutti di flusso di controllo del C++ inclusi if/else, switch, tutte le forme di ciclo, range-based for, inizializzatori C++17 e binding strutturati.
Il Flusso di Controllo in C++
Le istruzioni di flusso di controllo determinano l'ordine in cui il tuo programma viene eseguito. Il C++ fornisce un ricco insieme di costrutti di flusso di controllo che vanno ben oltre le basi ereditate dal C. Il C++ moderno (C++17 e successivi) aggiunge funzionalità come gli if con inizializzatori e constexpr if che rendono il flusso di controllo più espressivo e sicuro.
Un principio chiave: minimizza lo scope delle variabili. Ogni variabile dovrebbe essere dichiarata il più vicino possibile al suo primo utilizzo, e nello scope più ristretto possibile. Le funzionalità del C++17 come gli if con inizializzatori ti aiutano a seguire questo principio.
if/else e switch
Le istruzioni if e switch sono i meccanismi principali di branching. Il C++17 aggiunge clausole con inizializzatore a entrambi:
#include <iostream>
#include <map>
#include <string>
#include <optional>
std::optional<int> find_value(const std::string& key) {
std::map<std::string, int> data = {{"alpha", 1}, {"beta", 2}, {"gamma", 3}};
if (auto it = data.find(key); it != data.end()) { // C++17 if with initializer
return it->second;
}
return std::nullopt;
}
int main() {
// C++17: if with initializer — the variable's scope is limited to the if/else
if (auto val = find_value("beta"); val.has_value()) {
std::cout << "Found: " << *val << '\n';
} else {
std::cout << "Not found\n";
}
// 'val' is no longer in scope here — cleaner!
// switch with C++17 initializer
enum class Color { Red, Green, Blue };
Color c = Color::Green;
switch (auto name = "unknown"; c) { // C++17 switch with initializer
case Color::Red: name = "red"; std::cout << name << '\n'; break;
case Color::Green: name = "green"; std::cout << name << '\n'; break;
case Color::Blue: name = "blue"; std::cout << name << '\n'; break;
}
// [[fallthrough]] attribute (C++17) — document intentional fallthrough
int priority = 2;
switch (priority) {
case 1:
std::cout << "Critical: ";
[[fallthrough]]; // Tells the compiler this is intentional
case 2:
std::cout << "Important: ";
[[fallthrough]];
case 3:
std::cout << "Handle it\n";
break;
default:
std::cout << "Unknown priority\n";
}
}Premi Esegui per compilare ed eseguire questo esempio.
Cicli: for, while, do-while e range-based for
Il C++ offre molteplici forme di ciclo. Il ciclo range-based for (C++11) è preferito quando si itera sui contenitori perché elimina gli errori off-by-one ed è più chiaro:
#include <iostream>
#include <vector>
#include <string>
#include <map>
int main() {
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
// Traditional for loop — use when you need the index
for (std::size_t i = 0; i < names.size(); ++i) {
std::cout << i << ": " << names[i] << '\n';
}
// Range-based for loop (C++11) — preferred for iteration
for (const auto& name : names) { // const auto& avoids copying
std::cout << name << '\n';
}
// Range-based for with structured bindings (C++17)
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}, {"Charlie", 92}};
for (const auto& [name, score] : scores) { // Destructure the pair
std::cout << name << " scored " << score << '\n';
}
// C++20: range-based for with init-statement
for (auto v = std::vector{1, 2, 3, 4, 5}; auto elem : v) {
std::cout << elem << ' ';
}
std::cout << '\n';
// while loop — use when the number of iterations is unknown
int n = 1;
while (n <= 1024) {
std::cout << n << ' ';
n *= 2;
}
std::cout << '\n';
// do-while — body executes at least once
int input;
do {
std::cout << "Enter a positive number: ";
input = 42; // Simulated input for example
} while (input <= 0);
}Premi Esegui per compilare ed eseguire questo esempio.
Binding Strutturati (C++17)
I binding strutturati (C++17) ti permettono di decomporre un oggetto nelle sue parti costituenti. Funzionano con array, struct con tutti i membri pubblici, std::pair, std::tuple e qualsiasi tipo che supporti il protocollo dei binding strutturati.
La sintassi auto [a, b] = expr; crea riferimenti nominati ai componenti di expr. Puoi aggiungere qualificatori const e &: const auto& [a, b] = expr; crea riferimenti const senza copiare.
I binding strutturati sono particolarmente potenti con l'iterazione su std::map (decomposizione di std::pair), i ritorni di funzione che usano std::pair o std::tuple, e il valore di ritorno del metodo insert sui contenitori associativi.
Usa break per uscire da un ciclo in anticipo quando hai trovato ciò che cercavi. Usa continue per saltare il resto dell'iterazione corrente e passare alla successiva. Entrambi rendono i cicli più facili da capire se usati con giudizio.
Preferisci il range-based for rispetto al for basato su indice quando non hai bisogno dell'indice. Elimina intere classi di bug: errori off-by-one, uso della variabile sbagliata come indice e warning di confronto signed/unsigned.
Quando hai bisogno dell'indice, considera l'uso di std::size_t come tipo della variabile del ciclo per corrispondere al tipo di ritorno di .size(), o usa std::ssize() (C++20) che restituisce una dimensione con segno.
Evita i cicli profondamente annidati — se ti ritrovi ad annidare tre o più cicli, estrai i cicli interni in funzioni. Questo migliora la leggibilità e la testabilità.
- Preferisci i cicli range-based for rispetto a quelli basati su indice quando l'indice non è necessario
- Usa const auto& nel range-based for per evitare copie inutili
- Usa i binding strutturati per decomporre coppie e tuple nelle variabili del ciclo
- Estrai i cicli profondamente annidati in funzioni con nome per chiarezza
constexpr if (C++17): Branching a Tempo di Compilazione
if constexpr valuta la condizione a tempo di compilazione e scarta il ramo non preso. Questo è essenziale nella programmazione con template dove rami diversi potrebbero non essere nemmeno validi per tutti i tipi:
#include <iostream>
#include <type_traits>
#include <string>
template <typename T>
void describe(T value) {
std::cout << "Value: " << value;
if constexpr (std::is_integral_v<T>) {
// This branch only compiles when T is an integer type
std::cout << " (integer, " << (value % 2 == 0 ? "even" : "odd") << ")";
} else if constexpr (std::is_floating_point_v<T>) {
// This branch only compiles when T is a floating-point type
std::cout << " (floating-point)";
} else {
std::cout << " (other type)";
}
std::cout << '\n';
}
int main() {
describe(42); // Value: 42 (integer, even)
describe(3.14); // Value: 3.14 (floating-point)
describe("hello"); // Value: hello (other type)
}Premi Esegui per compilare ed eseguire questo esempio.
- L'if/switch con inizializzatori del C++17 limita lo scope delle variabili all'istruzione — usali per mantenere il codice pulito
- I cicli range-based for eliminano gli errori off-by-one e i problemi di confronto signed/unsigned
- I binding strutturati (C++17) decompongono coppie, tuple e struct in variabili con nome
- Usa [[fallthrough]] per documentare il fallthrough intenzionale nei case dello switch
- if constexpr (C++17) valuta le condizioni a tempo di compilazione — il ramo scartato non viene compilato
- Minimizza lo scope delle variabili: dichiara le variabili il più vicino possibile al primo utilizzo
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Qual è il vantaggio dell'`if` con inizializzatore del C++17: `if (auto x = f(); x > 0)`?
2. Cosa fa `for (const auto& [key, val] : my_map)`?
3. Cosa succede al ramo scartato di un'istruzione `if constexpr`?