Enums, Casting and Type Conversions
Give meaningful names to a fixed set of choices, and understand exactly when C++ converts one type into another for you — and when you have to do it yourself.
The problem enums solve
Imagine tracking a traffic light's color with anint:
int color = 0; // 0 = red? 1 = green? Who remembers?
if (color == 1) { /* go */ }
1 means. An enum replaces magic numbers with names:
enum class Color { Red, Green, Yellow };
Color light = Color::Green;
if (light == Color::Green) {
std::cout << "Go\n";
}
Color::Gren immediately, instead of it silently becoming some unrelated integer.
enum class versus plain enum
C++ has two flavors, and modern code should almost always use the first one.
enum class Color { Red, Green, Yellow }; // scoped — modern, prefer this
enum OldColor { RED, GREEN, YELLOW }; // unscoped — legacy
The difference is what happens to the names outside the enum:
Color c = Color::Red; // must qualify with Color::
OldColor o = RED; // RED leaks directly into the surrounding scope
RED, they'd collide and refuse to compile in the same scope. enum class fixes this — each enum's values live inside their own name, exactly like Color:: here, the same way std:: scopes everything in the standard library.
What an enum actually is underneath
Each name in an enum secretly is an integer, starting at 0 and counting up, unless you say otherwise:
enum class Direction { North, East, South, West };
// North = 0, East = 1, South = 2, West = 3
enum class HttpStatus { OK = 200, NotFound = 404, ServerError = 500 };
// explicit values, useful when they need to match an external system
enum class values don't silently convert to int (that's the whole point of being "scoped" — it also blocks accidental mixing), but you can force it when you genuinely need the number:
Direction d = Direction::East;
int n = static_cast<int>(d); // 1 — must be explicit
switch was made for enums
The control flow lesson covered switch. Enums are its best use case, because the compiler can warn you if you forget a case:
switch (light) {
case Color::Red: std::cout << "Stop\n"; break;
case Color::Green: std::cout << "Go\n"; break;
case Color::Yellow: std::cout << "Slow down\n"; break;
}
Implicit conversions: when C++ converts for you
Beyond enums, C++ constantly converts between types automatically — sometimes helpfully, sometimes as a trap. The variables-and-types lesson already showed the two big ones (integer division, overflow). Here's the general rule: when types mix in an expression, the "smaller" one is promoted to the "larger" one before the operation happens.
int a = 5;
double b = 2.0;
double result = a / b; // a is promoted to 5.0 first, then divided → 2.5
char c = 'A';
int code = c + 1; // c promoted to int (65), result is 66
bool → char → int → long → long long → float → double. Mixing any two causes the smaller to become the larger — never the other way around, which is exactly why 5 / 2.0 gives you 2.5 instead of truncating like 5 / 2 does.
Narrowing: when a conversion loses data
The dangerous direction is going from a larger type to a smaller one — this is called narrowing, and the compiler will let you do it without complaint unless you use certain modern syntax.
double pi = 3.14159;
int whole = pi; // 3 — the fractional part is just gone, no warning
int big = 300;
char small = big; // undefined/implementation-defined — char often can't hold 300
Brace initialization specifically refuses to compile this silently:
int whole{pi}; // error: narrowing conversion, won't compile
{} initialization over = for anything beyond the simplest cases — it turns a silent data-loss bug into a compile error you're forced to look at.
Explicit casts: telling the compiler "I meant to do this"
When you genuinely want a narrowing or type-changing conversion, say so explicitly withstatic_cast, rather than relying on an implicit one that looks accidental to anyone reading the code later.
double average = 7.0 / 2; // 3.5
int rounded_down = static_cast<int>(average); // 3, explicitly intentional
int total = 10, count = 3;
double avg = static_cast<double>(total) / count; // 3.333..., not the trap from the types lesson
double before the division forces the whole expression to promote to floating point.
There are other cast forms (dynamic_cast, const_cast, reinterpret_cast) used in more advanced situations — static_cast is the one you'll reach for in ordinary code, and seeing (int)average-style old C casts in the wild is a sign of older code; prefer static_cast since it's checked more strictly by the compiler and is easier to search for in a large codebase.
Try it
Common mistakes
- Using plain
enuminstead ofenum classin new code, risking name collisions. - Assuming
int-to-charordouble-to-intconversions warn you — they don't, unless you use brace initialization. - Old-style C casts
(int)xinstead ofstatic_cast<int>(x)— both work, butstatic_castis checked more strictly and easier to search for. - Forgetting to cast before division and hitting the integer-division trap again in a new disguise.
- Comparing an
enum classvalue directly to anint— it won't compile, which is a feature, not a bug; cast explicitly if you really need the number.
Summary
Enums replace magic numbers with names the compiler checks for you — always preferenum class over plain enum in new code, since it avoids leaking names into the surrounding scope. C++ automatically promotes smaller types to larger ones in mixed expressions, but silently narrows larger types down to smaller ones, losing data without a warning unless you use brace initialization. Use static_cast whenever you want a conversion to happen and want that intent to be visible to anyone reading the code.