Here is the original question. I have extensively revised and redesigned my custom scoped timer for my needs using the feedback I got from two helpful members of this community. However I was told to ask a new question in case I want my new solution to be reviewed.
Here is the revised version (live on Godbolt):
#include <chrono>
#include <type_traits>
#include <utility>
#include <functional>
#include <thread> // only needed for client code
#include <cstdio> // ditto
#include <fmt/core.h> // ditto
#include <fmt/chrono.h> // ditto
template < class Duration = std::chrono::microseconds,
class Clock = std::chrono::steady_clock >
requires ( std::chrono::is_clock_v<Clock> &&
requires { std::chrono::time_point<Clock, Duration>{ }; } )
struct [[ nodiscard ]] ScopedTimer
{
using clock = Clock;
using duration = Duration;
using rep = Duration::rep;
using period = Duration::period;
using time_point = std::chrono::time_point<Clock, Duration>;
using callback_type = std::add_pointer_t<void ( Duration ) noexcept>;
static_assert( std::is_nothrow_invocable_r_v<void, callback_type, duration> );
time_point const start { now( ) };
callback_type callback;
explicit ScopedTimer( callback_type const call_back = nullptr ) noexcept( noexcept( now( ) ) )
: callback { call_back }
{
}
ScopedTimer( const ScopedTimer& rhs ) = default;
ScopedTimer( ScopedTimer&& rhs ) noexcept
: start { std::move( rhs.start ) },
callback { std::exchange( rhs.callback, nullptr ) }
{
}
~ScopedTimer( )
{
if ( callback == nullptr )
return;
if constexpr ( noexcept( now( ) ) )
{
const time_point end { now( ) };
std::invoke_r<void>( callback, end - start );
}
else
{
try
{
const time_point end { now( ) };
std::invoke_r<void>( callback, end - start );
}
catch ( ... ) { }
}
}
[[ nodiscard ]] duration
elapsed_time( ) const& noexcept( noexcept( now( ) ) )
{
return now( ) - start;
}
[[ nodiscard ]] time_point static
now( ) noexcept( noexcept( clock::now( ) ) )
{
return std::chrono::time_point_cast<duration>( clock::now( ) );
}
};
// --------------------- client code e.g. in Main.cpp -----------------------
using std::chrono_literals::operator""ms;
template <class Duration, class Clock>
ScopedTimer<Duration, Clock> func( ScopedTimer<Duration, Clock> timer )
{
timer.callback = []( const auto duration ) noexcept
{
try
{
fmt::print( stderr, "\nTimer in func took {}\n", duration );
}
catch ( ... ) { }
};
std::this_thread::sleep_for( 200ms );
return timer;
}
int main( )
{
ScopedTimer timer { []( const auto duration ) noexcept
{
try
{
fmt::print( stderr, "\nTimer took {}\n", duration );
}
catch ( ... ) { }
} };
std::this_thread::sleep_for( 100ms );
ScopedTimer t;
auto t2 = func( std::move( timer ) ); // timer is now moved-from, so
// its dtor won't invoke its callback
t.callback = []( const std::chrono::microseconds d ) noexcept {
fmt::print( stderr, "\nTimer t took {}\n", d ); };
std::this_thread::sleep_for( 500ms );
}
Notable changes from the previous solution:
ScopedTimer
now has a move and a copy constructor that behave as I expect. The move constructor assignsnullptr
to thecallback
member of the moved-from object so whenever its destructor is called, it checks thecallback
member and if it'snullptr
then it realizes that*this
has been moved from and that it should not invoke its callback function. With that said, I could not implement the assignment operators apparently because thestart
member isconst
so it cannot be reassigned in the assignment operators.- It now has several
using
declarations for the typenames that are used by the struct (similar to classes in std library) so that the client code can have easy access to these typenames if needed. - Added a
[[nodiscard]]
attribute to the struct's declaration so that the compiler gives warnings whenever aScopedTimer
instance returned from a function is ignored. - Defined (and thus restricted) the
callback_type
as a pointer type to a function that takes aduration
and returnvoid
and must benoexcept
. - Used a static assertion to make sure that the
callback_type
isnothrow
-invocable. However, I'm not sure whether this assertion is redundant or not. - Assigned
nullptr
(as the default argument) to thecall_back
parameter of the constructor so that the client code can create an object without passing a callback pointer to the constructor (i.e. default construction is possible). - Used
std::invoke_r
to invoke the callback inside the destructor. - Added a non-static member function that retrieves the elapsed time when called which can come in handy for some purposes.
- The
now()
function is declared as astatic
member function since it doesn't access any non-static members of the struct. - Because the
callback_type
requires it, any callback function passed to aScopedTimer
object needs to be marked asnoexcept
. This requirement exists because thecallback
will be invoked inside the destructor so it should not throw. This puts the responsibility on the client code to make sure that any exceptions thrown inside the passed callback function are properly handled.
Regarding the above snippet, I have defined a sample function named func
that represents how a ScopedTimer
object (ScopedTimer timer
) can be moved/copied to different functions and that they can return it to their call site. Also it is shown that a ScopedTimer
object (ScopedTimer t;
) can be default-constructed (i.e. without specifying a callback) and then be assigned a callback at a later stage.
With all that said, three things still bother me:
- Firstly, in case
now()
throws inside the destructor, it is caught by a catch-all clause and ignored. I could not find an easy way to pass the exception (or maybe itsstd::exception_ptr
) to the call site where theScopedTimer
object was created. Is this really an issue? I guess if done correctly, it could increase the code correctness; - Secondly, my current implementation of the
ScopedTimer::callback_type
doesn't allow lambda expressions that have a non-empty capture clause (e.g.[&]
or[=]
). Those types of capture clauses could be useful for the client code in some ways. But my current code does not compile when those capture clauses are used. I have discovered a new C++23 class template called std::move_only_function which probably could be used as thecallback_type
(e.g.using callback_type = std::move_only_function<void ( Duration ) const noexcept>;
) to solve this issue. - Thirdly, I would like to prevent prvalue objects from calling
elapsed_time()
since it will always result in a 0 as a duration being returned (e.g. 0 ms). I mean semantically it does not make much sense if a prvalue object calls that member function. Can this be restricted?
As always, any further suggestions are appreciated.