简体   繁体   中英

How to generalize std::chrono::duration(s)?

I wrote three versions of algorithm for my university class.

One is brute-force, other is greedy and the last is heuristic.

I want to be able to measure how much time each of the algorithms takes to complete.

I'm using <chrono> library to achieve this

Right now my code looks like this:

#include <iostream>
#include <chrono>
#include <sstream>

using namespace std;

string getTimeElapsed(long time1, const string &unit1, long time2 = 0, const string &unit2 = "") {
    stringstream s;
    s << time1 << " [" << unit1 << "]";
    if (time2) s << " " << time2 << " [" << unit2 << "]";
    return s.str();
}

int main() {
    auto begin = chrono::system_clock::now();
    // algorithm goes here
    auto solution = /* can be anything */
    auto end = chrono::system_clock::now();
    auto diff = end - begin;

    string timeElapsed;
    auto hours = chrono::duration_cast<chrono::hours>(diff).count();
    auto minutes = chrono::duration_cast<chrono::minutes>(diff).count();
    if (hours) {
        minutes %= 60;
        timeElapsed = getTimeElapsed(hours, "h", minutes, "min");
    } else {
        auto seconds = chrono::duration_cast<chrono::seconds>(diff).count();
        if (minutes) {
            seconds %= 60;
            timeElapsed = getTimeElapsed(minutes, "min", seconds, "s");
        } else {
            auto milliseconds = chrono::duration_cast<chrono::milliseconds>(diff).count();
            if (seconds) {
                milliseconds %= 1000;
                timeElapsed = getTimeElapsed(seconds, "s", milliseconds, "ms");
            } else {
                auto microseconds = chrono::duration_cast<chrono::microseconds>(diff).count();
                if (milliseconds) {
                    microseconds %= 1000;
                    timeElapsed = getTimeElapsed(milliseconds, "ms", microseconds, "μs");
                } else {
                    auto nanoseconds = chrono::duration_cast<chrono::nanoseconds>(diff).count();
                    if (microseconds) {
                        nanoseconds %= 1000;
                        timeElapsed = timeElapsed = getTimeElapsed(microseconds, "μs", nanoseconds, "ns");
                    } else timeElapsed = getTimeElapsed(nanoseconds, "ns");
                }
            }
        }
    }

    cout << "Solution [" << solution << "] found in " << timeElapsed << endl;

    return 0;
}

As you can see, the stacked if-else clauses look really ugly and you can see a pattern here:

if (timeUnit) { 
    timeElapsed = /* process current time units */
} else {
    /* step down a level and do the same for smaller time units */
}

I would like to make that procedure a recursive function.

However, I have no clue what should be the parameters of such function, because the chrono::duration is a template struct (?)

This function would look somewhat like this:

string prettyTimeElapsed(diff, timeUnit) {
    // recursion bound condition
    if (timeUnit is chrono::nanoseconds) return getTimeElapsed(timeUnit, "ns");

    auto smallerTimeUnit = /* calculate smaller unit using current unit */
    if (timeUnit) return getTimeElapsed(timeUnit, ???, smallerTimeUnit, ???);
    else return prettyTimeElapsed(diff, smallerTimeUnit);
}

I was thinking of doing this:

auto timeUnits = {chrono::hours(), chrono::minutes(), ..., chrono::nanoseconds()};

Then I could take the pointer (or even an index) to the time unit and pass it to the function.

The problem is that I don't know how to generalize these structs.

CLion highlights an error Deduced conflicting types (duration<[...], ratio<3600, [...]>> vs duration<[...], ratio<60, [...]>>) for initializer list element type

The best general advice when working with chrono is to only escape the type-system (with the use of .count() ) when you absolutely have to. This could be interfacing with C or some C++ library that doesn't understand chrono. Before C++ 20 this also means outputting to a stream.

If we keep ourselves inside the type system, we can get a lot of nice conversions that are always correct.

Let's correct the code in the question to reflect this:

#include <iostream>
#include <chrono>
#include <sstream>

std::string getTimeElapsed(long time1, const std::string &unit1, long time2 = 0, const std::string &unit2 = "") {
    std::stringstream s;
    s << time1 << " [" << unit1 << "]";
    if (time2) s << " " << time2 << " [" << unit2 << "]";
    return s.str();
}

int main() {
    auto begin = std::chrono::system_clock::now();
    // algorithm goes here
    auto solution = "solution"; /* can be anything */
    auto end = std::chrono::system_clock::now();
    auto diff = end - begin;

    std::string timeElapsed{""};
    // Let's make the typing and reading easier for us but requires C++14
    using namespace std::chrono_literals;
    auto hours = std::chrono::duration_cast<std::chrono::hours>(diff);
    auto minutes = std::chrono::duration_cast<std::chrono::minutes>(diff % 1h);
    if (hours != 0h) {
        // We need to escape the type system to call getTimeElapsed
        timeElapsed = getTimeElapsed(hours.count(), "h", minutes.count(), "min");
    } else {
        auto seconds = std::chrono::duration_cast<std::chrono::seconds>(diff % 1min);
        if (minutes != 0min) {
            timeElapsed = getTimeElapsed(minutes.count(), "min", seconds.count(), "s");
        } else {
            auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(diff % 1s);
            if (seconds != 0s) {
                timeElapsed = getTimeElapsed(seconds.count(), "s", milliseconds.count(), "ms");
            } else {
                auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(diff % 1ms);
                if (milliseconds != 0ms) {
                    timeElapsed = getTimeElapsed(milliseconds.count(), "ms", microseconds.count(), "μs");
                } else {
                    auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(diff % 1us);
                    if (microseconds != 0us) {
                        timeElapsed = timeElapsed = getTimeElapsed(microseconds.count(), "μs", nanoseconds.count(), "ns");
                    } else timeElapsed = getTimeElapsed(nanoseconds.count(), "ns");
                }
            }
        }
    }

    std::cout << "Solution [" << solution << "] found in " << timeElapsed << std::endl;

    return 0;
}

Now we are sticking to chrono for as long as we can. Calling getTimeElapsed is not chrono compatible, yet.

I'm not entirely satisfied, so let's support duration s in getTimeElapsed as well:

template <typename Duration1, typename Duration2>
std::string getTimeElapsed(Duration1 time1, const std::string &unit1, Duration2 time2, const std::string &unit2) {
    std::stringstream s;
    s << time1.count() << " [" << unit1 << "]";
    if (time2 != Duration2::zero()) s << " " << time2.count() << " [" << unit2 << "]";
    return s.str();
}

template <typename Duration1>
std::string getTimeElapsed(Duration1 time1, const std::string &unit1) {
    std::stringstream s;
    s << time1.count() << " [" << unit1 << "]";
    return s.str();
}

We require two versions of getTimeElapsed since, in the last else we only use one time and unit argument pair, which means we cannot satisfy the template parameter requirements for two Duration types. Now the code is looking much better (keeping only the relevant changes):

...
    if (hours != 0h) {
        timeElapsed = getTimeElapsed(hours, "h", minutes, "min");
    } else {
        auto seconds = std::chrono::duration_cast<std::chrono::seconds>(diff % 1min);
        if (minutes != 0min) {
            timeElapsed = getTimeElapsed(minutes, "min", seconds, "s");
        } else {
            auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(diff % 1s);
            if (seconds != 0s) {
                timeElapsed = getTimeElapsed(seconds, "s", milliseconds, "ms");
            } else {
                auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(diff % 1ms);
                if (milliseconds != 0ms) {
                    timeElapsed = getTimeElapsed(milliseconds, "ms", microseconds, "μs");
                } else {
                    auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(diff % 1us);
                    if (microseconds != 0us) {
                        timeElapsed = timeElapsed = getTimeElapsed(microseconds, "μs", nanoseconds, "ns");
                    } else timeElapsed = getTimeElapsed(nanoseconds, "ns");
                }
            }
        }
    }
    ...

Great, However, we are still inviting users to send whatever they want to getTimeElapsed which, unless they happen to have a .count() member, will result in a compiler error. Let's constrain our template just a little bit:

template <typename Rep1, typename Ratio1, typename Rep2, typename Ratio2>
std::string getTimeElapsed(std::chrono::duration<Rep1, Ratio1> time1, const std::string &unit1, std::chrono::duration<Rep2, Ratio2>  time2, const std::string &unit2) {
    std::stringstream s;
    s << time1.count() << " [" << unit1 << "]";
    if (time2 != time2.zero()) s << " " << time2.count() << " [" << unit2 << "]";
    return s.str();
}

template <typename Rep, typename Ratio>
std::string getTimeElapsed(std::chrono::duration<Rep, Ratio> time1, const std::string &unit1) {
    std::stringstream s;
    s << time1.count() << " [" << unit1 << "]";
    return s.str();
}

We don't need to change the calling code for this one. I believe that this is sufficient to help you understand how to use std::chrono::duration in a more generic context, which whas a sub-question that you had.

Now we can start to tackle your question which I think (from reading the comments) is actually "How can I tidy up the nested if statements and print out only the first two, non-zero units."

This is not as simple as it would first appear. In my opinion, recursion is rarely the answer. Thinking of it as a loop over the types of unit is also over-engineering it and you would need to write some code to get the index of the current type from a tuple, increase it by one, then use that to index the same tuple to get the next unit with higher resolution. Then, when all that is said and done, you still need to know what unit to print to give the value its context. I would rather see the getTimeElapsed written as follows:

std::string getTimeElapsed(std::chrono::nanoseconds elapsed, size_t maxUnits = 2)
{
    using namespace std::chrono_literals;
    std::ostringstream formatted("");

    int usedUnits{};

    auto hours = std::chrono::duration_cast<std::chrono::hours>(elapsed);
    if (hours != 0h)
    {
        formatted << hours.count() << " [h] ";
        ++usedUnits;
    }

    auto minutes = std::chrono::duration_cast<std::chrono::minutes>(elapsed % 1h);
    if (minutes != 0min)
    {
        formatted << minutes.count() << " [min] ";
        ++usedUnits;
    }

    auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed % 1min);
    if (seconds != 0min && usedUnits != maxUnits)
    {
        formatted << seconds.count() << " [s] ";
        ++usedUnits;
    }

    auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed % 1s);
    if (milliseconds != 0ms && usedUnits != maxUnits)
    {
        formatted << milliseconds.count() << " [ms] ";
        ++usedUnits;
    }

    auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(elapsed % 1ms);
    if (microseconds != 0us && usedUnits != maxUnits)
    {
        formatted << microseconds.count() << " [us] ";
        ++usedUnits;
    }

    auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed % 1us);
    if (nanoseconds != 0us && usedUnits != maxUnits)
    {
        formatted << nanoseconds.count() << " [us] ";
        ++usedUnits;
    }

    return formatted.str();
}

Take the total elapsed time as std::chrono::nanoseconds (which you already have from end - begin ) and pass it in to getTimeElapsed . We now do the same calculations as before to get the component units, but also keep track of how many units we have calculated. If elapsed is 1'000'000'000ns, then the result is "1 [s] "; if elapsed is 1'234'568ns, then the result is "1 [ms] 234 [us] ". There is trailing space, but I will leave that to you to fix.

This also means that we no longer require the template s we had refactored before, but I added them to show my thought process throughout this refactoring. The final program looks like:

#include <chrono>
#include <iostream>
#include <sstream>

std::string getTimeElapsed(std::chrono::nanoseconds elapsed, size_t maxUnits = 2)
{
    using namespace std::chrono_literals;
    std::ostringstream formatted("");

    int usedUnits{};

    auto hours = std::chrono::duration_cast<std::chrono::hours>(elapsed);
    if (hours != 0h)
    {
        formatted << hours.count() << " [h] ";
        ++usedUnits;
    }

    auto minutes = std::chrono::duration_cast<std::chrono::minutes>(elapsed % 1h);
    if (minutes != 0min)
    {
        formatted << minutes.count() << " [min] ";
        ++usedUnits;
    }

    auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed % 1min);
    if (seconds != 0min && usedUnits != maxUnits)
    {
        formatted << seconds.count() << " [s] ";
        ++usedUnits;
    }

    auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed % 1s);
    if (milliseconds != 0ms && usedUnits != maxUnits)
    {
        formatted << milliseconds.count() << " [ms] ";
        ++usedUnits;
    }

    auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(elapsed % 1ms);
    if (microseconds != 0us && usedUnits != maxUnits)
    {
        formatted << microseconds.count() << " [us] ";
        ++usedUnits;
    }

    auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed % 1us);
    if (nanoseconds != 0us && usedUnits != maxUnits)
    {
        formatted << nanoseconds.count() << " [us] ";
        ++usedUnits;
    }

    return formatted.str();
}

int main() {
    auto begin = std::chrono::system_clock::now();
    // algorithm goes here
    auto solution = "solution"; /* can be anything */
    auto end = std::chrono::system_clock::now();
    auto diff = end - begin;

    using namespace std::chrono_literals;
    std::cout << "Solution [" << solution << "] found in " << getTimeElapsed(1'234'567ns) << std::endl;

    return 0;
}

If you want to take this one step further and never need to escape the type system, then I would suggest looking in to Howard Hinnant's date library. This library is the basis for the new chrono functionality in C++20 and brings string formatting to the table. Simply include date.h from the library in whatever way is appropriate for you and modify getTimeElapsed as follows:

std::string getTimeElapsed(std::chrono::nanoseconds elapsed, size_t maxUnits = 2)
{
    using namespace std::chrono_literals;
    std::ostringstream formatted("");

    int usedUnits{};

    auto hours = std::chrono::duration_cast<std::chrono::hours>(elapsed);
    if (hours != 0h)
    {
        formatted << hours << " ";
        ++usedUnits;
    }

    auto minutes = std::chrono::duration_cast<std::chrono::minutes>(elapsed % 1h);
    if (minutes != 0min)
    {
        formatted << minutes << " ";
        ++usedUnits;
    }

    auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed % 1min);
    if (seconds != 0min && usedUnits != maxUnits)
    {
        formatted << seconds << " ";
        ++usedUnits;
    }

    auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed % 1s);
    if (milliseconds != 0ms && usedUnits != maxUnits)
    {
        formatted << milliseconds << " ";
        ++usedUnits;
    }

    auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(elapsed % 1ms);
    if (microseconds != 0us && usedUnits != maxUnits)
    {
        formatted << microseconds << " ";
        ++usedUnits;
    }

    auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed % 1us);
    if (nanoseconds != 0us && usedUnits != maxUnits)
    {
        formatted << nanoseconds << " ";
        ++usedUnits;
    }

    return formatted.str();
}

Using the same values as before, the results would now be: "1ms 234us " for 1'234'567ns.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM