- Learning Boost C++ Libraries
- Arindam Mukherjee
- 2169字
- 2021-07-16 20:49:02
Simple data structures
In this section, we will look at two different libraries that will help you create simple data structures of immediate usefulness: Boost.Optional and Boost.Tuple. Boost.Optional can be used to represent optional values; objects that may or may not be there. Boost.Tuple is used to create ordered sets of heterogeneous values.
Boost.Optional
Let us consider that you need to maintain about musicians in a data store. Among other things, you can look up the latest album released by an artiste. You have written a simple API in C++ for doing this:
std::string find_latest_album_of(const std::string& artisteName);
For simplicity we will ignore the possibility that two or more artistes could share the same name. Here is a simple implementation of this function:
1 #include <string> 2 #include <map> 3 4 typedef std::map<std::string, std::string> artiste_album_map; 5 6 extern artiste_album_map latest_albums; 7 8 std::string find_latest_album_of( 9 const std::string& artiste_name) { 10 auto iter = latest_albums.find(artiste_name); 11 12 if (iter != latest_albums.end()) { 13 return iter->second; 14 } else { 15 return ""; 16 } 17 }
We store the names of artistes and their latest albums in a map called latest_albums
. The find_latest_album_of
function takes the name of an artiste and uses the find
member function of std::map
to look up the latest album. If it does not find an entry, it returns an empty string. Now, it is possible that some artistes have not released an album yet. Returning an empty string seems legit for such cases until you realize that musicians have their unique whims and sometimes, they release an album without a name. So, how do you distinguish between the cases where the musician is yet to release an album, versus where the musician's latest album was untitled? In one case, there is no value to return while in the other case, it is an empty string.
The boost::optional<T>
template can be used to represent an optional value; one that may or may not be present. In this case, it is tailor-made for our problem. To represent a std::string
value that may or may not be present, you use boost::optional<std::string>
. We can rewrite the find_latest_album_of
function using boost::optional
, as shown in the following code listing:
Listing 2.1: Using Boost.Optional
1 #include <string> 2 #include <map> 3 #include <boost/optional.hpp> 4 5 typedef std::map<std::string, std::string> artiste_album_map; 6 7 extern artiste_album_map latest_albums; 8 9 boost::optional<std::string> find_latest_album_of( 10 const std::string& artiste_name) { 11 auto iter = latest_albums.find(artiste_name); 12 13 if (iter != latest_albums.end()) { 14 return iter->second; 15 } else { 16 return boost::none; 17 } 18 }
We simply return the value found (line 14), which is automatically wrapped in a boost::optional
container. If there is no value to return, we return a special object, boost::none
(line 16). This causes an empty boost::optional
object to be returned. The code using boost::optional
does exactly what we need; it checks whether a key is present in the container and returns the value or indicates that it is absent without any ambiguity (that is, empty versus untitled).
Tip
A default-initialized instance of boost::optional
is always empty. If the value stored in boost::optional
is movable (see Appendix, C++11 Language Features Emulation), the wrapper optional
object is also movable. If the stored value is copyable, the wrapper optional
object is also copyable.
We can generalize the lookup function in listing 2.1 to any container with a map-like or dictionary interface as follows:
Listing 2.2: Generic lookup using optional
1 #include <boost/optional.hpp> 2 3 template <typename C> 4 boost::optional<typename C::mapped_type> 5 lookup(const C& dict, const typename C::key_type& key) 6 { 7 typename C::const_iterator it = dict.find(key); 8 if (it != dict.end()) { 9 return it->second; 10 } else { 11 return boost::none; 12 } 13 }
In the preceding code, we have converted lookup
to a function template that can be called on any map
, multimap
, their unordered variants, or any other nonstandard container, exposing a similar interface. It is parameterized on the container type C
. The container type C
must have nested type definitions: key_type
and mapped_type
corresponding to the types of keys and values the map stores; a constraint satisfied by std:map
and other associative containers from the Standard Library.
The use of the typename
keyword (lines 4, 5, 7) may need some explanation. If we omit the typename
keyword from these lines, the compiler will fail to identify C::mapped_type
, C::key_type
, and C::const_iterator
as names of types. Because mapped_type
, key_type
, and const_iterator
are names that are dependent on the type template parameter C
, the compiler needs to be told that they identify types. We use the typename
keyword to do this.
You can check whether an optional
object contains a value or is empty, and extract the value stored in a non-empty optional
object:
1 std::string artiste("Korn"); 2 boost::optional<std::string> album = 3 find_latest_album_of(artiste); 4 if (album) { 5 std::cout << "The last album from " << artiste; 6 7 if (album->empty()) { 8 std::cout << " is untitled\n"; 9 } else { 10 std::cout << " is named " << *album << '\n'; 11 } 12 } else { 13 std::cout << "No information on albums from " 14 << artiste << '\n'; 15 }
In the code that calls find_latest_album_of
, to test whether the returned value is empty, we invoke the object in a Boolean context (line 4). If it evaluates to true
, it means that album
is not empty. If it has a value, we can obtain a reference to the contained value using the overloaded operator*
(line 10). We can access members of the underlying object using an overloaded operator->
; in this case we call the empty member function of std::string
(line 7). We could also use get member function of a nonempty boost::optional
object instead of the overloaded operator*
to access the value stored. Dereferencing an empty optional value by calling the operator*
, get
, or operator->
causes a runtime error, which is why we first check whether the optional
object is empty before trying to dereference it.
Using optional
, we indicate that there may or may not be a value present for albums. But we would sometimes need to use APIs that should have taken optional values but do not. In such cases, we may want to return empty values with some default value. Imagine residents of Paris being asked about their favorite city and for those who do not name one, Paris being used as the default favorite:
1 void printFavoriteCity(const std::string& name, 2 const std::string& city) 3 { 4 std::cout << name "'s favorite city is " << city << '\n'; 5 } 6 7 boost::optional<std::string> getFavoriteCity( 8 const std::string& resident_id); 9 ... 10 std::string resident = "Serge"; 11 boost::optional<std::string> fav_city = 12 getFavoriteCity(resident); 13 14 printFavoriteCity(fav_city.get_value_or("Paris"));
If the imaginary getFavoriteCity
function returns an empty value, we want Paris
to be passed to the printFavoriteCity
function. We do this using the get_value_or
member function (line 14).
If we did not use optional
, what would the functions find_last_album_of
or lookup
return in order to indicate that there was no value found? They would either need to return a pointer to a dynamically-allocated object or nullptr
if there was no value found. Besides using dynamic memory, it requires that the caller function manage the lifetime of the dynamically-allocated object that is returned. This condition can be mitigated using smart pointers (Chapter 3, Memory Management and Exception Safety), but it does not eliminate free store allocations that are costly. The boost::optional
class eliminates free store allocations and stores the encapsulated object in its layout. In addition, it stores a Boolean flag to keep track of whether it is initialized or not.
Boost.Tuple
Boost Tuples are a cool way to group disparate types of data together into ordered tuples and pass them around. Structures do the same thing but a couple of things set tuples apart:
- You can write generic code to manipulate tuples of all kinds, for example, to print all their members and comparing two tuples for similarity in structure and types.
- Each new structure or class defines a new type in your software. Types should represent interfaces and behaviors. Representing every ad hoc clumping of data with a type results in proliferation of types that have no meaning in the problem space or its abstraction.
A Boost Tuple is an incredibly useful library that helps you conveniently create schemas for moving related data around together, such as exchanging data between functions. Boost Tuples are a generalization of std::pair
, which is used to create 2-element tuples.
Let us look at an example. Given a series of stock prices at different points in time, we want to find out the best two points in time to buy and sell the stock to maximize the profit. We can assume that there is no option to short-sell, that is, you must buy before you sell. For simplicity, the input can be assumed to be a vector of doubles. In this vector, we are interested in the pair of indices that represent the best time to buy and sell the stock to maximize profit:
Listing 2.3: Using tuples
1 #include <boost/tuple/tuple.hpp> 2 #include <vector> 3 4 boost::tuple<size_t, size_t, double> 5 getBestTransactDays(std::vector<double> prices) 6 { 7 double min = std::numeric_limits<double>::max(); 8 double gain = 0.0, max_gain = 0.0; 9 size_t min_day, max_day; 10 size_t buy_day; 11 for (size_t i = 0, days = prices.size(); i < days; ++i) { 12 if (prices[i] < min) { 13 min = prices[i]; 14 min_day = i; 15 } else if ((gain = prices[i] - min) > max_gain) { 16 max_gain = gain; 17 buy_day = min_day; 18 max_day = i; 19 } 20 } 21 22 return boost::make_tuple(buy_day, max_day, max_gain); 23 }
The function getBestTransactDays
returns a tuple of two unsigned integers (size_t
) and a double (line 4) that represent the two indices at which buying and selling the stock would maximize profit, and the maximum profit possible. The return type of the function is boost::tuple<size_t, size_t, double>
. The header boost/tuple/tuple.hpp
provides the necessary functions and types for working with tuples (line 1).
The function getBestTransactDays
implements a simple linear algorithm that runs through the vector, keeping track of the lowest stock price seen so far. If the current element has a lesser value than the lowest stock price so far, then this is set as the new lowest, and its index is noted (lines 12-14). The function also keeps track of the maximum gain, that is, the maximum difference in prices noted so far. If we encounter an element whose difference from the lowest price is higher than the maximum gain, then we note this difference as the new maximum gain (line 15), and also note the days of transaction required to achieve this gain (lines 16-18).
We create the tuple using boost::make_tuple
(line 22), which is a convenience function for creating tuples from its elements without explicit template instantiations. You could have also created and returned a tuple like this in place of line 22:
22 boost::tuple<size_t, size_t, double> best_buy(buy_day, max_day, 23 max_gain); 24 return best_buy;
As you can see, boost::make_tuple
is more compact and, being a function template, resolves the types of its arguments automatically to create the tuple of correct types. This is a frequently seen pattern where you use a factory function template to instantiate a class template, thus automating type detection.
There are several ways in which we can access the elements in a tuple. Look at the following example of calling the getBestTransactDays
function:
1 std::vector<double> stockPrices; 2 ... 3 boost::tuple<size_t, size_t, double> best_buy = 4 getBestTransactDays(stockPrices); 5 6 size_t buyDay = boost::get<0>(best_buy); // Access 0th element 7 size_t sellDay = boost::get<1>(best_buy); // Access 1st element 8 double profit = boost::get<2>(best_buy); // Access 2nd element
We can also unpack the elements in the tuple into individual variables using boost::tie
:
1 size_t buyDay, sellDay; 2 double profit; 3 boost::tie(buyDay, sellDay, profit) = 4 getBestTransactDays(stockPrices);
The preceding line of code will assign the first element of the tuple to buyDay
, the second to sellDay
, and the third to profit
. If we are interested in only a subset of the elements in the tuple, we can ignore the others using boost::tuples::ignore
. Here is the same example, but we have ignored sellDay
this time using boost::tuples::ignore
:
1 size_t buyDay, sellDay; 2 boost::tie(buyDay, sellDay, boost::tuples::ignore) = 3 getBestTransactDays(stockPrices);
Tuples of the same length can be compared to relational operators, such as ==
, <
, >
, <=
, and >=
. In any such comparison, the corresponding elements at each position are compared. The types of elements at the corresponding positions need not be identical; they just need to be comparable using the relational operator in question:
1 boost::tuple<int, int, std::string> t1 = 2 boost::make_tuple(1, 2, "Hello"); 3 boost::tuple<double, double, const char*> t2 = 4 boost::make_tuple(1, 2, "Hi"); 5 assert(t1 < t2); // because Hello < Hi
Note that the actual types in tuples t1
and t2
are different, but both have the same length, and the elements at corresponding positions are comparable with each other. In general, comparison stops at the first pair of elements that determines the outcome of the comparison. In this example, all three elements are compared because the first two elements compare equal.
1 boost::tuple<int, int, std::string> t1 = 2 boost::make_tuple(1, 20, "Hello"); 3 boost::tuple<double, double, const char*> t2 = 4 boost::make_tuple(1, 2, "Hi"); 5 assert(t1 > t2); // because 20 > 2
The following code is used to define relational operators for structs with very little code:
1 struct my_type { 2 int a; 3 double b; 4 char c; 5 }; 6 7 bool operator<(const my_type& left, const my_type& right) { 8 return boost::make_tuple(left.a, left.b, left.c) < 9 boost::make_tuple(right.a, right.b, right.c); 10 }
We will now write a generic function to find the number of elements in a tuple:
1 template <typename T> 2 size_t tuple_length(const T&) { 3 return boost::tuples::length<T>::value; 4 }
This function simply uses the boost::tuples::length<T>
metafunction to compute the number of elements in the tuple. This computation takes place at compile time. A metafunction is just a class template that has an accessible static member or a nested type computed at compile time from its template arguments (see Chapter 7, Higher Order and Compile-time Programming, for a more rigorous definition). In this case, the boost::tuples::length<T>
metafunction has a public static member called value
, which is computed as the number of elements in the tuple T
. If you use tuples from the Standard Library, you should use std::tuple_size<T>
instead of boost::tuples::length<T>
. This is just a small illustration of generic programming using metafunctions and type computation.
- Extending Jenkins
- Django+Vue.js商城項目實戰
- 垃圾回收的算法與實現
- Dependency Injection in .NET Core 2.0
- C語言程序設計
- Hadoop+Spark大數據分析實戰
- Learning Neo4j 3.x(Second Edition)
- Java應用開發技術實例教程
- Hands-On Microservices with Kotlin
- QGIS:Becoming a GIS Power User
- Android系統原理及開發要點詳解
- PHP從入門到精通(第4版)(軟件開發視頻大講堂)
- Visual Studio 2015高級編程(第6版)
- 從零開始:UI圖標設計與制作(第3版)
- Android Studio Cookbook