C++ optional and variant Explained
Learn C++ optional and variant with simple examples: missing values versus zero, safe checks, one active type, get_if and a checked lookup report.
What you will learn
- Represent a missing integer without inventing a magic number.
- Check an optional before reading its value.
- Distinguish a present zero from no value.
- Read the active variant type with get_if.
- Keep invalid input separate from a valid search with no match.
Sometimes a valid answer is no answer
Suppose a function searches for a reading. It might find the number zero, or it might find no reading at all. Returning zero for both cases loses information. Returning minus one is also wrong when minus one is a valid reading. A special number used to mean something else is often called a sentinel. It works only when the program can clearly reserve that number and enforce the rule everywhere.
std::optional<int> expresses a simpler idea: there is an integer, or there is no integer. Zero and negative numbers remain ordinary present values. An empty optional does not secretly contain a readable default integer. You must check whether the value is present before using the dereference operator. This lesson uses explicit checks instead of relying on an exception to handle an expected missing result.
An optional owns its contained value when it is present. In these examples that value is a copied integer, not a borrowed pointer into the original vector. A returned optional<int> can therefore survive the local integer used to create it. This is different from returning a dangling view or pointer. Choose the result type according to what you need to express: presence, borrowed access, or some other clearly defined state.

A present zero is still present
The if checks presence, not whether the stored integer is nonzero.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Reading: 0
No reading
After editing inputs, calculate a new prediction; this reference describes only the starter.
Ask about presence before asking about the number
The first optional is initialized with zero, so it contains a value. Its if condition is true. The output therefore prints Reading: 0. The second optional is initialized with std::nullopt, the standard spelling for no value. Its presence check is false. The ! operator makes the missing branch run. Trace these two objects separately instead of treating them as two ways to store the same integer.
has_value is another clear spelling for the presence question. After a positive check, *reading accesses the contained integer. Do not dereference an empty optional to see what happens; that violates the operation's requirement. value() checks and can throw when empty, but exceptions are a later lesson. value_or supplies a fallback copy, which can be convenient, though it deliberately merges the missing case with whatever fallback number you choose.
Use a fallback only when that loss of distinction is acceptable to the task. For example, a display might show a missing count as zero by design. A measurement report may need to show Missing instead, because a measured zero is meaningful. The type gives you a way to keep the two states separate, but your later code must still respect the difference. A good result type helps only if you read it according to its meaning.
- reading contains the integer 0.
- Its presence check succeeds, so *reading is safe here.
- missing contains no integer because it was set to nullopt.
- The missing branch reports absence without reading a nonexistent value.
Return a checked value copy from a lookup
The function uses a one-based requested position but converts only after checking the valid range.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Found: 0
After editing inputs, calculate a new prediction; this reference describes only the starter.
A variant chooses one kind of value
optional answers whether one kind of value is present. std::variant answers a different question: which kind of value is active from a fixed list of alternatives? For example, variant<int, string> can currently hold an integer or a string. In our report, the integer is a found reading, while the string is a missing message. It does not hold both at once, and it is not an untyped box for any possible object.
get_if<int>(&reply) asks for a pointer to the active integer, if there is one. A null result means that integer alternative is not active. If it is present, the returned pointer borrows from reply and is valid only while the appropriate contained object remains alive. Do not keep that pointer across a change to another alternative. For the string branch, get_if<string> asks the matching question about text.
holds_alternative<int>(reply) can check the type without obtaining its value. std::get<int> is another access operation, but it throws if the expected alternative is not active. We start with get_if because the branch is visible and easy to follow. More general code often uses visit to handle alternatives; that is a useful next step after you can explain this two-type example without memorizing a long template expression.
Check the active type before reading it
The string alternative is active. Both accesses are checked, so the program never asks an inactive type for a value.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Message: Missing
After editing inputs, calculate a new prediction; this reference describes only the starter.
Keep different states clear and avoid extra machinery
Do not choose variant merely because it looks advanced. If the only question is whether an integer exists, optional<int> is often enough. If a small set of genuinely different result types matters, a variant can describe it. Our lab combines them to practice their different jobs, not because every lookup needs both layers. In real application code, use the smallest result model that clearly represents the required states.
A default-constructed variant usually constructs its first alternative when that type allows it. For variant<int, string>, that means an integer zero, not an automatically empty value. Initialize the intended state explicitly so the reader does not have to guess. There is also an exceptional valueless_by_exception state that can arise during some throwing changes of alternatives. It is not an ordinary missing-data marker; this lesson does not use it for a normal lookup result.
Keep parsing failure separate from absence. Text such as 2cats is not a valid requested position under our input rules. A well-formed integer position with no matching element is a completed lookup with a missing result. If you print the same success message for both, the student cannot tell whether the input was understood. Clear output and exit statuses are part of the interface, not decoration added after the type choices.
Look up a reading without losing a real zero
Read a count from zero through five, then that many readings between minus one hundred and one hundred, then a whole integer position. The position is a one-based request. A well-formed position outside the collected data gives no match, including zero or a negative number. Bad numeric syntax, overflow, a missing line or an out-of-range reading prints Invalid input with status one before reporting.
Write readingAt to return optional<int>. Check the signed position before converting to the vector's unsigned size type, then return a copy on a valid index. Convert the result into variant<int, string>: a found integer or the explicit string Missing. Use checked get_if branches to print Value or Message. Both found and missing results end with status zero because both are valid outcomes of an understood request.
Test a found zero, a negative reading, the first and last valid positions, empty data, and requests beyond either end. Explain why if(result) succeeds for a stored zero. Explain why a missing result is not a pointer you should dereference. Finally, describe when you would remove the variant layer and simply branch on optional in a real program. The purpose is to make state easier to understand, not to add types without a need.
Run the same program on your computer
Save one complete example as main.cpp. Choose your installed compiler and run only after a successful build. These are terminal commands, not C++ source. The in-page Run button needs the configured compiler sandbox.
macOS / Linux:
clang++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
# Or use g++ with the same options.
./lesson
Windows Developer Command Prompt:
cl /std:c++20 /EHsc /W4 main.cpp /Fe:lesson.exe
lesson.exeIndependent lab
Represent a found reading, including zero, separately from an absent lookup result.
Acceptance checklist
- Read count 0..5, bounded readings -100..100, and a whole integer one-based position.
- Reject bad required input as Invalid input with status 1 before reporting.
- Return optional<int> from a lookup that validates before unsigned conversion and indexing.
- Use variant<int, string> with an integer result or the explicit Missing message.
- Read with checked get_if branches; print Value or Message and return status 0 for either valid lookup outcome.
Need a hint?
- A position below one means no matching element, not a valid unsigned index.
- Check the optional's presence, not whether its stored integer is nonzero.
- Initialize the variant to the Missing string, then assign the found integer when present.
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
Value: 0
The count and readings are bounded; the requested position can be any valid int because positions outside the data are defined as missing.
optional<int> owns a value copy on success. A present zero passes the presence check just like any other integer.
The variant is explicitly initialized as a message and changes to an integer only for a found result. get_if checks each access instead of assuming the active alternative.
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.
- Represent a missing integer without inventing a magic number.
- Check an optional before reading its value.
- Distinguish a present zero from no value.
- Read the active variant type with get_if.
- Keep invalid input separate from a valid search with no match.
References and further reading
Original examples and exercises accompany these primary references. Tool-specific diagnostics and support depend on your installed version.