HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Scoped time sink implementation

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
sinkimplementationscopedtime

Problem

I have written a little class that is intended to be used to spend unused time in a loop body, sleeping.

That is, I have a loop. I want every iteration to take at least X seconds. If the operations actually complete quicker, the excess time shall be spent sleeping.

My implementation is based on a class that is created in a scope, and whose destructor sleeps for the required amount of time when the object's lifetime ends.

Is this a sane approach to the problem? Are there any flaws in the implementation? Is there potential for improvements regarding coding style?

#include 
#include 

class TimeSink {
public:
    typedef std::chrono::milliseconds duration_ms;

    TimeSink(duration_ms const & min_duration)
    : min_duration(min_duration), start(std::chrono::steady_clock::now()) {}

    ~TimeSink() {
        time_point end = std::chrono::steady_clock::now();
        auto elapsed = end-start;
        auto remaining_time_to_sleep = min_duration - elapsed;
        if (remaining_time_to_sleep > epsilon) {
            std::this_thread::sleep_for(remaining_time_to_sleep);
        }
    }

private:
    typedef std::chrono::time_point time_point;
    const duration_ms epsilon = duration_ms(0);
    duration_ms min_duration;
    time_point start;
};

int main() {
    TimeSink t(TimeSink::duration_ms(1000));
}

Solution

Allow users to customize your TimeSink class

We achieve this by making it a template class:

template 
class time_sink;


This will allow your class to be used in different scenarios; it removes restrictions.

Since the TimeUnit template parameter is the one most likely to be changed, we can provide a default argument for Clock:

template 
class time_sink;


Your destructor can be simplified

Instead of doing...

time_point end = std::chrono::steady_clock::now();
auto elapsed = end-start;
auto remaining_time_to_sleep = min_duration - elapsed;
if (remaining_time_to_sleep > epsilon)
{
    std::this_thread::sleep_for(remaining_time_to_sleep);
}


...we can do:

auto time_spent = Clock::now() - begin;
if ( time_spent < min_duration )
{
    std::this_thread::sleep_for( min_duration - time_spent );
}


I find this clearer and easier to understand.

Consider using std::chrono::high_resolution_clock as your default clock type

This stack overflow question should tell you why that's the preferred clock type for timing function execution.

template 
class time_sink;


Concise usage with the std::chrono_literals namespace and a template function

This would simply make the code shorter. Note that there is a repetition, as we must define time_sink's time unit type.

using namespace std::chrono_literals;
using time_sink = time_sink;
time_sink{ 500ms };


In order to make usage fully concise (avoid repetition), we can use a template function and implement the idea proposed by 5gon12eder, in this comment which simply creates the time_sink object and uses template function type deduction to fill in the details.

template 
time_sink make_time_sink( TimeUnit const minimum_duration )
{
    return time_sink{ minimum_duration };
}


This allows us to create a time_sink in a short and clear way:

using namespace std::chrono_literals;
make_time_sink( 500ms );
// rest of function code, etc.


The improved code

Here's what the final code could look like:

#include 
#include 

template 
class time_sink
{
public:
    time_sink( TimeUnit const minimum_duration ) :
        min_duration{ minimum_duration },
        begin{ Clock::now() }
    {}

    ~time_sink()
    {
        auto time_spent = Clock::now() - begin;
        if ( time_spent  begin;
};

template 
time_sink make_time_sink( TimeUnit const minimum_duration )
{
    return time_sink{ minimum_duration };
}


Sample usage

As 5gon12eder points out, you must also ensure that you keep the return of make_time_sink in order to prevent time_sink's destructor from running early, since destructors are called at the "end of the full expression, for nameless temporaries" (source).

void f()
{
    using namespace std::chrono_literals;

    // we keep the return of make_time_sink in order to prevent
    // time_sink's destructor from executing before the end of the function
    auto ts = make_time_sink( 500ms );
    // ... do work ...
}

int main()
{
    f();
}

Code Snippets

template <typename TimeUnit, typename Clock>
class time_sink;
template <typename TimeUnit, typename Clock = std::chrono::steady_clock>
class time_sink;
time_point end = std::chrono::steady_clock::now();
auto elapsed = end-start;
auto remaining_time_to_sleep = min_duration - elapsed;
if (remaining_time_to_sleep > epsilon)
{
    std::this_thread::sleep_for(remaining_time_to_sleep);
}
auto time_spent = Clock::now() - begin;
if ( time_spent < min_duration )
{
    std::this_thread::sleep_for( min_duration - time_spent );
}
template <typename TimeUnit, typename Clock = std::chrono::high_resolution_clock>
class time_sink;

Context

StackExchange Code Review Q#114189, answer score: 7

Revisions (0)

No revisions yet.