SovranCode
Modern C++ Lesson 36
Course contents50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
Modern C++50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
PREVIOUS LESSONC++20 Ranges: Views, filter and transform
NEXT LESSONC++ Exceptions: try, catch and Safe Cleanup
C++ foundations · Lesson 36 About 46 min with practice

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.

Before you begin

Know functions, strings, vectors and checked lookup. We introduce one small optional result before adding a two-type variant.

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.

An optional integer holding zero is shown as present, beside a separate empty optional. A variant with integer and string alternatives has only its hello string selected as the active value, not both types at once.
Presence is separate from the integer's value: zero can be a real answer. A variant normally holds one active alternative from its declared list; check which one is active before reading it.
C++ PRACTICE RUNNER

A present zero is still present

The if checks presence, not whether the stored integer is nonzero.

#include <iostream>
#include <optional>
int main() {
    const std::optional<int> reading{0};
    const std::optional<int> missing = std::nullopt;
    if (reading) std::cout << "Reading: " << *reading << '\n';
    if (!missing) std::cout << "No reading\n";
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.
C++ PRACTICE RUNNER

Return a checked value copy from a lookup

The function uses a one-based requested position but converts only after checking the valid range.

#include <iostream>
#include <optional>
#include <vector>
std::optional<int> readingAt(const std::vector<int>& values, int position) {
    if (position < 1 || static_cast<std::size_t>(position) > values.size())
        return std::nullopt;
    return values[static_cast<std::size_t>(position - 1)];
}
int main() {
    const std::vector<int> values{0, -3, 8};
    const auto found = readingAt(values, 1);
    if (found) std::cout << "Found: " << *found << '\n';
    else std::cout << "Missing\n";
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

C++ PRACTICE RUNNER

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.

#include <iostream>
#include <string>
#include <variant>
int main() {
    const std::variant<int, std::string> reply{std::string{"Missing"}};
    if (const auto* number = std::get_if<int>(&reply))
        std::cout << "Value: " << *number << '\n';
    else if (const auto* message = std::get_if<std::string>(&reply))
        std::cout << "Message: " << *message << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

LOCAL TERMINAL COMMANDS

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.exe
APPLY THE CONCEPT

Independent 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?
  1. A position below one means no matching element, not a valid unsigned index.
  2. Check the optional's presence, not whether its stored integer is nonzero.
  3. Initialize the variant to the Missing string, then assign the found integer when present.
C++ PRACTICE RUNNER

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.

#include <iostream>
#include <string>
#include <sstream>
#include <limits>

int main() {
    // Implement the acceptance checklist above.
    return 0;
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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
C++ PRACTICE RUNNER

Reference lab solution

Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.

#include <iostream>
#include <sstream>
#include <string>
bool readInteger(int& value) {
    std::string line;
    if (!std::getline(std::cin, line)) return false;
    std::istringstream parser{line};
    if (!(parser >> value)) return false;
    parser >> std::ws;
    return parser.eof();
}
#include <optional>
#include <variant>
#include <vector>
std::optional<int> readingAt(const std::vector<int>& values, int position) {
    if (position < 1 || static_cast<std::size_t>(position) > values.size())
        return std::nullopt;
    return values[static_cast<std::size_t>(position - 1)];
}
int main() {
    int count{};
    if (!readInteger(count) || count < 0 || count > 5) {
        std::cout << "Invalid input\n"; return 1;
    }
    std::vector<int> values;
    for (int i = 0; i < count; ++i) {
        int value{};
        if (!readInteger(value) || value < -100 || value > 100) {
            std::cout << "Invalid input\n"; return 1;
        }
        values.push_back(value);
    }
    int position{};
    if (!readInteger(position)) {
        std::cout << "Invalid input\n"; return 1;
    }
    const auto result = readingAt(values, position);
    std::variant<int, std::string> reply{std::string{"Missing"}};
    if (result) reply = *result;
    if (const auto* number = std::get_if<int>(&reply))
        std::cout << "Value: " << *number << '\n';
    else if (const auto* message = std::get_if<std::string>(&reply))
        std::cout << "Message: " << *message << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

  • C++ draft: optional
  • C++ draft: variant
  • C++ draft: variant access
KNOWLEDGE CHECK

Check your understanding

Answer all five questions, then review the explanations. Answers are not saved or synced; completion is a separate self-review choice.

01optional<int> contains zero. Is a value present?
02What does nullopt mean?
03How many alternatives does a normal variant hold at once?
04get_if<int> returns nullptr. What does that tell you?
05A valid position has no match. Is that malformed input here?
PREVIOUS LESSONC++20 Ranges: Views, filter and transform
NEXT LESSONC++ Exceptions: try, catch and Safe Cleanup
ON THIS PAGELearning objectivesSometimes a valid answer is no answerAsk about presence before asking about the numberA variant chooses one kind of valueKeep different states clear and avoid extra machineryLook up a reading without losing a real zeroIndependent labReviewKnowledge checkReferences
Course overview