Il costrutto switch
è un'altra delle istruzioni mediante le quali si implementa il controllo di flusso in C++.
Similarmente all'istruzione if, esso consente infatti di eseguire istruzioni differenti a seconda del risultato prodotto dalla valutazione di un'espressione. Tuttavia, il costrutto switch
presenta alcune peculiarità degne di nota che esamineremo in questa lezione.
La sintassi generale è la seguente:
switch (<espressione>)
{
case <valore costante 1>:
// istruzioni
break;
case <valore costante 2>:
// istruzioni
break;
...
case <valore costante N>:
// istruzioni
break;
default:
// istruzioni
break;
}
All'istruzione switch
è associata un'espressione il cui valore restituito deve essere di tipo intero (incluso anche il tipo char
e le sue varianti), o un valore di tipo enum. Non sono ammesse ad esempio, espressioni che restituiscono una variable di tipo std::string
o di un tipo definito dall'utente, a meno che per esso esista una catena di conversione ad un tipo intero o enum
.
Tale limitazione, è anche uno dei principali vantaggi dell'uso di switch
rispetto le altre istruzioni condizionali poiché, limitandosi all'elaborazione di tipi interi, esso può essere tradotto dal compilatore in maniera molto efficiente sotto forma di jump table (tabelle dei salti) in codice assembly, garantendo alte prestazioni anche in presenza di un numero elevato di diramazioni.
All'interno del blocco delimitato da switch
è possibile definire un numero arbitrario di etichette case
cui è associato un valore costante, cioè noto a tempo di compilazione. Non si possono quindi usare espressioni valutabili solo a tempo di esecuzione, ad esempio non è lecito scrivere:
int x;
switch (x)
{
case (x <= 0): // errore: l'espressione non è valutabile dal compilatore
// istruzioni
break;
case (x > 0): // come sopra
// istruzioni
break;
}
Se esiste una corrispondenza esatta tra il valore dell'espressione ed il valore costante associato ad una delle etichette case
, il flusso viene rediretto alle istruzioni che seguono tale etichetta.
Se nessuna delle etichette ha un valore corrispondente a quello dell'espressione, il flusso viene rediretto all'etichetta opzionale default
, se presente. Infine, al raggiungimento di un'istruzione break
, o all'uscita dal blocco switch
, il controllo di flusso viene restituito al contesto esterno.
Semantica e casi particolari
A parte la sintassi generale che abbiamo appena introdotto, il costrutto switch
ha, nella pratica, una semantica meno stringente rispetto quella di un blocco if-else if-else. Più specificamente, i seguenti fattori possono alterare il comportamento generale che abbiamo appena discusso:
- il blocco di istruzioni associato ad un'etichetta può essere vuoto;
- l'istruzione di salto non condizionato
break
al termine di un blocco è opzionale; - tutte le etichette (
case
edefault
) condividono il medesimo ambito di visibilità, delimitato dalle parentesi graffe del bloccoswitch
.
Come consequenza dei punti 1 e 2, è lecito associare più casi al medesimo blocco di istruzioni, come ad esempio mostrato nel listato seguente, dove viene usata la stessa formula per il calcolo dell'area di un quadrato e di un rettangolo, ma non per quella di un triangolo o di un'ellisse.
#include <cmath>
#include <iostream>
enum class Shapes2D { Quad, Rect, Triangle, Ellipse };
int main()
{
float width = 5;
float height = 2;
Shapes2D type = Shapes2D::Quad;
float area = 0;
switch (type)
{
case Shapes2D::Quad: /* case senza istruzioni associate e senza istruzione break */
case Shapes2D::Rect:
area = width * height;
break;
case Shapes2D::Triangle:
area = width * height / 2.0f;
break;
case Shapes2D::Ellipse:
area = width * height * M_PI;
break;
}
std::cout << "area = " << area << std::endl;
return 0;
}
Questo comportamento è reso possibile grazie alla particolare semantica del costrutto switch
che implementa una politica nota come fallthrough: in assenza di un'istruzione esplicita per il salto, come break
, il flusso non viene rediretto all'uscita dal blocco switch
quando viene raggiunta l'etichetta successiva. Essa viene semplicemente "attraversata", per eseguire il blocco di istruzioni che segue, in questo caso il calcolo dell'area del rettangolo.
L'adozione di tale politica deriva dal fatto che l'implementazione di switch
in C e C++ è basata sulla semantica non strutturata del costrutto computed GOTO di Fortran. Come è facilmente intuibile, a parte i casi in cui questo è il comportamento voluto, ciò può produrre facilmente effetti indesiderati in caso di dimenticanza dell'istruzione break
.
Il punto 3 ha invece ripercussioni sulla dichiarazione di variabili all'interno del blocco switch
. Ad esempio, il frammento seguente genererà un errore di compilazione:
int opt = 1;
switch (opt)
{
case 0:
int x = 1; // errore di compilazione!
break;
case 1:
// anche questo sarebbe parte dell'ambito di visibilità di x, ma x non è stata dichiarata!
break;
}
Poichè infatti l'ambito di visibilità è unico, la dichiarazione di variabili dopo un'etichetta seplicemente non è consentita, a meno che non sia esplicitamente creato un ambito di visibilità mediante l'uso di parentesi graffe:
int opt = 1;
switch (opt)
{
case 0:
{
int x = 1; // ok
} break;
case 1:
// x non esiste in questo contesto
break;
}
Espansione C++17: switch con istruzione di inizializzazione
Lo standard C++17 prevede un'espansione del costrutto switch
, del tutto analoga a quella prevista per le istruzioni if-else
, in cui l'espressione intera può essere corredata dall'inizializzazione di una o più variabili seguita da ';
', come illustrato nel seguente frammento:
int opt = 1;
switch (int x = 0; opt)
{
// ambito di visibilità di x
case 0:
// ambito di visibilità di x
break;
case 1:
// ambito di visibilità di x
break;
// ambito di visibilità di x
}
La parte di inizializzazione può essere una qualsiasi espressione o la definizione di una o più variabili dello stesso tipo, separate da virgole.
Con questa variante l'ambito di visibilità della variabile inizializzata è trasversale a tutte le etichette e non "inquina" il contesto esterno. In pratica, l'uso di questo costrutto è equivalente alla definizione di un contesto esplicito mediante l'uso di parentesi graffe che contenga sia la definizione della variabile, sia il blocco switch
.