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.