Lesson 2
Optional values with std::optional
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:

C++
1#include <optional> 2#include <iostream> 3 4int main() { 5 // An optional containing an integer value 6 std::optional<int> val = 42; // Initialized with a value 7 // An empty optional 8 std::optional<int> emptyVal; // Initialized without a value (empty) 9 return 0; 10}

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:

C++
1#include <optional> 2#include <iostream> 3 4int main() { 5 std::optional<int> val = 42; // Initialized with a value 6 7 // Check if val has a value 8 if (val.has_value()) { 9 // Access and print the value 10 std::cout << "Value inside val: " << val.value() << std::endl; // Output: Value inside val: 42 11 } else { 12 std::cout << "val is empty" << std::endl; 13 } 14 15 std::optional<int> emptyVal; // Initialized without a value (empty) 16 17 // Check if emptyVal has a value 18 if (emptyVal.has_value()) { 19 std::cout << "Value inside emptyVal: " << emptyVal.value() << std::endl; 20 } else { 21 std::cout << "emptyVal is empty" << std::endl; // Output: emptyVal is empty 22 } 23 24 return 0; 25}

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:

C++
1#include <optional> 2#include <iostream> 3 4int main() { 5 std::optional<int> val = 42; // Initialized with a value 6 val.reset(); // Reset val to an empty state 7 8 // Check if val no longer has a value 9 if (!val.has_value()) { 10 std::cout << "val is now empty" << std::endl; // Output: val is now empty 11 } 12 13 return 0; 14}

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.

C++
1#include <optional> 2#include <vector> 3#include <string> 4 5std::optional<int> findStringIndex(const std::vector<std::string>& strings, const std::string& target) { 6 for (int i = 0; i < strings.size(); ++i) { 7 if (strings[i] == target) { 8 return i; // Return optional containing the index of the found string 9 } 10 } 11 return std::nullopt; // Return empty optional if not found 12}
  • 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:

C++
1#include <optional> 2#include <iostream> 3#include <vector> 4#include <string> 5 6std::optional<int> findStringIndex(const std::vector<std::string>& strings, const std::string& target) { 7 for (int i = 0; i < strings.size(); ++i) { 8 if (strings[i] == target) { 9 return i; // Return optional containing the index of the found string 10 } 11 } 12 return std::nullopt; // Return empty optional if not found 13} 14 15int main() { 16 std::vector<std::string> myStrings = {"apple", "banana", "cherry"}; 17 std::optional<int> result = findStringIndex(myStrings, "banana"); 18 19 // Check if result has a value 20 if (result.has_value()) { 21 std::cout << "Found at index: " << result.value() << std::endl; // Output: Found at index: 1 22 } else { 23 std::cout << "String not found" << std::endl; 24 } 25 26 return 0; 27}

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.

C++
1#include <optional> 2#include <iostream> 3 4int main() { 5 std::optional<int> val = 42; // Initialized with a value 6 std::optional<int> emptyVal; // Empty optional 7 8 // Get the value or default 9 int value1 = val.value_or(-1); // Will return 42 10 int value2 = emptyVal.value_or(-1); // Will return -1 11 12 std::cout << "Value in val: " << value1 << std::endl; // Output: Value in val: 42 13 std::cout << "Value in emptyVal: " << value2 << std::endl; // Output: Value in emptyVal: -1 14 15 return 0; 16}

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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.