Posted inInformation Technology

std::optional Example and Explanation

collection of eyeglasses on shelves in store

std::optional is an attempt by the academics on the C++ standards committee to “fix” what they believe to be abhorrent behavior. As usual they created a committee quality result. To understand my disgust with what they created you have to understand that I’ve been programming on real computers since the early 1980s. Even then VAX BASIC had OPTIONAL and a much better implementation for OPTIONAL parameters. You can read more about it in this book. Here is a snippet.

Languages created in the 1970s all tended to have some form of OPTIONAL keyword and you just put in commas for the parameters you skipped. If the standards committee would have went the extra step it the result would have been far sweeter.

Academics: size_t vs. int

Academics have been sitting around telling us how great they are, having never worked in the real world supporting production code on real computers. They chastise and brow beat all developers who wish to use integers with strings instead of size_t. For those who don’t know, on most platforms size_t is an unsigned integer of some size.

The length of a string cannot be negative!

academics

Technically a string can be of negative length when rendering right to left text in a left to right editor, but that is a conversation for a different time.

You artificially limit the length of a string by using integer!

academics

Well, maybe. Again, technically we can have 128-bit integers on 64-bit systems and arbitrary length integers given Integer classes.

Emphatic belief by academics that they could be the only ones who were right lead to bastardizations like this with std::string. For those who have yet to experience such joy, when any of the “find” functions fail to find they return the length of the string because they cannot return -1. Now, you have to make an additional function call to obtain the length of the string then compare the two results to see if your find succeeded.

Adding Insult to Injury

The STL (Standard Template Library) has been around in various flavors for many decades now. While std::optional is the academics attempting to “fix” their complete hosing of the standard, they can’t fix std::string without completely braking the ABI. You can’t just presto-chango :

size_t find( ...)

to

std::optional<size_t> find ( ...)

because the signatures won’t match at dynamic link time.

The C++17 standard finally added machinery that could un-hose an avalanche of academic hosings that have happened over the past 3+ decades, . . . but . . . using it will break 3+ decades of code in production.

Know this. That major ABI break is coming and it will make Y2K look like one short debug session.

Previous Optional Parameter Work

The C++ language standard has dabbled with “optional parameters” for decades. You know them as “default parameters.”

void promptUser( const QString &promptText,
                     bool allowDirection = false, bool ctrlMSubstitution = false );

If the user doesn’t provide a value for direction or ctrlMSubstitution they “default” to the values provided in the prototype. This works well when you have a predictable default behavior. What, per chance, do you provide to indicate a boolean value was omitted? How do you indicate “unknown?”

std::optional was created to help with this, mainly because the academics painted themselves into a corner not using int for the string functions. They just can’t un-pooch their previous pooches without breaking everything.

The Code

cmake_minimum_required(VERSION 3.10)

# Set some basic project attributes
project (OptionalExample
	VERSION 0.1
	DESCRIPTION "OptionalExample to show std::optional")

set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)  

# This project will output an executable file
add_executable(${PROJECT_NAME} OptionalExample.cpp)

# Create a simple configuration header
configure_file(config.h.in config.h)

# Include the configuration header in the build
target_include_directories(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}")

This time I remembered to change the description at line 6 while adding lines 8 and 9. I didn’t remember for our string_view discussion.

#include <iostream>
#include <iomanip>
#include <string>
#include <optional>

#include "config.h"

using std::optional;

optional<double> divide_these( int first, int second);
void threeParameters( optional<int> parm1 = std::nullopt,
					  optional<double> parm2 = std::nullopt,
					  optional<std::string> str1 = std::nullopt);

const int WE_ARE_FINISHED {999};

int main(int argc, char **argv) {
	std::cout << "OptionalExample" << std::endl;
	std::cout << "Version " << OptionalExample_VERSION_MAJOR << "." << OptionalExample_VERSION_MINOR << std::endl;

	std::cout << "\n\n\nOur first test will prompt you for 2 integers to divide \n";
	std::cout << "in a loop until you type 999 as the first integer. Enter 0 as the\n";
	std::cout << "second integer to see hopw optional can work for return values: \n\n";

	bool finished = false;
	while (!finished)
	{
		int first {};
		int second {};

		std::cout << "Enter the number to be divided: ";
		std::cin >> first;
		if (first != WE_ARE_FINISHED)
		{
			std::cout << "\nEnter a number to divide the first by: ";
			std::cin >> second;
			const optional<double> dbl {divide_these( first, second)};
			if (dbl.has_value())
			{
				std::cout << "\n\nThe result is: " << std::setprecision(4) << dbl.value() << std::endl;
			}
			else
			{
				std::cout << "\n\nYou cannot divide by zero!!!!" << std::endl;
			}
		}
		else
		{
			finished = true;
		}
	}

	std::cout << "\n\nNow trying optional parameters\n" << std::endl;

	threeParameters();
	threeParameters( std::nullopt, std::nullopt, "This is a string");
	threeParameters( std::nullopt, 3.14159);
	threeParameters(8);

	return (EXIT_SUCCESS);
}

optional<double> divide_these( int first, int second)
{
	if (second == 0)
	{
		return (std::nullopt);
	}

	return (first/second);
}

void threeParameters( optional<int> parm1, optional<double> parm2, optional<std::string> str1)
{
	std::cout << "Three parameters called with:   ";

	if (parm1.has_value())
	{
		std::cout << "first parameter: " << parm1.value() << "   ";
	}

	if (parm2.has_value())
	{
		std::cout << "second parameter: " << std::setprecision(4) << parm2.value() << "    ";
	}

	if (str1.has_value())
	{
		std::cout << "str1:  " << str1.value();
	}

	if (!parm1.has_value() && !parm2.has_value() && !str1.has_value())
	{
		std::cout << "  no parameters\n";
	}

	std::cout << std::endl;
}

Before we go too far, I want to show you the output.

OptionalExample
Version 0.1



Our first test will prompt you for 2 integers to divide
in a loop until you type 999 as the first integer. Enter 0 as the
second integer to see hopw optional can work for return values:

Enter the number to be divided: 47

Enter a number to divide the first by: 3


The result is: 15
Enter the number to be divided: 45

Enter a number to divide the first by: 0


You cannot divide by zero!!!!
Enter the number to be divided: 999


Now trying optional parameters

Three parameters called with: no parameters

Three parameters called with: str1: This is a string
Three parameters called with: second parameter: 3.142
Three parameters called with: first parameter: 8

std::optional Return Discussion

optional<double> divide_these( int first, int second);
void threeParameters( optional<int> parm1 = std::nullopt,
					  optional<double> parm2 = std::nullopt,
					  optional<std::string> str1 = std::nullopt);

Our two function prototypes show quite a few uses of std::optional. The first has an optional return value. The second has three optional parameters. You will note that this is really just a specialized case of “default values.” Instead of being OPTIONAL as VAX BASIC and so many languages from the 1980s provided. The C++ standard has a ways to go to catch up to the 1980s.

Calling a function that returns a std::optional result can look a bit funky. Using the value might look a bit funkier still.

			const optional<double> dbl {divide_these( first, second)};
			if (dbl.has_value())
			{
				std::cout << "\n\nThe result is: " << std::setprecision(4) << dbl.value() << std::endl;
			}
			else
			{
				std::cout << "\n\nYou cannot divide by zero!!!!" << std::endl;
			}

Never use auto in production code!

We have to declare a matching std::optional entity to receive the value. I declared it const because I don’t want to mess around with changing its value (or lack thereof). To see if we had a value we use has_value(). Yes, many/most compilers will also convert a std::optional entity to a bool for a test condition, but

if (dbl)

would be confusing as all get out to new or very old developers. When a std::optional has a value it is retrieved by value(). Don’t worry if it takes you a while to get used to seeing what your mind believes to be a native data type with a .value() behind it. That is normal. The else of the above if statement is interesting only from the standpoint we have successfully tested for something that doesn’t exist.

std::optional Parameter Discussion

Here is where much of my beginning rant will make sense.

	threeParameters();
	threeParameters( std::nullopt, std::nullopt, "This is a string");
	threeParameters( std::nullopt, 3.14159);
	threeParameters(8);

They aren’t “optional.” You can’t skip them. You can only “not provide” trailing ones. What I really don’t like about this situation is if threeParameters() was an overloaded method/function. You can’t be certain which one got used. With VAX BASIC and 3GLs of the 1960-1980s you had documentation in the code of which one got called.

threeParameters(,,);
threeParameters( ,, "This is a string");
threeParameters( , 3.14159 ,);
threeParameters(8 ,,);

Then again, many of those languages didn’t allow for overloading of functions.

About the only thing the standards committee did was give us a universal invalid value. They still haven’t admitted that making size_t an unsigned integer was a bullshit thing to do. It has existed so long it cannot really be undone at this point.

Summary

Now that we have a universal invalid value via std::optional you can quit throwing away one value in a fixed range as was too often done to indicate error. The academics, however, still need to admit they’ve been wrong for decades.

Roland Hughes started his IT career in the early 1980s. He quickly became a consultant and president of Logikal Solutions, a software consulting firm specializing in OpenVMS application and C++/Qt touchscreen/embedded Linux development. Early in his career he became involved in what is now called cross platform development. Given the dearth of useful books on the subject he ventured into the world of professional author in 1995 writing the first of the "Zinc It!" book series for John Gordon Burke Publisher, Inc.

A decade later he released a massive (nearly 800 pages) tome "The Minimum You Need to Know to Be an OpenVMS Application Developer" which tried to encapsulate the essential skills gained over what was nearly a 20 year career at that point. From there "The Minimum You Need to Know" book series was born.

Three years later he wrote his first novel "Infinite Exposure" which got much notice from people involved in the banking and financial security worlds. Some of the attacks predicted in that book have since come to pass. While it was not originally intended to be a trilogy, it became the first book of "The Earth That Was" trilogy:
Infinite Exposure
Lesedi - The Greatest Lie Ever Told
John Smith - Last Known Survivor of the Microsoft Wars

When he is not consulting Roland Hughes posts about technology and sometimes politics on his blog. He also has regularly scheduled Sunday posts appearing on the Interesting Authors blog.