Chapter 6 General Purpose Utilities
6.1 Expressing time intervals with chrono::duration
C++11 provides a
chrono
library to deal with data and time. It mainly consists of three components: durations which represents a time interval, time pointes which represents a period of time since the epoch of a clock and clock which defines an epoch (start of time) and a tick rate.Duration is essentially a class template, whose template parameters are the underlying type of the tick and the kind of the tick represented by
std::ratio
(ratio to the unit of second). For example, the standard library has defined some types for us:c++1
2
3
4
5
6
7
8
9
10namespace std {
namespace chrono {
typedef duration<long long, ratio<1, 1000000000>> nanoseconds;
typedef duration<long long, ratio<1, 1000000>> microseconds;
typedef duration<long long, ratio<1, 1000>> milliseconds;
typedef duration<long long> seconds;
typedef duration<int, ratio<60>> minutes;
typedef duration<int, ratio<3600>> hours;
}
}Remember C++14 brings us some user-defined literals? Yep,
std::chrono_literals
are included. With the help of them, we can define duration like below:c++1
2
3
4
5
6
7using namespace std::chrono_literals;
auto half_day = 12h;
auto half_hour = 30min;
auto half_minute = 30s;
auto half_second = 500ms;
auto half_millisecond = 500us;
auto half_microsecond = 500ns;Duration with lower precision can be converted implicitly to one with higher precision while duration with higher precision should use
std::chrono::duration_cast
to convert to one with lower precision.Also, we can use
count()
method to retrieve the number of ticks.
6.2 Measuring function execution time with a standard clock
Use the concept of clock and time points to measure function execution time:
c++1
2
3auto start = std::chrono::high_resolution_clock::now();
func();
auto diff = std::chrono::high_resolution_clock::now() - start;std::chrono::time_point
is essentially a class template, whose template parameters are clock and duration (you can consider that time_point = clock + duration)And a clock defines two things: the epoch and tick rate.
There are three kinds of clocks:
system_clock
,high_resolution_clock
andsteady_clock
. They are different in terms of precision and steady attribute.If a clock is steady, it means it’s never adjusted, i.e. the difference between two time pointes is always positive as time passes. When measuring the function execution time, we should always use steady clocks.
6.3 Generating hash values for custom types
We know that
std::unordered_*
containers use hash table to store values, which requires the underlying type has corresponding hash function.To be more precise, only types which have specialization of
std::hash
class can be used as template argument ofstd::unordered_*
. The standard has specialized it for all basic types and some common used types likestd::string
. When we want to use custom types, it needs to specialize for it manually:c++1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace std {
template<>
struct hash<Item> {
typedef Item argument_type;
typedef size_t result_type;
result_type operator()(argument_type const & item) const {
result_type hashValue = 17;
hashValue = 31 * hashValue + std::hash<int>{}(item.id);
hashValue = 31 * hashValue + std::hash<std::string>{}(item.name);
hashValue = 31 * hashValue + std::hash<double>{}(item.value);
return hashValue;
}
};
}Note that
std::hash
is essentially a functor template, whoseoperator()
returns the same result for the same arguments and has a very small chance to return the same value for non-equal arguments.Here the non-equal doesn’t necessarily means comparing all fields because some fields don’t play a role in the
operator==
. So when calculating hash values, we just need to consider those fields that will determine whether two objects are equal.One thing worth noting is that, we choose
31
as the multiplier. That is because31*x
can be replaced with(x<<5)-x
, which is advantageous for performance optimization. Other choices can be127
and8191
.
6.4 Using std::any to store any value
C++17 introduces
std::any
to store a value of any type, just likeObject
in JavaScript.To store a value into
std::any
, use its constructor or assign operator:c++1
2
3std::any value(42); // integer 12
value = 42.0; // double 12.0
value = "42"s; // std::string "12"To read a value from
std::any
, usestd::any_cast
. Note that it can throwstd::bad_any_cast
:c++1
2
3
4
5
6
7
8std::any value = 42.0;
try {
auto d = std::any_cast<double>(value);
std::cout << d << std::endl;
}
catch (std::bad_any_cast const & e) {
std::cout << e.what() << std::endl;
}Use
type()
method to check type of the stored value (if no, returnvoid
) and usehas_value()
method to check whether there is a stored value:c++1
2
3inline bool is_integer(std::any const & a) {
return a.type() == typeid(int);
}
6.5 Using std::optional to store optional values
C++17 introduces
std::optional
to store an optional value that may exist or not.To store a value into
std::optional
, use its constructor and assign operator:c++1
2
3
4std::optional<int> v1; // v1 is empty
std::optional<int> v2(42); // v2 contains 42
v1 = 42; // v1 contains 42
std::optional<int> v3 = v2; // v3 contains 42To read a value from
std::optional
, useoperator*
(operator->
) orvalue()
(value_or()
) method. The difference is that when the optional is empty, behavior of the former is undefined while the latter will throw astd::bad_option_access
.Use
has_value()
method to check if the optional is empty.The common use case of
std::optional
is to be the return value indicating failure when empty.
6.6 Using std::variant as a type-safe union
C++17 introduces
std::variant
to store a value whose type is in a type set (a little bit like union)To store a value into
std::variant
, use its constructor, assign operator oremplace()
method:c++1
2
3
4
5
6
7struct foo {
int value;
explicit foo(int const i) : value(i) {}
};
std::variant<int, std::string, foo> v = 42; // holds int
v.emplace<foo>(42); // holds fooTo read a value from
std::variant
, usestd::get()
with template argument of index or type (if unique). If type of the stored value is not the specified one, astd::bad_variant_access
will be thrown:c++1
2
3
4
5
6
7
8
9
10std::variant<int, double, std::string> v = 42;
auto i1 = std::get<int>(v);
auto i2 = std::get<0>(v);
try {
auto f = std::get<double>(v);
}
catch (std::bad_variant_access const & e) {
std::cout << e.what() << std::endl; // Unexpected index
}Use
index()
method to retrieve index of the stored value and usestd::holds_alternatives()
to check whether thestd::variant
holds the specified type.Note that
std::variant
is default initialized with its first alternative, so it needs to usestd::monostate
, which is an empty type intended to make variants default constructible, if otherwise the first type isn’t default constructible.
6.7 Visiting a std::variant
Use
std::visit()
to visit astd::variant
, which is to do some operation potentially according to the alternative type.The first parameter of
std::visit()
is a callable which takes a single parameter (so-called visitor). The callable is required to be able to accept any alternative type of the variants, so we can useauto&&
or functor that overloadsoperator()
for all alternative types.And the following parameters are variants to be visited:
c++1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// lambda
for (auto const & d : dvds) {
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, Movie>) {}
else if constexpr (std::is_same_v<T, Music>) {}
else if constexpr (std::is_same_v<T, Software>) {}
}, d);
}
// functor
struct visitor_functor {
void operator()(Movie const & arg) const {}
void operator()(Music const & arg) const {}
void operator()(Software const & arg) const {}
};
for (auto const & d : dvds) {
std::visit(visitor_functor(), d);
}When visiting a variant, the visitor will be invoked with the currently stored value.
A visitor isn’t necessarily a non-return callable. Instead it could return the type of the second parameter of
std::visit
(which is the variant). And it seems that when the visitor has return value,std::visit
cannot take more than two parameters.
6.8 Registering a function to be called when a program exits normally
Use
std::atexit()
andstd::at_quick_exit()
to register a function to be called when the program exits normally, which means returning frommain
function,std::exit()
orstd::quick_exit()
.The function registered takes no parameter and has no return value.
One thing worth noting is that the order of invocation of functions registered and destruction of static objects are reverse of that they are registered and constructed:
c++1
2
3
4
5
6
7
8
9
10
11std::atexit(exit_handler_1);
static_foo::instance();
std::atexit(exit_handler_2);
std::atexit([]() {std::cout << "exit handler 3" << std::endl; });
std::exit(42);
// output:
// exit handler 3
// exit handler 2
// static foo destroyed!
// exit handler 1
6.9 Using type traits to query properties of types
There are two categories of type traits:
- query information about types like
std::is_void
,std::is_same
, they have a static member calledvalue
; - transform properties of types like
std::add_const
,std::remove_pointer
, they have atypedef
calledtype
.
- query information about types like
Typically type traits for querying type information are implemented with full or partial specialization mechanism. To be more precise, return false in primary template and return true in specialization:
c++1
2
3
4
5
6
7template <typename T>
struct is_pointer
{ static const bool value = false; };
template <typename T>
struct is_pointer<T*>
{ static const bool value = true; };Type traits for querying can be used in many occasions such as
std::enable_if
,static_assert
,std::conditional
and constexpr if.
6.10 Writing your own type traits
- When writing your own type traits for querying, define the
value
field in type traits asstatic constexpr
6.11 Using std::conditional to choose between types
Use
std::conditional
to choose between types at compile time. It takes three template parameters, the first one is a const bool expression at compile time and the following two are types to be chosen.It can be implemented like this:
c++1
2
3
4
5
6
7template<bool _Cond, typename _Iftrue, typename _Iffalse>
struct conditional
{ typedef _Iftrue type; };
template<typename _Iftrue, typename _Iffalse>
struct conditional<false, _Iftrue, _Iffalse>
{ typedef _Iffalse type; };