Switch and enum model choices in C++ by giving a fixed set of possibilities a name and a type. A [[switch]] dispatches on the value of one expression, and an [[enum]] enumerates the legal values so the compiler can check you used a real one. Together they replace a long [[else if]] ladder with something the reader can scan in one pass.
Defining the choices with an enum
An enumeration names a set of integer constants. The underlying type is an integer, but the named values are what your code actually uses.
#include
enum PizzaSize {
Small,
Medium,
Large,
Party
};
int main() {
PizzaSize chosen = Medium;
std::cout << "chosen size value: " << chosen << "\n";
std::cout << "large value: " << Large << "\n";
return 0;
}Without an explicit initialiser the values count up from zero, so Small is 0, Medium is 1 and so on. You can set them yourself when the numbers matter.
#include
enum PizzaSize {
Small = 10,
Medium = 12,
Large = 16,
Party = 22
};
int main() {
std::cout << "diameter: " << Medium << " inches\n";
return 0;
}Scoped enums keep names out of the way
An unscoped enum leaks its names into the enclosing scope, so Small might collide with something else. A scoped enum, written enum class, nests the names inside the type and requires the type name when you use them.
#include
enum class PizzaSize {
Small,
Medium,
Large,
Party
};
enum class Topping {
ExtraCheese,
Olives,
Basil
};
int main() {
PizzaSize chosen = PizzaSize::Large;
Topping extra = Topping::Basil;
std::cout << "size value: " << static_cast(chosen) << "\n";
std::cout << "topping value:" << static_cast(extra) << "\n";
return 0;
}Scoped enums also do not implicitly convert to integers, which is exactly why the static_cast is needed. That extra step is a feature: it stops a size from being passed where a topping is expected.
Dispatching with switch
A switch evaluates one expression and jumps to the matching case. Each case ends with break, which stops the fall-through.
#include
enum class PizzaSize { Small, Medium, Large, Party };
int main() {
PizzaSize size = PizzaSize::Large;
int price = 0;
switch (size) {
case PizzaSize::Small:
price = 899;
break;
case PizzaSize::Medium:
price = 1299;
break;
case PizzaSize::Large:
price = 1699;
break;
case PizzaSize::Party:
price = 2299;
break;
}
std::cout << "price: " << price << "\n";
return 0;
}The switch reads like a table. There is one expression at the top, one label per row, and the body of each case is the code that belongs to that row.
Fall-through is a bug until it is a feature
If you forget the break, execution continues into the next case and runs its body too. This is called fall-through.
#include
int main() {
int extra_toppings = 1;
int charge = 0;
switch (extra_toppings) {
case 0:
charge = 0;
break;
case 1:
charge = 100; // no break: falls into case 2
// fall through
case 2:
charge += 50;
break;
default:
charge += 25;
break;
}
std::cout << "charge: " << charge << "\n";
return 0;
}One topping costs 150 rather than 100, because the single-topping case added 100 and then fell through and added another 50. Most of the time an accidental fall-through is a bug, and the compiler will warn about it when you compile with -Wall.
Fall-through is occasionally deliberate — grouping cases that share a body — and C++17 has an attribute that says so on purpose.
#include
int main() {
int extra_toppings = 3;
int charge = 0;
switch (extra_toppings) {
case 3:
charge += 25;
[[fallthrough]]; // the intent is explicit, so no warning
case 2:
charge += 50;
break;
default:
break;
}
std::cout << "charge: " << charge << '\n';
return 0;
}The [[fallthrough;]] attribute tells the compiler that the fall-through is on purpose, so it does not warn. GCC and Clang also accept a plain comment such as // fall through as a signal of intent.
The default case and matching a range
The default label runs when nothing else matched. Always include it, even if it only prints a message, because it documents that you thought about the other cases.
A switch can also match a range of values using the case-with-initializer syntax added in C++17.
#include
int main() {
int delivery_minutes = 42;
const char* status = "unknown";
switch (delivery_minutes) {
case 0:
status = "not started";
break;
default:
if (delivery_minutes < 30) {
status = "on the way";
} else if (delivery_minutes <= 60) {
status = "arriving soon";
} else {
status = "delayed";
}
break;
}
std::cout << "status: " << status << "\n";
return 0;
}A switch only works on integral and enumeration types, and the case labels must be constant expressions. If your choices are not a fixed set of values, use an if ladder instead.
When to use switch and when to use if
Use a switch when the expression is a small fixed set of named values — a status, a menu choice, a state machine. Use if chains when the conditions involve ranges, floating-point comparisons or compound logic.
The practical test is whether a reader can scan your switch and see every option. If the list is long and each case needs different logic, an if ladder may read better. If the values are named and discrete, the switch wins because it makes the exhaustiveness visible.
Quick reference
- An
enumnames a set of integer constants; a scopedenum classkeeps the names inside the type. - Scoped enums do not convert to integers implicitly; use
static_cast<int>. - A
switchmatches one expression againstcaselabels. - Every case needs a
break, or it falls into the next one. - Mark an intentional fall-through with
[[fallthrough;]] in C++17. - Always write a
defaultcase, even a trivial one. - Switch works only on integral and enumeration types with constant case labels.
Try it yourself: define a scoped enum for pizza size and another for crust type, then write a switch that computes a price from both. Add a [[default]] case for an unknown crust, and compile with [[-Wall]] to see the compiler flag a missing break. Then convert the crust switch into an [[if]] ladder and decide which version you would rather maintain.