DEV Community

Swastik Baranwal
Swastik Baranwal

Posted on

Modern C++: std::optional

Hey, I am finally started to continue my series from less-known features of Modern C++.

The Problem

There are times when you want to return a potential value like NULL or some error to indicate a problem, for example.

int check_name(std::string name) {
     if (name == "Tom")
         return 0;
     else
         return -1; //  Used to indicate invalid input 
}
Enter fullscreen mode Exit fullscreen mode

But, when calling this function one would have to check if -1 is returned or not according to the input.

code = check_name("Same")
if (code == -1) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Which makes the code unreadable and inconsistent if other values are returned in rest functions.

That's where std::optional comes to the rescue!

std::optional

std::optional is a feature introduced in C++ 17 which is used to represent optional values i.e. the possibility of having a value or not having a value in a type-safe manner.

Syntax

std::optional<T> opt_var;
Enter fullscreen mode Exit fullscreen mode

where T represents the optional value to be stored.

Methods:

  • has_value: checks whether the object contains a value
  • value: returns the contained value
  • value_or: returns the contained value if available, another value otherwise

Usage

  • Check if one number can divide another number or not
#include <iostream>
#include <optional>

std::optional<int> divide(int num1, int num2) {
    if (num2 != 0) {
        return num1/num2;
    }
    return std::nullopt; // Indicates no type-safe value
}

int main() {
    auto result = divide(10, 2); // To infer into std::optional<int>, used to save time 
    if (result.has_value()) {  // has_value checks if a type-value is returned
        std::cout << "Result: " << result.value() << std::endl; // value returns the value as function arguments are correct
    } else {
        std::cout << "Division by zero" << std::endl;
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
  • Simplifying error handling; in scenarios where you want to read a file, parse strings then convert them into integers and format them.
#include <iostream>
#include <fstream>
#include <optional>
#include <string>

std::optional<std::string> try_reading_file(const std::string& filename) { // const to make sure it doesn't get modified
    std::ifstream file(filename); // std::ifstream for reading files 
    if (!file.is_open()) {  // if file cannot be opened
        return std::nullopt;
    }

    std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); //Written the content inside the file
    return content;
}

int main() {
    const std::string filename = "exmpl.txt";

    // try to read the file
    auto fileContent = try_reading_file(filename);

    if (fileContent.has_value()) {
        std::cout << "File content:\n" << fileContent.value() << std::endl;
    } else {
        std::cerr << "Error: Could not open the file " << filename << std::endl;
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
  • Checking and changing the default value
#include <optional>
#include <iostream>

int main() {
    std::optional<int> value;

    int result = value.value_or(42);  // Assigns 42 if value is not present

    std::cout << "Result: " << result << std::endl;

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

These are the most common usages of std::optional and more usages can seen by reading C++ code of many open source organizations.

Comparison from other languages

  • Rust has an Option enum to implement optionals
  • Go doesn't have optional so it uses a type called error for error handling
  • Java also implements optional via generics/classes
  • V implements option for returning optional values and result for resultant values.

Conclusion

Whenever you possibly have to denote an answer or value-or-not-value, it is recommended to use std::optional unless you are working with legacy code where integers are used to represent. It also increases the capacity to describe complex problems, readability and also handle errors gracefully.

Code

All the code used in this article is available in this repo.

Top comments (3)

Collapse
 
pauljlucas profile image
Paul J. Lucas

You neglected to mention that it's not a good idea to have std::optional<bool>. The problem is that it's too easy to write (wrong) code like this:

std::optional<bool> b;
// ...
if ( b ) {
    // ...
Enter fullscreen mode Exit fullscreen mode

The problem specifically is std::optional<T>::operator bool(). In the above code, the if checks whether b has a value — but it looks like it checks whether b is true. So b could be false yet the above if would evaluate to true.

Collapse
 
delta456 profile image
Swastik Baranwal

Oh, I had thought of mentioning this as well but I really forgot and proceeded further! I will add this and credit you.

Thanks!

Collapse
 
mike4online profile image
Michael Litwak

You can instead return a std::expected, which returns either an expected value or an error value. The error value can be a different type than your expected value, e.g. an integer, string, tuple or other object. So if an error does occur, the unexpected object can provide the details about the particular error.

std::expected is in C++23; however, the reference implementation tl::expected is available for C++11 or newer. tl::expected includes a set of "monadic" or "functional style" extensions which are very powerful but best understood by reviewing examples. These functional extensions are also making their way into the standard.