avatar

Catalog
Modern C++ Programming Cookbook Notes 6: General Purpose Utilities

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
    10
    namespace 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
    7
    using 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
    3
    auto 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 and steady_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 of std::unordered_*. The standard has specialized it for all basic types and some common used types like std::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
    15
    namespace 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, whose operator() 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 because 31*x can be replaced with (x<<5)-x, which is advantageous for performance optimization. Other choices can be 127 and 8191.

6.4 Using std::any to store any value

  • C++17 introduces std::any to store a value of any type, just like Object in JavaScript.

  • To store a value into std::any, use its constructor or assign operator:

    c++
    1
    2
    3
    std::any value(42); // integer 12
    value = 42.0; // double 12.0
    value = "42"s; // std::string "12"
  • To read a value from std::any, use std::any_cast. Note that it can throw std::bad_any_cast:

    c++
    1
    2
    3
    4
    5
    6
    7
    8
    std::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, return void) and use has_value() method to check whether there is a stored value:

    c++
    1
    2
    3
    inline 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
    4
    std::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 42
  • To read a value from std::optional, use operator* (operator->) or value() (value_or()) method. The difference is that when the optional is empty, behavior of the former is undefined while the latter will throw a std::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 or emplace() method:

    c++
    1
    2
    3
    4
    5
    6
    7
    struct 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 foo
  • To read a value from std::variant, use std::get() with template argument of index or type (if unique). If type of the stored value is not the specified one, a std::bad_variant_access will be thrown:

    c++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    std::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 use std::holds_alternatives() to check whether the std::variant holds the specified type.

  • Note that std::variant is default initialized with its first alternative, so it needs to use std::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 a std::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 use auto&& or functor that overloads operator() 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() and std::at_quick_exit() to register a function to be called when the program exits normally, which means returning from main function, std::exit() or std::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
    11
    std::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 called value;
    • transform properties of types like std::add_const, std::remove_pointer, they have a typedef called type.
  • 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
    7
    template <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 as static 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
    7
    template<bool _Cond, typename _Iftrue, typename _Iffalse>
    struct conditional
    { typedef _Iftrue type; };

    template<typename _Iftrue, typename _Iffalse>
    struct conditional<false, _Iftrue, _Iffalse>
    { typedef _Iffalse type; };
Author: Gusabary
Link: http://gusabary.cn/2020/11/20/Modern-C++-Programming-Cookbook-Notes/Modern-C++-Programming-Cookbook-Notes-6/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment