Enums and Real-World Data Modelling
Many programs need to represent values that belong to a small, fixed set of possibilities. An order can be placed, packed, shipped, delivered, or cancelled. A traffic light can be red, yellow, or green. A task can be pending, in progress, or completed.
Although integers and strings can represent these states, they permit invalid values and often hide the meaning of the program. Enumerations provide meaningful names for a controlled set of alternatives, making the code easier to read, validate, and maintain.
The Problem with Raw Integer States
Consider an e-commerce application that stores an order's status using an integer:
0means placed.1means packed.2means shipped.3means delivered.4means cancelled.
#include <iostream>using namespace std;int main() { int orderStatus = 3; if (orderStatus == 3) { cout << "The order cannot be cancelled\n"; }}The condition works, but the number 3 does not reveal its meaning. A developer must remember the mapping or search for a comment explaining it. Such unexplained values are often called magic numbers.
An integer also permits meaningless states such as -7 or 1000. The variable itself does not communicate which values are intended to be valid.
Defining a Scoped Enumeration
A scoped enumeration is defined using enum class. It introduces a new type with a fixed set of named enumerators.
#include <iostream>using namespace std;enum class OrderStatus { Placed, Packed, Shipped, Delivered, Cancelled};int main() { OrderStatus status = OrderStatus::Placed; if (status == OrderStatus::Placed) { cout << "The order has been placed\n"; } status = OrderStatus::Delivered; cout << static_cast<int>(status) << '\n';}OrderStatus is the type, while status is an object of that type. An enumerator is accessed using the scope-resolution operator:
OrderStatus::Placed
The expression clearly identifies both the enum type and the selected state. A condition such as status == OrderStatus::Delivered communicates its purpose without a supporting comment.
Default Underlying Values
Every enumerator has an underlying integral value. Unless values are assigned explicitly, numbering begins at zero and increases by one:
OrderStatus::Placedhas underlying value0.OrderStatus::Packedhas underlying value1.OrderStatus::Shippedhas underlying value2.OrderStatus::Deliveredhas underlying value3.OrderStatus::Cancelledhas underlying value4.
The numeric representation is an implementation detail unless the program deliberately assigns values for a protocol, file format, database, or external API. Program logic should normally compare named states rather than depend on their default numbers.
Converting an Enum Class to an Integer
A scoped enumeration does not convert implicitly to an integer. The following assignment is not allowed:
int value = status;
An explicit conversion is required:
int value = static_cast<int>(status);
This restriction prevents an enum value from accidentally participating in unrelated integer calculations.
Explicit Enumerator Values
Enumerator values can be assigned explicitly. After an explicit value, later unassigned enumerators continue by adding one.
#include <iostream>using namespace std;enum class OrderStatus { Placed = 100, Packed, Shipped, Delivered = 90, Cancelled};int main() { cout << static_cast<int>(OrderStatus::Placed) << '\n'; cout << static_cast<int>(OrderStatus::Packed) << '\n'; cout << static_cast<int>(OrderStatus::Delivered) << '\n'; cout << static_cast<int>(OrderStatus::Cancelled) << '\n';}The resulting values are:
Placed = 100Packed = 101Shipped = 102Delivered = 90Cancelled = 91
If only the first enumerator is assigned 100, the following enumerators receive 101, 102, 103, and 104.
Explicit values are useful when an enum must match codes defined outside the program. They should not be introduced merely to replace meaningful state comparisons with numeric comparisons.
Scoped and Unscoped Enumerations
C++ supports both scoped and unscoped enumerations.
Scoped Enumeration
A scoped enumeration uses enum class:
Enumerators remain inside the enum's scope.
Names are qualified, such as
OrderStatus::Placed.Values do not implicitly convert to integers.
Different enum classes can reuse the same enumerator names.
Unscoped Enumeration
An unscoped enumeration uses only enum:
Enumerator names are introduced into the surrounding scope.
An enumerator may be written directly, such as
Placed.Values can convert implicitly to integral types.
Names from different unscoped enums can conflict.
#include <iostream>using namespace std;enum class OrderStatus { Placed, Delivered};enum class ShipmentStatus { Placed, Dispatched};enum LegacyStatus { Pending, Completed};int main() { OrderStatus order = OrderStatus::Placed; ShipmentStatus shipment = ShipmentStatus::Placed; LegacyStatus legacy = Pending; int numericValue = legacy; cout << numericValue << '\n';}Both scoped enums can contain Placed because their complete names are different. An unscoped Placed introduced into the same surrounding scope could conflict with another unscoped declaration using that name.
Modern C++ generally prefers enum class because it provides stronger type safety and cleaner name scoping.
Using an Enum in a Switch Statement
An enum value can control a switch statement. Each case uses a named enumerator instead of a raw number.
This is especially suitable when each state requires distinct behavior. It also allows the compiler to help identify missing enum cases in suitably configured builds.
Combining Enums with Structures
Structures describe complete entities, while enums describe one controlled state or category inside those entities. Combining them produces expressive real-world models.
For example, a task contains an identifier, a title, and a status. The status should be limited to pending, in progress, or completed.
#include <iostream>#include <string>using namespace std;enum class TaskStatus { Pending, InProgress, Completed};struct Task { int id; string title; TaskStatus status;};int main() { Task task{ 101, "Complete the assignment", TaskStatus::Pending }; if (task.status == TaskStatus::Pending) { cout << task.title << " is pending\n"; }}TaskStatus is used as the type of the status member. The structure cannot accidentally store an arbitrary string or unrelated integer in that member without an explicit and suspicious conversion.
Modelling State Transitions
A state transition moves an entity from one valid state to another. Enums name the states, while program logic defines which transitions are permitted.
Consider the following order workflow:
A placed order can become packed.
A packed order can become shipped.
A shipped order can become delivered.
A placed or packed order can be cancelled.
A delivered order cannot be cancelled.
An order should not jump directly from placed to shipped.
These rules protect the model from logically impossible transitions.
#include <iostream>using namespace std;enum class OrderStatus { Placed, Packed, Shipped, Delivered, Cancelled};bool canTransition(OrderStatus current, OrderStatus next) { if (current == OrderStatus::Placed) { return next == OrderStatus::Packed || next == OrderStatus::Cancelled; } if (current == OrderStatus::Packed) { return next == OrderStatus::Shipped || next == OrderStatus::Cancelled; } if (current == OrderStatus::Shipped) { return next == OrderStatus::Delivered; } return false;}int main() { OrderStatus status = OrderStatus::Placed; if (canTransition(status, OrderStatus::Packed)) { status = OrderStatus::Packed; } if (canTransition(status, OrderStatus::Shipped)) { status = OrderStatus::Shipped; } cout << static_cast<int>(status) << '\n';}The enum does not enforce transitions automatically. It provides a safe vocabulary for the states; functions such as canTransition() enforce the business rules.
Problem 1: Print a Traffic Light's Name
Given a TrafficLight value, print its corresponding name. The possible states are red, yellow, and green.
Approach
Define a scoped enumeration containing the three lights.
Pass the current light to a function.
Use a
switchstatement to select the appropriate text.Terminate each case using
break.
#include <iostream>using namespace std;enum class TrafficLight { Red, Yellow, Green};void printName(TrafficLight light) { switch (light) { case TrafficLight::Red: cout << "Red\n"; break; case TrafficLight::Yellow: cout << "Yellow\n"; break; case TrafficLight::Green: cout << "Green\n"; break; default: cout << "Invalid value\n"; }}int main() { TrafficLight light = TrafficLight::Green; printName(light);}Passing TrafficLight::Green prints Green. The named cases are considerably clearer than cases based on unexplained integers.
Complexity Analysis
Time complexity:
O(1).Auxiliary space:
O(1).
Problem 2: Find the Next Traffic Light
Given the current traffic light, determine the next light in the repeating cycle:
Red becomes green.
Green becomes yellow.
Yellow becomes red.
A function can return the next TrafficLight value. The existing printing function can then display its name.
#include <iostream>using namespace std;enum class TrafficLight { Red, Yellow, Green};TrafficLight nextLight(TrafficLight current) { switch (current) { case TrafficLight::Red: return TrafficLight::Green; case TrafficLight::Green: return TrafficLight::Yellow; case TrafficLight::Yellow: return TrafficLight::Red; } return TrafficLight::Red;}void printName(TrafficLight light) { switch (light) { case TrafficLight::Red: cout << "Red\n"; break; case TrafficLight::Yellow: cout << "Yellow\n"; break; case TrafficLight::Green: cout << "Green\n"; break; }}int main() { TrafficLight current = TrafficLight::Yellow; printName(nextLight(current));}For TrafficLight::Yellow, the function returns TrafficLight::Red. The cycle is expressed using enum values rather than numeric assumptions about their underlying representation.
Complexity Analysis
Time complexity:
O(1).Auxiliary space:
O(1).
Problem 3: Count Tasks by Status
Given a list of task statuses and a target status, count how many list elements equal the target.
For the list:
Pending
Completed
Pending
If the target is Pending, the answer is 2.
Approach
Initialize a counter to zero.
Traverse every status in the vector.
Compare the current status with the target.
Increment the counter whenever they match.
Return the final count.
#include <iostream>#include <vector>using namespace std;enum class TaskStatus { Pending, InProgress, Completed};int countByStatus( const vector<TaskStatus>& statusList, TaskStatus targetStatus) { int count = 0; for (TaskStatus status : statusList) { if (status == targetStatus) { count++; } } return count;}int main() { vector<TaskStatus> statusList{ TaskStatus::Pending, TaskStatus::Completed, TaskStatus::Pending }; TaskStatus target = TaskStatus::Pending; cout << countByStatus(statusList, target) << '\n';}Complexity Analysis
Time complexity:
O(n), wherenis the number of statuses.Auxiliary space:
O(1), excluding the input vector.
Problem 4: Determine Whether an Order Can Be Cancelled
An order can be cancelled only while it is placed or packed. Once it has been shipped or delivered, cancellation is no longer permitted.
The function should therefore return true when either of these conditions holds:
The status is
OrderStatus::Placed.The status is
OrderStatus::Packed.
#include <iostream>using namespace std;enum class OrderStatus { Placed, Packed, Shipped, Delivered, Cancelled};bool isCancellationPossible(OrderStatus status) { return status == OrderStatus::Placed || status == OrderStatus::Packed;}int main() { OrderStatus first = OrderStatus::Placed; OrderStatus second = OrderStatus::Delivered; cout << isCancellationPossible(first) << '\n'; cout << isCancellationPossible(second) << '\n'; return 0;}The first call prints 1, representing true. The second prints 0, representing false.
Complexity Analysis
Time complexity:
O(1).Auxiliary space:
O(1).
Guidelines for Real-World Data Modelling
Use an enum when a value must belong to a small, known set of states or categories.
Prefer meaningful enumerator names over raw integer or string codes.
Prefer
enum classfor stronger type safety and scoped names.Store enums inside structures when a real-world record has a controlled state.
Keep transition rules in clearly named functions rather than changing states freely throughout the program.
Compare named enum values rather than depending on their default underlying numbers.
Use explicit integral values only when integration with an external representation requires them.
Convert a scoped enum to an integer explicitly when the underlying value is genuinely needed.
Use
switchwhen behavior differs for each enumerated state.Handle invalid or unexpected values when enums cross unsafe boundaries such as files, databases, user input, or network messages.
Key Takeaways
An enumeration represents a fixed set of named alternatives.
Raw numbers hide meaning and permit states that may not make sense.
enum classkeeps enumerator names scoped and prevents implicit integer conversion.Default enum values begin at zero and increase by one.
Explicit enumerator values can be assigned when required.
Scoped enum values are accessed with the scope-resolution operator.
An enum can be used as a structure member, function parameter, return type, or vector element.
Enums provide the vocabulary for state modelling; separate program logic enforces valid transitions.
Combining structures and enums produces readable and maintainable models of real-world entities.
Be the first to add a comment.