Some generic code manipulating functions, need to operate differently depending on whether a function has a return value or not. E.g., borrowing a problem from this question, say we need to write a time_it
function takes a function and some arguments, runs it, and prints the elapsed time. The following code can do this:
#include <chrono>
#include <type_traits>
#include <cmath>
#include <iostream>
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) ->
typename std::enable_if<
!std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::type
{
const auto start = std::chrono::system_clock::now();
auto const res = fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return res;
}
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) ->
typename std::enable_if<
std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
void>::type
{
const auto start = std::chrono::system_clock::now();
fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
}
int main()
{
time_it([](double x){return std::cos(x);}, 3.0);
time_it([](double x){}, 3.0);
}
As can be seen, there is a difference between the cases of the function returning a value or not. In the former case, the value must be stored, the elapsed time printed, and the value returned; in the latter case, after printing the elapsed time, nothing more needs to be done.
The question is how to deal with both case:
The above code uses std::enable_if
and is_void
, but the first (cumbersome in itself) argument to is_void
is repeated as the last argument to enable_if
- this is cumbersome and smells, esp. as much of the body is repeated.
The aforementioned answer bypasses the problem by having the elapsed time being printed as the byproduct of a destructor of some elapsed-timer class being called. It's a nice idea, but in more complex uses would lead to convoluted code (substantial work is done in a destructor of some separate class - it's not a natural flow).
Is there a better way of doing this?
You could isolate the invoke-and-store code:
template<class R>
struct invoke_and_store_t {
std::experimental::optional<R> ret;
template<class F, class...Args>
invoker_t&& operator()(F&& f, Args&&...args)&& {
ret.emplace( std::forward<F>(f)(std::forward<Args>(args)...) );
return std::move(*this);
}
R&& get()&&{ return std::move( *ret ) ); }
template<class F>
auto chain(F&& f)&&{
return [r = std::move(*this).get(),f=std::move<F>(f)](auto&&...args)mutable
{
return std::move(f)(std::move(r), decltype(args)(args)...);
};
}
};
template<>
struct invoke_and_store_t<void> {
template<class F, class...Args>
invoker_t&& operator()(F&& f, Args&&...args)&& {
std::forward<F>(f)(std::forward<Args>(args)...);
return std::move(*this);
}
void get()&&{}
template<class F>
auto chain(F&& f)&&{
return [f=std::move<F>(f)](auto&&...args)mutable{
return std::move(f)(decltype(args)(args)...);
};
}
};
template<class F, class...Args, class R=std::decay_t<std::result_of_t<F(Args...)>>>
auto invoke_and_store(F&& f, Args&&...args) {
return invoke_and_store_t<R>{}(std::forward<F>(f), std::forward<Arg>(args)...);
}
now your code becomes:
template <class R, class Fn, class... Args>
R time_it(tag<R>, Fn&& fn, Args&&... args)
{
const auto start = std::chrono::system_clock::now();
auto&& res = invoke_and_store(
std::forward<Fn>(fn), std::forward<Args>(args)...
);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return std::move(res).get();
}
which now has the same body for the two cases. I factored out the problem of storing the return value (or not) into a helper, thus making the code that wants to deal with it not have to worry about it.
I also included chain
, which takes a function object and either passes it the previous return value as the first argument, or doesn't, depending on if the previous return value was void. I find that pattern pretty common in monad/functor-like code.
template<class A, class B>
auto then( A&& a, B&& b ) {
return [a = std::forward<A>(a), B=std::forward<B>(b)](auto&&...args)mutable{
return
invoke_and_store(std::move(a))
.chain(std::move(b))(decltype(args)(args)...);
};
}
then(a,b)(...)
calls a()
then b(a(),...)
or a()
then b(...)
depending on what a()
returns.