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.
VARIANT
Section titled “VARIANT”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: 1Contains an int: falseUse 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::stringException: bad variant accessTo 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::stringRetrieved int: n/aAn 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::stringIf 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::stringA 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 = falseanInt.has_value = true
anInt wrapped type = intaString wrapped type = class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>
3Exception: Bad any_castYou 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 = boolYou 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 = TestIterating 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 = 3If 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 = 3With 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.28Without 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)) };Decompose Tuples
Section titled “Decompose Tuples”There are two ways in which you can decompose a tuple into its individual elements: structured bindings and std::tie().
Structured Bindings
Section titled “Structured Bindings”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 = falseAfter: i = 16, str = "Test", b = trueWith 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;Concatenation
Section titled “Concatenation”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")Comparisons
Section titled “Comparisons”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>= t2Tuple 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 trueprintln("{}", (f2 > f1)); // Outputs truemake_from_tuple
Section titled “make_from_tuple”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.
OPTIONAL: MONADIC OPERATIONS
Section titled “ OPTIONAL: MONADIC OPERATIONS”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
optionalcontaining the result of invokingFwith the value of *this as argument if*thishas a value; otherwise, returns an emptyoptional and_then(F): Returns the result (which must be an optional) of invokingFwith the value of *this as argument if*thishas a value; otherwise, returns an emptyoptionalor_else(F): Returns*thisif*thishas a value; otherwise, returns the result (which must be an optional) of invokingF
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: 42Enter an integer (q to stop): Test > Result: No Integer EXPECTED
Section titled “ EXPECTED”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 valueE: 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()andoperator bool: Returnstrueif theexpectedhas a value of typeT,falseotherwise.value(): Returns the value of typeT. Throwsstd::bad_expected_accessif called on anexpectedcontaining a value of typeE.- operator* and ->: Accesses the value of type
T. The behavior is undefined if theexpecteddoesn’t contain a value of typeT. error(): Returns the error of typeE. The behavior is undefined if theexpecteddoesn’t contain a value of typeE.value_or(): Returns the value of typeT, or another given value if theexpecteddoesn’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 = 123456789result1 = 123456789result1 = 123456789result2 contains an error: stoi argument out of rangeresult3 contains an error: invalid stoi argumentAdditionally, 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 anexpectedcontaining the result of invokingFwith the expected value as argument if*thishas an expected value; otherwise, just returns theexpectedas isand_then(F): Returns the result (which must be an expected) of invokingFwith the expected value as argument if*thishas an expected value; otherwise, just returns theexpectedas isor_else(F): Returns*thisif*thishas an expected value; otherwise, returns the result (which must be an expected) of invokingFwith the unexpected value as argumenttransform_error(F): Returns*thisif*thishas an expected value; otherwise, returns anexpectedcontaining the unexpected value transformed by invokingFwith 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:
| EXCEPTION | ERROR RETURN CODE | EXPECTED | |
|---|---|---|---|
| VISIBILITY | Not 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. |
| DETAILS | Contains as many details about the error as possible. | Often just a simple integer. | Contains as many details about the error as possible. |
| CODE NOISE | Allows 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. |
SUMMARY
Section titled “SUMMARY”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.
EXERCISES
Section titled “EXERCISES”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.
- 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
Errorclass that just stores a single message, has a constructor to set the message, and has a getter to retrieve the message. Next, write agetData()function with a single Boolean parameter calledfail. Iffailisfalse, the function returns avectorof some data; otherwise, it returns an instance ofError. You are not allowed to use reference-to-non-constoutput parameters. Try to come up with a solution that doesn’t use the C++23std::expectedclass template yet. Test your implementation in yourmain()function. - Exercise 24-2: Modify your solution to Exercise 24-1 to use the C++23
std::expectedclass template and discover how it makes the solution much easier to read and understand. - 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)whereargcis the number of command-line parameters, andargvis an array of strings, one string for each parameter. Assume for this exercise that a command-line parameter is of the formname=value. Write a function that can parse a single parameter and that returns apaircontaining the name of the parameter and avariantcontaining the value as a Boolean if the value can be parsed as a Boolean (trueorfalse), an integer if the value can be parsed as an integer, or astringotherwise. To split thename=valuestring, 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 yourmain()function, loop over all command-line parameters, parse them, and output the parsed results to the standard output usingholds_alternative(). - 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. - Exercise 24-5: Modify your solution to Exercise 24-4 to use
tuples instead ofpairs.