C++ enum class, struct, pair and tuple: How to Model Small Data Safely

Model Ticket 417 three ways, calculate the same response window, then add an owner to see why named fields often beat positional access in changing code.

KnowledgeGate Team

Exam prep & CS education

Updated 7 Sep 20266 min read

std::pair and std::tuple look attractively short, while a named struct can feel like ceremony. The shorter syntax starts hiding meaning, however, as soon as data travels or changes. One support ticket with id = 417, priority = Priority::high, and later owner = "Meera" shows when a named type earns its place.

C++ enum class gives a finite value its own type

An enum class is a scoped, strongly typed enumeration:

enum class Priority : unsigned char { low = 1, medium = 2, high = 3 };

Callers write Priority::high, so the enumerator names do not leak into the surrounding scope. The value also does not convert implicitly to int. The keyword class here does not create a record with fields. It strengthens the enumeration.

enum class Access { low = 1, high = 3 };

Priority p = Access::high;        // compile-time error
int n = Priority::high;           // compile-time error

By contrast, int n = static_cast<int>(Priority::high); is deliberate and produces 3.

Priority and Access remain different types even when enumerators have the same names and values. The explicit numeric values make this example predictable. In real code, map external data deliberately instead of scattering casts. The C++ Tutorial places this idea within a wider path through language basics, OOP, and the STL.

C++ struct, pair and tuple can hold the same two values

A named record makes the domain visible:

struct Ticket { int id; Priority priority; };
Ticket named{417, Priority::high};

std::pair<int, Priority> paired{417, Priority::high};
std::tuple<int, Priority> tupled{417, Priority::high};

A struct is a user-defined type whose members are public by default. A class has private default member access. Both can have constructors and methods, so this choice is about the abstraction, not a capability ban. A transparent data record is often a natural job for a struct.

Shape

ID access

Priority access

Ticket

named.id

named.priority

std::pair<int, Priority>

paired.first

paired.second

std::tuple<int, Priority>

std::get<0>(tupled)

std::get<1>(tupled)

The stored values and field types match, but only Ticket carries a domain type and field names in the source. Ticket also appears by name in function signatures and diagnostics. A pair identifies two positions. A tuple identifies a fixed sequence of positions, which may have different types. Neither positional type explains that 417 is a ticket ID.

Worked C++ example: compute the same result through all three shapes

Use one helper to turn a valid priority into its rank, then apply a toy support rule:

constexpr int priority_rank(Priority p) { return static_cast<int>(p); }
constexpr int response_window(Priority p) { return 90 / priority_rank(p); }

The 90 minutes defines the example's base response window. For Priority::high, the calculation is:

  1. priority_rank(Priority::high) = 3

  2. response_window(Priority::high) = 90 / 3 = 30 minutes

The three reads use different access expressions but do not change the data:

std::cout << "struct: " << named.id << ", "
          << response_window(named.priority) << '\n';
std::cout << "pair: " << paired.first << ", "
          << response_window(paired.second) << '\n';
std::cout << "tuple: " << std::get<0>(tupled) << ", "
          << response_window(std::get<1>(tupled)) << '\n';

The exact output is:

struct: 417, 30
pair: 417, 30
tuple: 417, 30

Matching outputs show that all three models produce the same result for this two-value task; they do not make the models equally readable. A structured binding improves a local tuple use:

const auto& [ticket_id, ticket_priority] = tupled;

Here ticket_id is 417, and static_cast<int>(ticket_priority) is 3. const auto& avoids copying and supplies local names, but those names exist only at this use site. The tuple type still does not encode the domain.

Ticket 417 as a struct, pair, and tuple, each computing the same 30-minute response window and printing 417, 30.

C++ small-data models reveal their cost when a third field arrives

Now the requirement adds owner = "Meera". The named record grows directly:

struct Ticket { int id; Priority priority; std::string owner; };
Ticket named{417, Priority::high, "Meera"};

The new value is named.owner. Existing reads remain named.id and named.priority, so each access still communicates meaning.

The tuple can also grow, but the new role is positional:

std::tuple<int, Priority, std::string> tupled{417, Priority::high, "Meera"};
const auto& [id, priority, owner] = tupled;
// Direct owner access: std::get<2>(tupled)

A pair has only two slots, so representing all three values honestly requires nesting:

std::pair<int, std::pair<Priority, std::string>> paired{
    417, {Priority::high, "Meera"}
};
// Owner access: paired.second.second

Named types become more valuable when data later gains behaviour or invariants, an idea developed further in Object Oriented Technology Explained. That does not mean every struct must become a class. It means the model should keep its meaning as requirements grow.

Adding owner Meera to Ticket 417: the struct gains a named field while the tuple and nested pair grow positional slots.

C++ type choice: use the smallest abstraction that preserves meaning

Choose by lifetime, meaning, and expected change rather than character count.

Type

Prefer it when

enum class

One value comes from a closed domain, such as Priority::high.

pair

A local two-value hand-off has conventional, obvious roles.

tuple

A short-lived heterogeneous result is immediately unpacked.

struct

Values form a domain concept, cross a function boundary, are stored or reused, need validation, or are likely to change.

For example, std::pair<int, int>{4, 9} is reasonable for a local range result when the function contract clearly says first is the minimum and second is the maximum. std::pair<int, Priority> is weaker for Ticket, because every caller must remember the positions.

A parser might reasonably return {417, Priority::high, "Meera"} as a tuple when its caller immediately binds id, priority, and owner. Pair and tuple are useful vocabulary types, not mistakes. A named Ticket becomes worth creating when the relationship among values matters beyond one small expression.

C++ enum, struct and tuple traps that hide in short code

  • Same-typed positions can be swapped. std::pair<int, int>{30, 417} still compiles even if the intended contract was {ticket_id = 417, response_minutes = 30}. Use named fields when both integers need durable meaning.

  • Zero initialisation need not select the first enumerator. Priority starts at 1, so a value-initialised member can hold underlying value 0, which has no named enumerator. Give the member a valid default with struct Ticket { int id{}; Priority priority{Priority::low}; };, and validate external integers before conversion.

  • Structured bindings can copy. auto [id, priority, owner] = tupled; creates copies. Use const auto& for read-only access to the existing tuple, or auto& when mutation is intended.

  • A struct is not automatically packed wire data. C++ object layout can include padding, and this enum uses its declared underlying type. Serialise named fields explicitly instead of writing raw object bytes.

How exam-style and interview questions test C++ small-data types

Generic questions usually ask you to decide whether an enum conversion compiles, trace pair or tuple access and structured binding, or select a type for a stated lifetime and change requirement.

For a rapid trace, this C++17-or-later code prints 417 3:

auto [id, p] = std::pair{417, Priority::high};
std::cout << id << ' ' << static_cast<int>(p);

int raw = Priority::high; does not compile, while int raw = static_cast<int>(Priority::high); yields 3. For design, a tuple can suit a returned value used only in the next line. For a support ticket stored in a queue and later extended with owner = "Meera", prefer the named Ticket struct.

The short version and the next step

enum class names a finite state, pair names two positions, tuple names several positions, and struct names the concept and its fields. Rebuild Ticket 417 in all three record shapes, add "Meera", and compare the access expressions. The Coding & DSA Courses for Placements collection maps the broader track; for focused C++ concepts, MCQs, and coding practice, continue with the C++ Programming Course. If a reviewer must ask what position 2 means, the data probably deserves a field name.