Skip to content

Additional Vocabulary Types

Vocabulary types are types that you are likely to use all the time, just as much as primitive types such as int and double. They are often used to build more complex types. Using vocabulary types makes your code safer, more efficient, and easier to write, read, and maintain. Examples of vocabulary types discussed earlier in this book are vector, optional, string, unique_ptr, shared_ptr, and so on.

This chapter starts the discussion with two additional vocabulary types: variant and any. It then continues with a more in-depth discussion of tuples, a generalization of pairs, and their operations. Next is monadic operation support for optionals, which makes chaining operations on optionals so much easier. This is because you won’t have to verify whether an optional is empty before applying a next operation on it. The chapter finishes with a discussion of expected, which is a data type capable of storing either a value of an expected type or an error value. The type used to represent the error can be different than the type of the value.

std::variant, defined in <variant>, can hold a single value of one of a given set of types. When you define a variant, you must specify the types it can potentially contain. For example, the following code defines a variant that can contain an integer, a string, or a floating-point value, but only one at a time:

variant<int, string, float> v;

The template type arguments for a variant must be unique; for example, variant<int,int> is invalid. A default-constructed variant contains a default-constructed value of its first type, int in the case of the variant v. If you want to be able to default construct a variant, you must make sure that the first type of the variant is default constructible. For example, the following does not compile because Foo is not default constructible:

class Foo { public: Foo() = delete; Foo(int) {} };
class Bar { public: Bar() = delete; Bar(int) {} };
variant<Foo, Bar> v;

In fact, neither Foo nor Bar is default constructible. If you still want to be able to default construct such a variant, then you can use std::monostate, a well-behaved empty alternative, as the first type of the variant:

variant<monostate, Foo, Bar> v;

You can use the assignment operator to store something in a variant:

variant<int, string, float> v;
v = 12;
v = 12.5f;
v = "An std::string"s;

A variant can contain only one value at any given time. So, with these three assignment statements, first the integer 12 is stored in the variant, then the variant is modified to contain a single floating-point value, and lastly, the variant is modified again to contain a single string value.

You can use the index() member function to get the zero-based index of the value’s type that is currently stored in the variant, and you can use the std::holds_alternative() function template to figure out whether a variant currently contains a value of a certain type:

println("Type index: {}", v.index());
println("Contains an int: {}", holds_alternative<int>(v));

The output is as follows:

Type index: 1
Contains an int: false

Use std::get<index>() or get<T>() to retrieve the value from a variant, where index is the zero-based index of the type you want to retrieve, and T is the type you want to retrieve. These functions throw a bad_variant_access exception if you are using the index of a type, or a type, that does not match the current value in the variant:

println("{}", get<string>(v));
try {
println("{}", get<0>(v));
} catch (const bad_variant_access& ex) {
println("Exception: {}", ex.what());
}

This is the output:

An std::string
Exception: bad variant access

To avoid exceptions, use the std::get_if<index>() or get_if<T>() helper function. These functions accept a pointer to a variant and return a pointer to the requested value, or nullptr on error:

string* theString { get_if<string>(&v) };
int* theInt { get_if<int>(&v) };
println("Retrieved string: {}", (theString ? *theString : "n/a"));
println("Retrieved int: {}", (theInt ? to_string(*theInt) : "n/a"));

Here is the output:

Retrieved string: An std::string
Retrieved int: n/a

An std::visit() helper function is available that you can use to apply the visitor pattern to a variant. A visitor has to be a callable, e.g., a function, a lambda expression, or a function object, that can accept any type that may be stored in the variant. A first example just uses a generic lambda, which can accept any type, as the callable passed as the first argument to visit():

visit([](auto&& value) { println("Value = {}", value); }, v);

The output is as follows:

Value = An std::string

If you want to handle each type stored in the variant in a different way, then you can write your own visitor class. Suppose you have the following visitor class that defines a number of overloaded function call operators, one for each possible type in the variant. This implementation marks all its function call operators as static (possible since C++23), as they don’t require access to any non-static member functions or data members of MyVisitor.

class MyVisitor
{
public:
static void operator()(int i) { println("int: {}", i); }
static void operator()(const string& s) { println("string: {}", s); }
static void operator()(float f) { println("float: {}", f); }
};

You can use this with std::visit() as follows:

visit(MyVisitor{}, v);

The result is that the appropriate overloaded function call operator is called based on the current value stored in the variant. The output for this example is as follows:

string: An std::string

A variant cannot store an array, and as with optional introduced in Chapter 1, “A Crash Course in C++ and the Standard Library,” it cannot store references. You can store either pointers or instances of reference:wrapper<T> or reference:wrapper<const T> (see Chapter 18, “Standard Library Containers”).

std::any, defined in <any>, is a class that can contain a single value of any type. You can create an instance with an any constructor or with the std::make_any() helper function. Once it is constructed, you can ask an any instance whether it contains a value and what the type of the contained value is. To get access to the contained value, you need to use any_cast(), which throws an exception of type bad_any_cast in the case of failure. Here is an example:

any empty;
any anInt { 3 };
any aString { "An std::string."s };
println("empty.has_value = {}", empty.has_value());
println("anInt.has_value = {}\n", anInt.has_value());
println("anInt wrapped type = {}", anInt.type().name());
println("aString wrapped type = {}\n", aString.type().name());
int theInt { any_cast<int>(anInt) };
println("{}", theInt);
try {
int test { any_cast<int>(aString) };
println("{}", test);
} catch (const bad_any_cast& ex) {
println("Exception: {}", ex.what());
}

The output is as follows. Note that the wrapped type of aString is compiler dependent.

empty.has_value = false
anInt.has_value = true
anInt wrapped type = int
aString wrapped type = class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>
3
Exception: Bad any_cast

You can assign a new value to an any instance and even assign a new value of a different type:

any something { 3 }; // Now it contains an integer.
something = "An std::string"s; // Now the same instance contains a string.

Instances of any can be stored in Standard Library containers. This allows you to have heterogeneous data in a single container. The only downside is that you have to perform explicit any_casts to retrieve specific values, as the following example demonstrates:

vector<any> v;
v.push_back(42);
v.push_back("An std::string"s);
println("{}", any_cast<string>(v[1]));

As with optional and variant, you cannot store references in an any instance. You can again store either pointers or instances of reference:wrapper<T> or reference:wrapper<const T>.

The std::pair class, defined in <utility> and introduced in Chapter 1, can store exactly two values, each with a specific type. The type of each value must be known at compile time. Here is a short reminder:

pair<int, string> p1 { 16, "Hello World" };
pair p2 { true, 0.123f }; // Using CTAD.
println("p1 = ({}, {})", p1.first, p1.second);
println("p2 = ({}, {})", p2.first, p2.second);

The output is as follows:

p1 = (16, Hello World)
p2 = (true, 0.123)

Starting with C++23, std::format() and the print() functions have full support for pairs. For example, the two println() statements in the previous code snippet can be written as follows:

println("p1 = {}", p1);
println("p2 = {}", p2);

The output is as follows, with strings surrounded by double quotes:

p1 = (16, "Hello World")
p2 = (true, 0.123)

An std::tuple, defined in <tuple>, is a generalization of a pair. It allows you to store any number of values, each with its own specific type. Just like a pair, a tuple has a fixed size and fixed value types, which are determined at compile time.

A tuple can be created with a tuple constructor, specifying both the template types and the actual values. For example, the following code creates a tuple where the first element is an integer, the second element is a string, and the last element is a Boolean:

using MyTuple = tuple<int, string, bool>;
MyTuple t1 { 16, "Test", true };

Just as for pair, starting with C++23, std::format() and the print() functions fully support tuples:

println("t1 = {}", t1);
// Outputs: t1 = (16, "Test", true)

std::get<i>() is used to get the ith element from a tuple, where i is a zero-based index; that is, <0> is the first element of the tuple, <1> is the second element of the tuple, and so on. The value returned has the correct type for that index in the tuple:

println("t1 = ({}, {}, {})", get<0>(t1), get<1>(t1), get<2>(t1));
// Outputs: t1 = (16, Test, true)

You can check that get<i>() returns the correct type by using typeid(), from <typeinfo>. The output of the following code confirms that the value returned by get<1>(t1) is indeed an std::string (as mentioned before, the exact string returned by typeid().name() is compiler dependent):

println("Type of get<1>(t1) = {}", typeid(get<1>(t1)).name());
// Outputs: Type of get<1>(t1) = class std::basic_string<char,
// struct std::char_traits<char>,class std::allocator<char> >

You can use the std::tuple_element class template to get the type of an element based on the element’s index at compile time. tuple_element requires you to specify the type of the tuple (MyTuple in this case) and not an actual tuple instance like t1. Here is an example:

println("Type of element with index 2 = {}",
typeid(tuple_element<2, MyTuple>::type).name());
// Outputs: Type of element with index 2 = bool

You can also retrieve an element from a tuple based on its type with std::get<T>(), where T is the type of the element you want to retrieve instead of the index. The compiler generates an error if the tuple has several elements with the requested type. For example, you can retrieve the string element from t1 as follows:

println("String = {}", get<string>(t1));
// Outputs: String = Test

Iterating over the values of a tuple is unfortunately not straightforward. You cannot write a simple loop and call something like get<i>(mytuple) because the value of i must be known at compile time. A possible solution is to use template metaprogramming, which is discussed in detail in Chapter 26, “Advanced Templates,” together with an example on how to print tuple values.

The size of a tuple can be queried with the std::tuple_size class template. As with tuple_element, tuple_size requires you to specify the type of the tuple, not an actual tuple:

println("Tuple Size = {}", tuple_size<MyTuple>::value);
// Outputs: Tuple Size = 3

If you don’t know a tuple’s exact type, you can always use decltype() to query for its type as follows:

println("Tuple Size = {}", tuple_size<decltype(t1)>::value);
// Outputs: Tuple Size = 3

With class template argument deduction (CTAD) you can omit the template type parameters when constructing a tuple and let the compiler deduce them automatically based on the types of the arguments passed to the constructor. For example, the following defines the same t1 tuple consisting of an integer, a string, and a Boolean. Note that you now have to specify "Test"s using the s string literal to make sure it’s an std::string:

tuple t1 { 16, "Test"s, true };

With CTAD, you do not explicitly specify the types stored in a tuple and so you cannot use & to specify references. If you want to use CTAD to generate a tuple containing a reference-to-non-const or a reference-to-const, then you need to use ref() or cref(), respectively, both defined in <functional>. These create instances of reference:wrapper<T> or reference:wrapper<const T>. For example, the following statements result in a tuple of type tuple<int, double&, const double&, string&>:

double d { 3.14 };
string str1 { "Test" };
tuple t2 { 16, ref(d), cref(d), ref(str1) };

To test the double reference stored in t2, the following code first writes the value of the double variable to the console. The call to get<1>(t2) returns a reference to d because ref(d) is used for the second (index 1) tuple element. The second statement changes the value of the variable referenced, and the last statement shows that the value of d is indeed changed through the reference stored in the tuple. Note that the third line fails to compile because cref(d) is used for the third tuple element; that is, it is a reference-to-const to d:

println("d = {}", d);
get<1>(t2) *= 2;
//get<2>(t2) *= 2; // ERROR because of cref().
println("d = {}", d);
// Outputs: d = 3.14
// d = 6.28

Without class template argument deduction, you can use the std::make_tuple() function template to create a tuple. Since it is a function template, it supports function template argument deduction and hence also allows you to create a tuple by only specifying the actual values. The types are deduced automatically at compile time. Here’s an example:

auto t2 { make_tuple(16, ref(d), cref(d), ref(str1)) };

There are two ways in which you can decompose a tuple into its individual elements: structured bindings and std::tie().

Structured bindings, available since C++17, make it easy to decompose a tuple into separate variables. For example, the following code defines a tuple consisting of an integer, a string, and a Boolean value, and then uses a structured binding to decompose it into three distinct variables:

tuple t1 { 16, "Test"s, true };
auto [i, str, b] { t1 };
println("Decomposed: i = {}, str = \"{}\", b = {}", i, str, b);

You can also decompose a tuple into references, allowing you to modify the contents of the tuple through those references. Here’s an example:

auto& [i2, str2, b2] { t1 };
i2 *= 2;
str2 = "Hello World";
b2 = !b2;

With structured bindings, you cannot ignore specific elements while decomposing a tuple. If your tuple has three elements, then your structured binding needs three variables.

If you want to decompose a tuple without structured bindings, you can use the std::tie() utility function, which generates a tuple of references. The following example first creates a tuple consisting of an integer, a string, and a Boolean value. It then creates three variables—an integer, a string, and a Boolean—and writes the values of those variables to the console. The tie(i, str, b) call creates a tuple containing a reference to i, a reference to str, and a reference to b. The assignment operator is used to assign tuple t1 to the result of tie(). Because the result of tie() is a tuple of references, the assignment actually changes the values in the three separate variables, as is shown by the output of the values after the assignment:

tuple t1 { 16, "Test"s, true };
int i { 0 };
string str;
bool b { false };
println("Before: i = {}, str = \"{}\", b = {}", i, str, b);
tie(i, str, b) = t1;
println("After: i = {}, str = \"{}\", b = {}", i, str, b);

The result is as follows:

Before: i = 0, str = "", b = false
After: i = 16, str = "Test", b = true

With tie() you can ignore certain elements that you do not want to be decomposed. Instead of a variable name for the decomposed element, you use the special std::ignore value. For example, the string element of the t1 tuple can be ignored by replacing the tie() statement from the previous example with the following:

tie(i, ignore, b) = t1;

You can use std::tuple_cat() to concatenate two tuples into one. In the following example, the type of t3 is tuple<int, string, bool, double, string>:

tuple t1 { 16, "Test"s, true };
tuple t2 { 3.14, "string 2"s };
auto t3 { tuple_cat(t1, t2) };
println("t3 = {}", t3);

The output is as follows:

t3 = (16, "Test", true, 3.14, "string 2")

Tuples support all comparison operators. For the comparison operators to work, the element types stored in the tuple should support them as well. Here is an example:

tuple t1 { 123, "def"s };
tuple t2 { 123, "abc"s };
if (t1 < t2) { println("t1 < t2"); }
else { println("t1 >= t2"); }

The output is as follows:

t1>= t2

Tuple comparisons can be used to easily implement lexicographical comparison operators for custom types that have several data members. For example, suppose you have the following class with three data members:

class Foo
{
public:
explicit Foo(int i, string s, bool b)
: m_int { i }, m_str { move(s) }, m_bool { b } { }
private:
int m_int;
string m_str;
bool m_bool;
};

Correctly implementing a full set of comparison operators that compare all data members of Foo is trivial by explicitly defaulting operator<=> as follows:

auto operator<=>(const Foo& rhs) const = default;

This automatically compares all data members. However, if the semantics of a class are such that a comparison between two instances should take only a subset of the data members into account, then correctly implementing a full set of comparison operators for such a class is not trivial! But, with std::tie() and the three-way comparison operator (operator<=>), it does become easy, a simple one-liner. The following is an implementation of operator<=> for Foo comparing only the m_int and m_str data members and ignoring m_bool:

auto operator<=>(const Foo& rhs) const
{
return tie(m_int, m_str) <=> tie(rhs.m_int, rhs.m_str);
}

Here is an example of its use:

Foo f1 { 42, "Hello", false };
Foo f2 { 42, "World", false };
println("{}", (f1 < f2)); // Outputs true
println("{}", (f2 > f1)); // Outputs true

std::make_from_tuple<T>() constructs an object of a given type T, passing the elements of a given tuple as arguments to the constructor of T. For example, suppose you have the following class:

class Foo
{
public:
explicit Foo(string str, int i) : m_str { move(str) }, m_int { i } { }
private:
string m_str;
int m_int;
};

You can use make_from_tuple() as follows:

tuple myTuple { "Hello world.", 42 };
auto foo { make_from_tuple<Foo>(myTuple) };

Technically, the argument to make_from_tuple() does not have to be a tuple, but it has to be something that supports std::get<>() and tuple_size. Both std::array and pair satisfy these requirements as well.

This function is not that practical for everyday use, but it comes in handy when writing generic code using templates and template metaprogramming.

std::apply() calls a given callable, passing the elements of a given tuple as arguments. Here is an example:

int add(int a, int b) { return a + b; }
println("{}", apply(add, tuple { 39, 3 }));

As with make_from_tuple(), this function is also more useful when writing generic code using templates and template metaprogramming than for everyday use.

Chapter 1 introduces the basics of std::optional. C++23 adds three new member functions to optional, collectively called monadic operations. These allow you to chain operations on an optional without having to check whether the optional has a value before applying each operation.

The following monadic operations are available:

  • transform(F): Returns an optional containing the result of invoking F with the value of *this as argument if *this has a value; otherwise, returns an empty optional
  • and_then(F): Returns the result (which must be an optional) of invoking F with the value of *this as argument if *this has a value; otherwise, returns an empty optional
  • or_else(F): Returns *this if *this has a value; otherwise, returns the result (which must be an optional) of invoking F

Let’s look at an example. The following function parses a given string for an integer and returns the result as an optional. If the string cannot be parsed as an integer, an empty optional is returned.

optional<int> Parse(const string& str)
{
try { return stoi(str); }
catch () { return {}; }
}

The following loop repeatedly asks the user to give some input. Parse() is called to parse the user’s input. If the input is successfully parsed as an integer, the integer is doubled with and_then() and converted back to a string with transform(). If the input cannot be parsed, or_else() is used to return the string “No Integer.” Thanks to monadic operations, there is no need to check whether the optionals returned from Parse() and and_then() contain a value before applying the next operation on them. The error handling is taken care of for you. The different operations can simply be chained together.

while (true) {
print("Enter an integer (q to stop): ");
string str;
if (!getline(cin, str) || str == "q") { break; }
auto result { Parse(str)
.and_then([](int value) -> optional<int> { return value * 2; })
.transform([](int value) { return to_string(value); })
.or_else([] { return optional<string> { "No Integer" }; }) };
println(" > Result: {}", *result);
}

Here is some sample output:

Enter an integer (q to stop): 21
> Result: 42
Enter an integer (q to stop): Test
> Result: No Integer

As Chapter 14, “Handling Errors,” explains, a function in C++ can return only a single type. If a function can fail, it should inform the caller about the failure. In the past, you had a couple of options to do so. You could throw an exception with details of the error. Or you could try to come up with a special value of the return type to signal an error.

For example, if a function returns a pointer, the function could return nullptr in case of an error. If a function returns only positive integers for its normal operation, you could return negative values to signal different errors, and so on. But coming up with such a special value is not always possible. If the return type of a function is int and the valid range of returned values is the entire range of integers, then you don’t have any integers left to use as special error values. In such cases, you could use the std::optional vocabulary type. It’s a type that can either contain a value of a certain type or be empty. A function could then return an empty optional to signal an error.

That’s all fine, but when a caller of the function receives an empty optional, it has no way of knowing what exactly went wrong; i.e., the function cannot return the real reason of the error. These problems are solved with std::expected, defined in <expected>, and introduced with C++23. It’s a class template accepting two template type parameters:

  • T: The type of the expected value
  • E: The type of an error value, also known as an unexpected value

An expected is never empty; it always contains either a value of type T or a value of type E. That’s the biggest difference compared to optional, which can be empty, leaving you with no clue as to why it’s empty. Thus, a function returning an expected should either return a value of the expected type or return a value of the error type to signal the exact reason of the failure. The error type can be whatever you want. It can be a simple integer or a complex class. Often, it’s best to encode errors in a class capable of representing as many details about an error as possible, for example, the filename, line number, and column number where parsing of some data file failed.

An instance of expected<T,E> can be created implicitly from a value of type T, just as an optional<T>. To create an instance of expected<T,E> containing a value of the error type E, you must use std::unexpected<E>. A default constructed expected<T,E> contains a default constructed value of the expected type, T. This is different compared to optional. A default constructed optional is empty! In other words, a default constructed expected represents success, while a default constructed optional represents an error.

Let’s look at an example. The following is a function receiving a string and trying to parse the string as an integer. The stoi() function throws invalid_argument if the string doesn’t represent an integer and throws out_of_range if the parsed integer is larger than what can be represented as an int. Suppose you don’t want parseInteger() to throw such exceptions but instead return an expected. The function catches the two exceptions and transforms them to a string, the error type of the returned expected.

expected<int, string> parseInteger(const string& str)
{
try { return stoi(str); }
catch (const invalid_argument& e) { return unexpected { e.what() }; }
catch (const out_of_range& e) { return unexpected { e.what() }; }
}

expected has the following member functions. All of them, except error(), are analogous to the similarly named member functions for optional.

  • has_value() and operator bool: Returns true if the expected has a value of type T, false otherwise.
  • value(): Returns the value of type T. Throws std::bad_expected_access if called on an expected containing a value of type E.
  • operator* and ->: Accesses the value of type T. The behavior is undefined if the expected doesn’t contain a value of type T.
  • error(): Returns the error of type E. The behavior is undefined if the expected doesn’t contain a value of type E.
  • value_or(): Returns the value of type T, or another given value if the expected doesn’t contain such a value.

The following example demonstrates most of these member functions:

auto result1 { parseInteger("123456789") };
if (result1.has_value()) { println("result1 = {}", result1.value()); }
if (result1) { println("result1 = {}", *result1); }
println("result1 = {}", result1.value_or(0));
auto result2 { parseInteger("123456789123456") };
if (!result2) { println("result2 contains an error: {}", result2.error()); }
auto result3 { parseInteger("abc") };
if (!result3) { println("result3 contains an error: {}", result3.error()); }

Here is the output:

result1 = 123456789
result1 = 123456789
result1 = 123456789
result2 contains an error: stoi argument out of range
result3 contains an error: invalid stoi argument

Additionally, expected supports monadic operations: and_then(), transform(), or_else(), and transform_error(). The first three are analogous to the monadic operations supported by optional.

  • transform(F): Returns an expected containing the result of invoking F with the expected value as argument if *this has an expected value; otherwise, just returns the expected as is
  • and_then(F): Returns the result (which must be an expected) of invoking F with the expected value as argument if *this has an expected value; otherwise, just returns the expected as is
  • or_else(F): Returns *this if *this has an expected value; otherwise, returns the result (which must be an expected) of invoking F with the unexpected value as argument
  • transform_error(F): Returns *this if *this has an expected value; otherwise, returns an expected containing the unexpected value transformed by invoking F with the unexpected value as argument

Here is an example of using and_then() on an expected. Just as for monadic operations on optionals, there is no need to explicitly check whether the result of calling parseInteger() contains an expected value before applying the operation. The error handling is taken care of for you.

auto transformedResult { parseInteger("123456789")
.and_then([](int value) -> expected<int, string> { return value * 2; }) };

The error type of expected can be any type you want. Returning multiple error types is also possible by using the variant vocabulary type discussed earlier in this chapter. For example, instead of returning a simple string, the parseInteger() function can return two different error types for the two error cases. The following version returns errors of two custom types OutOfRange and InvalidArgument:

expected<int, variant<OutOfRange, InvalidArgument>>
parseInteger(const string& str) {}

To conclude, it’s clear that optional and expected are somewhat related. Use the following rule to decide which one to use in certain use cases.

Exceptions, Error Return Codes, and expected

Section titled “Exceptions, Error Return Codes, and expected”

There are three major options to handle errors in a function. The function can throw an exception, discussed in detail in Chapter 14; return an error code; or return an expected. They all have their own merits. The following table, based on the official proposal paper for std::expected, P0323R12, summarizes them:

EXCEPTIONERROR RETURN CODEEXPECTED
VISIBILITYNot visible, unless you read the function documentation or analyze the code.Immediately visible from the function prototype. But easy to ignore the return value.Immediately visible from the function prototype. Cannot be ignored as it contains the result of the function.
DETAILSContains as many details about the error as possible.Often just a simple integer.Contains as many details about the error as possible.
CODE NOISEAllows for writing clean code with separate error handling.Error handling is intertwined with the normal flow, making code harder to read and maintain.Allows for clean code. Thanks to monadic operations, error handling is not intertwined with normal flow.

This chapter gave an overview of additional vocabulary types provided by the C++ Standard Library. You learned how to use the variant and any vocabulary data types. You also learned about tuples, which are a generalization of pairs, and the operations you can apply to tuples. You discovered the power of monadic operations for optionals allowing you to easily chain operations on optionals. The chapter finished with expected, a vocabulary type capable of holding either a value of a certain type or an error.

This chapter concludes Part 3 of the book. The next part discusses some more advanced topics and starts with a chapter showing you how to customize and extend the functionality provided by the C++ Standard Library by implementing your own Standard Library–compliant algorithms and data structures.

By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/proc++6e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website.

  1. Exercise 24-1: Chapter 14, “Handling Errors,” explains error handling in C++ and explains that there are basically two major options: either you work with error codes or you work with exceptions. I recommend using exceptions for error handling, but for this exercise, you’ll use error codes. Write a simple Error class that just stores a single message, has a constructor to set the message, and has a getter to retrieve the message. Next, write a getData() function with a single Boolean parameter called fail. If fail is false, the function returns a vector of some data; otherwise, it returns an instance of Error. You are not allowed to use reference-to-non-const output parameters. Try to come up with a solution that doesn’t use the C++23 std::expected class template yet. Test your implementation in your main() function.
  2. Exercise 24-2: Modify your solution to Exercise 24-1 to use the C++23 std::expected class template and discover how it makes the solution much easier to read and understand.
  3. Exercise 24-3: Most command-line applications accept command-line parameters. In most, if not all, of the sample code in this book the main function is simply main(). However, main() can also accept parameters: main(int argc, char** argv) where argc is the number of command-line parameters, and argv is an array of strings, one string for each parameter. Assume for this exercise that a command-line parameter is of the form name=value. Write a function that can parse a single parameter and that returns a pair containing the name of the parameter and a variant containing the value as a Boolean if the value can be parsed as a Boolean (true or false), an integer if the value can be parsed as an integer, or a string otherwise. To split the name=value string, you can use a regular expression (see Chapter 21, “String Localization and Regular Expressions”). To parse integers, you can use one of the functions explained in Chapter 2, “Working with Strings and String Views.” In your main() function, loop over all command-line parameters, parse them, and output the parsed results to the standard output using holds_alternative().
  4. Exercise 24-4: Modify your solution to Exercise 24-3. Instead of using holds_alternative(), use a visitor to output the parsed results to the standard output.
  5. Exercise 24-5: Modify your solution to Exercise 24-4 to use tuples instead of pairs.