C++ Structs and enum class Explained
Learn C++ structs and enum class with aggregate initialization, member access, value copying, explicit text parsing and a task-status modeling lab.
What you will learn
- Group related values into a named struct instead of parallel arrays.
- Initialize and access record members explicitly.
- Use enum class for named domain states without implicit integer conversion.
- Parse external text through an explicit accepted-state mapping.
- Explain value copying and the limits of public mutable records.
A struct keeps the values for one thing together
A task title and task status describe the same record. Keeping titles in one vector and statuses in another makes their association depend on matching positions forever. Inserting into one collection but not the other can silently assign the wrong status to a title. A struct groups related members into one named type, so each element in a vector<Task> can carry its own coherent set of values together.
Declare struct Task with a braced member list and a semicolon after the closing brace. Members are public by default for a struct. Access a member on an object with the dot operator, such as task.title. The member's type still matters: a string manages text, while a dedicated status type names the permitted concepts. Grouping fields does not automatically explain their units, valid ranges or relationships, so document those contracts alongside the declaration.
For this lesson a Task is a small value record, not an object with a custom constructor or hidden allocation policy. Its std::string member handles its own owned text. We do not need new, delete, inheritance or a base class to represent a task. Start with the simplest model that expresses the data, then introduce encapsulation in the next lesson when preserving rules across every possible mutation becomes the central problem.
Replace magic status numbers with enum class
enum class Status { todo, doing, done }; declares a distinct type with scoped enumerator names. Write Status::done to identify the done state instead of relying on an unexplained number two or an arbitrary spelling scattered throughout the application. The scope allows different enumeration types to use similar member names without putting those enumerators directly into the surrounding scope. The type also makes function parameter intentions easier to read.
A scoped enum does not implicitly become an integer just because its representation is integral. You cannot assign an unrelated integer to Status and assume the compiler has validated a workflow state. The deliberately failing example demonstrates that boundary. An explicit cast can bypass some type friction, but it is not a parser and does not establish that an external number corresponds to a named business state. Prefer an explicit accepted mapping for user input.
Do not claim an enum class makes unnamed values impossible. Explicit conversions and lower-level interfaces can produce values outside your named states. A display function should therefore have a documented fallback or another explicit policy for unexpected input. Our statusName function covers all named cases and returns Unknown after the switch. That makes the function total for a supplied Status value without advertising the fallback as validation of malformed external data.
Copy a task without sharing its owned title
The string and enum are value members. Updating the copied record does not change the original record.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Write tests
Review tests
Original still todo: 1
After editing inputs, calculate a new prediction; this reference describes only the starter.
Repair an integer-to-status assignment
Intentional compile failure. Replace the integer with the named Status::done enumerator rather than using a cast to hide an unvalidated mapping.
Predict the output, edit the source, then select Run code.
Initialize records deliberately and understand defaults
The simple Task declaration here is an aggregate, so braces can supply its members in declaration order. Task{"Write tests", Status::todo} initializes title and status together. A default member initializer such as Status status{Status::todo} makes an omitted status deliberate in this record shape. Do not mentally reorder positional initializer values based on their apparent meaning; the declared member order is the contract for ordinary aggregate list initialization.
For numeric members without defaults, a local declaration with no initializer can leave values uninitialized. Using braces or explicit member defaults avoids that mistake. std::string has its own initialization behavior, but that does not mean every other member magically gains a sensible value. Review each member's initialization individually. A record with an initialized string and an indeterminate integer still has an unsafe integer read if you use it before assignment.
C++20 has designated initialization for suitable aggregates, but it has restrictions and is not a reason to ignore member-order rules. This lesson uses the simpler positional form and explicit defaults. As records grow, names, validation and construction interfaces become more important than compressing object creation onto one line. The next class lesson will show how a constructor can establish rules instead of asking every caller to remember the same validation sequence.
Convert external text through a deliberate parser
The lab reads a title line and a status line. Accepted status spellings are exactly todo, doing and done; the comparison is case-sensitive and does not trim or normalize text. The parser assigns an output Status only when a spelling matches. It returns false otherwise, leaving the caller responsible for rejecting the record. This connects the parsing result to the permitted names without treating arbitrary text as a trusted domain value.
Parsing and display are different functions. parseStatus handles external spellings and reports whether conversion succeeded; statusName maps a known stored state to a presentation label. A display function returning Unknown is not permission to accept every status input. The caller must check parsing success before constructing and reporting the accepted Task. Keeping both directions explicit also makes later changes, such as translated labels, less likely to accidentally redefine the input protocol.
A struct alone does not enforce a nonempty title. Its public string can be changed after successful input validation, and an empty aggregate could still be constructed elsewhere. Our lab validates a title of one through forty bytes with at least one byte other than an ordinary space. This is a narrow text policy, not Unicode normalization, grapheme counting or a universal definition of blank text. In particular, byte length is not a count of user-perceived characters.
Value modeling is useful even before full encapsulation
A copy of this Task owns an independent std::string and copies the enum value. Changing the copy's title does not affect the original title. That statement relies on the current member types; it would need reconsideration if the record instead stored pointers to shared mutable objects. Passing const Task& to a display function avoids unnecessary copies while preventing mutation through that reference. It does not make the original record globally immutable.
The report prints Title and Status, using Todo, Doing or Done for accepted states. Missing required lines produce Invalid input, a rejected title produces Invalid title, and an unsupported status spelling produces Invalid status; each failure returns status one without a success report. Extra input after the two required lines is not another task and remains unread. The explicit branches make failure transcripts testable instead of silently repairing the user's input.
Test every accepted status, an unknown word, a capitalization mismatch, an empty title, an ordinary-space-only title, the forty-byte boundary and a forty-one-byte title. Also test end-of-input after the title and a valid status on a final line without a newline. Then explain which rules the public struct itself cannot enforce. That reflection prepares you for encapsulation: grouping data improves coherence, while protecting invariants requires an interface that controls construction and mutation.
Compile and test outside the lesson
The compiler-backed editors support complete single-file programs when the execution service is available. Otherwise save one program as main.cpp and run these commands with your configured compiler. Supply all standard input before Run; this is not an interactive terminal.
macOS / Linux (choose your installed compiler):
clang++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
# Or: g++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
./lesson
Windows Developer Command Prompt:
cl /std:c++20 /EHsc /W4 main.cpp /Fe:lesson.exe
lesson.exeIndependent lab
Parse a two-line task record into a struct with a scoped status, then print only a validated task.
Acceptance checklist
- Define Status with todo, doing and done, and Task with a title and status.
- Read title and status as two complete lines; missing required input prints Invalid input and status 1.
- Accept a 1–40-byte title containing something other than ordinary spaces; otherwise print Invalid title and status 1.
- Accept exactly todo, doing or done through an explicit parser; otherwise print Invalid status and status 1.
- Print Title and the corresponding Todo/Doing/Done label only after validation; do not cast arbitrary integers or strings into a state.
Need a hint?
- A bool parser with a Status& output can distinguish accepted mapping from failure.
- Validate title.empty, title.size and find_first_not_of(' ') separately from status parsing.
- A switch over Status makes the output labels visible; keep an explicit unexpected-value fallback.
Write and run your lab
Build your own solution before revealing the reference. Use the input field for normal, boundary and invalid cases; this runner executes your program but does not grade it.
Predict the output, edit the source, then select Run code.
Your lab evidence and notes
Record predictions, actual output, commands and the cause of each repair. Code is plain text: this scratchpad does not execute or grade C++.
Loading local notes…
Review your acceptance checklist and quiz explanations before marking this lesson complete. This is self-review, not a grade or certification.
Reveal the explained lab solution
Reference lab solution
Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.
Predict the output, edit the source, then select Run code.
Expected output
Title: Write tests
Status: Doing
Text parsing happens before constructing the accepted record. A rejected status never becomes a success report through a fallback label.
The title limit is a documented byte policy with ordinary-space detection, not a promise about Unicode characters or all whitespace.
The struct groups values and the enum expresses named states, but public fields still do not enforce title invariants across arbitrary future mutations.
Review before moving on
Explain the lesson's objectives in your own words, compare your lab evidence with the acceptance checklist, and keep the first failing case alongside your repair. Completing a page is not proof that every C++ feature or input is supported.
- Group related values into a named struct instead of parallel arrays.
- Initialize and access record members explicitly.
- Use enum class for named domain states without implicit integer conversion.
- Parse external text through an explicit accepted-state mapping.
- Explain value copying and the limits of public mutable records.
References and further reading
Original examples and exercises accompany these primary references. Tool-specific diagnostics and support depend on your installed version.