Lesson Introduction

Handling optional values is crucial in C++ to prevent runtime errors and undefined behavior. Imagine you're looking for a specific item in a list. If it isn't found, how do you handle that? Traditionally, you might use pointers or sentinel values, but these can be error-prone. The std::optional class template in C++17 offers a cleaner, type-safe alternative.

The goal of this lesson is to help you understand and use std::optional to handle values that may or may not be present. We'll cover its creation, usage, and practical applications with real examples.

Introduction to `std::optional`

So, what is std::optional? Think of it as a container that may or may not hold a value. It’s useful when a function might not always return a meaningful value. For example, if you try to find an item in a list and it's not there, instead of returning a null pointer or a special sentinel value, you can return an std::optional.

Creating Optional Values

Creating an std::optional value is simple:

#include <optional>
#include <iostream>

int main() {
    // An optional containing an integer value
    std::optional<int> val = 42; // Initialized with a value
    // An empty optional
    std::optional<int> emptyVal; // Initialized without a value (empty)
    return 0;
}

In this code snippet:

  • val is an std::optional initialized with the integer value 42.
  • emptyVal is an empty std::optional of type int.
Accessing Values in `std::optional`

Accessing the value contained in an std::optional can be done safely by first checking if it contains a value using the .has_value() member function. If the std::optional does contain a value, the .value() member function can be used to access the contained value. Here's an example to illustrate these concepts:

#include <optional>
#include <iostream>

int main() {
    std::optional<int> val = 42; // Initialized with a value

    // Check if val has a value
    if (val.has_value()) {
        // Access and print the value
        std::cout << "Value inside val: " << val.value() << std::endl; // Output: Value inside val: 42
    } else {
        std::cout << "val is empty" << std::endl;
    }

    std::optional<int> emptyVal; // Initialized without a value (empty)

    // Check if emptyVal has a value
    if (emptyVal.has_value()) {
        std::cout << "Value inside emptyVal: " << emptyVal.value() << std::endl;
    } else {
        std::cout << "emptyVal is empty" << std::endl; // Output: emptyVal is empty
    }

    return 0;
}

Using .value() without checking .has_value() will throw an exception if the std::optional is empty. To avoid this, always check with .has_value() before accessing the value.

Alternatively, you can access the value with the * operator. Instead of val.value(), you can use *val.

Resetting Optional Values

To reset an std::optional to an empty state:

#include <optional>
#include <iostream>

int main() {
    std::optional<int> val = 42; // Initialized with a value
    val.reset(); // Reset val to an empty state
    
    // Check if val no longer has a value
    if (!val.has_value()) {
        std::cout << "val is now empty" << std::endl; // Output: val is now empty
    }

    return 0;
}

In this code snippet:

  • val is an std::optional initialized with the integer value 42.
  • The reset function is called on val, setting it to the empty state.
  • The if statement checks if val is now empty and prints the corresponding message.
Returning `std::optional` from Functions

Returning an std::optional from a function is useful when a function might not always have a meaningful value to return. Let's walk through the findStringIndex function that returns an index of a target string in the provided vector of strings.

#include <optional>
#include <vector>
#include <string>

std::optional<int> findStringIndex(const std::vector<std::string>& strings, const std::string& target) {
    for (int i = 0; i < strings.size(); ++i) {
        if (strings[i] == target) {
            return i; // Return optional containing the index of the found string
        }
    }
    return std::nullopt; // Return empty optional if not found
}
  • It iterates over a vector of strings.
  • If the target string is found, it returns an std::optional containing the string.
  • If not found, it returns std::nullopt, representing an empty optional.
Using Optional Values in Code

Here is how we utilize it:

#include <optional>
#include <iostream>
#include <vector>
#include <string>

std::optional<int> findStringIndex(const std::vector<std::string>& strings, const std::string& target) {
    for (int i = 0; i < strings.size(); ++i) {
        if (strings[i] == target) {
            return i; // Return optional containing the index of the found string
        }
    }
    return std::nullopt; // Return empty optional if not found
}

int main() {
    std::vector<std::string> myStrings = {"apple", "banana", "cherry"};
    std::optional<int> result = findStringIndex(myStrings, "banana");

    // Check if result has a value
    if (result.has_value()) {
        std::cout << "Found at index: " << result.value() << std::endl; // Output: Found at index: 1
    } else {
        std::cout << "String not found" << std::endl;
    }

    return 0;
}

In this code snippet:

  • A vector of strings myStrings is created and populated with fruit names.
  • The findStringIndex function is called with myStrings and the target string "banana".
  • The if statement checks if result contains a value, printing the found string if so.
  • If result is empty, it prints "String not found."
Using `value_or` Function

The value_or function can be used to provide a default value when the std::optional is empty. This simplifies code and reduces the need for manual checks.

#include <optional>
#include <iostream>

int main() {
    std::optional<int> val = 42; // Initialized with a value
    std::optional<int> emptyVal; // Empty optional

    // Get the value or default
    int value1 = val.value_or(-1); // Will return 42
    int value2 = emptyVal.value_or(-1); // Will return -1

    std::cout << "Value in val: " << value1 << std::endl; // Output: Value in val: 42
    std::cout << "Value in emptyVal: " << value2 << std::endl; // Output: Value in emptyVal: -1

    return 0;
}

In this code snippet:

  • value_or returns the contained value if the std::optional is not empty. Otherwise, it returns the provided default value.
  • value1 will contain 42 as val has a value.
  • value2 will contain -1 as emptyVal is empty.
Lesson Summary

To sum up, you've learned about std::optional, a type-safe way to handle values that may or may not be present. We've covered:

  • What std::optional is and its significance.
  • How to create, return, and use optional.
  • Error handling using optional.
  • How to manage optional with has_value and value methods.
  • Using the value_or function to simplify code.

Next, you'll put these concepts into practice. Get ready to solidify your understanding through hands-on exercises!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal