I do a lot of work with higher-order functions or templates that modify functions in C
A pattern that's often useful is a function-modifier. For example, I use this simple wallclock benchmark quite a lot:
#include <chrono>
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock, typename... Args>
auto wallclock(const auto& f, Args&... a){
auto start = Clock::now();
auto ret = f(a...);
return std::make_tuple(ret,(std::chrono::duration<std::ratio<1,Unit>(Clock::now() - start).count());
}
This keeps even arguments-by-reference intact, and profiles the function nicely.
However, this invocation doesn't work with void functions, and I end up having to write a function with a different name to call for them, like so:
#include <chrono>
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock, typename... Args>
auto wallclock_but_void(const auto& f, Args&... a){
auto start = Clock::now();
return f(a...), (std::chrono::duration<std::ratio<1,Unit>(Clock::now() - start).count();
}
I guess this is okay, but there are more complicated cases where it gets annoying. I'm wondering if there's a good way to specialize a function or template based on the return type of an arbitrary callable, so that the interface can be the same, and I keep thinking maybe I could do it with concepts. Like maybe
template <typename F, typename... Args>
concept vreturn = requires(F&& f, Args&... a){
{ std::invoke(std::forward<F>(f), std::forward<Args>(a)...) } -> std::same_as<void>;
};
template <typename F, typename... Args>
concept nvreturn = !vreturn<F, Args...>;
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock, typename... Args>
auto wallclock(const auto& f, Args&... a) requires vreturn<decltype(f), Args...> {
auto start = Clock::now();
return f(a...), (Clock::now() - start).count();
}
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock, typename... Args>
auto wallclock(const auto& f, Args&... a) requires (!vreturn<decltype(f), Args...>) {
auto start = Clock::now();
auto ret = f(a...);
return std::make_tuple(ret, (Clock::now() - start).count());
}
But this nonetheless pattern-matches to the non-void version even when I call it with a void function. I'm not sure how I can get the behavior I want, but I feel like it has to be possible
EDIT: (Min reproducible example)
Because someone asked, the code that failed was (With the above definitions in place)
#include <iostream>
#include <random>
void plusassign(int a, int b, int& x){
x = a b;
}
int plus(int a, int b){
return a b;
}
int main(int, char**){
std::random_device rd;
std::default_random_engine dre;
dre.seed(rd());
std::uniform_int_distribution<int> dist(0,0xFFFFFFFF);
int a = dist(dre)
, b = dist(dre)
, x;
auto t = wallclock(plusassign, a, b, x);
std::cout << a << " " << b << " = " << x << " (" << t << "ns)\n";
auto [c, t2] = wallclock(plus, a, b);
std::cout << a << " " << b << " = " << c << " (" << t2 << "ns)\n";
return 1;
}
This gave me a compiler error with g 11.2.0 (That "auto ret" invalidly assigned a void result)
CodePudding user response:
Your issue is simply that you are not correctly forwarding arguments. Replace Args&
with Args&&
everywhere and also replace a...
with std::forward<Args>(a)...
. Then it will work as expected.
With just Args&
you are never deducing the value category for Args
and std::invoke
will always try to call the callable with a rvalue, which fails if a function parameter is non-const lvalue reference.
Also, I would suggest to be consistent and also use std::invoke
in the function definitions, given that you use it in the concept. std::invoke
allows more forms of function calls than a simple call expression.
CodePudding user response:
Instead of using concept, you can just simply use if constexpr
in the function body to determine whether the return type of the callable is void
, and return the appropriate type.
#include <functional>
#include <tuple>
#include <chrono>
template<size_t Unit = 1000, class Clock = std::chrono::steady_clock,
typename F, typename... Args>
auto wallclock(const F& f, Args&... a) {
auto start = Clock::now();
if constexpr (std::same_as<std::invoke_result_t<const F&, Args&...>, void>)
return f(a...), (Clock::now() - start).count();
else
return std::make_tuple(f(a...), (Clock::now() - start).count());
}
CodePudding user response:
I found a way to do this, and it's super weird to me that this works:
template <typename F, typename... Args>
concept vreturn = requires(F&& f, Args&... a){
{ std::invoke(std::forward<F>(f), std::forward<Args>(a)...) } -> std::same_as<void>;
};
template <typename F, typename... Args>
concept nvreturn = !vreturn<F, Args...>;
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock>
auto wallclock(const auto& f) requires vreturn<decltype(f)> {
auto start = Clock::now();
return f(), (Clock::now() - start).count();
}
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock, typename... Args>
auto wallclock(const auto& f, Args&... a) requires vreturn<decltype(f), Args...> {
return wallclock([&](){ f(a...); });
}
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock>
auto wallclock(const auto& f) requires (!vreturn<decltype(f)>) {
auto start = Clock::now();
auto ret = f();
return std::make_tuple(ret, (Clock::now() - start).count());
}
template <size_t Unit = 1000, class Clock = std::chrono::steady_clock, typename... Args>
auto wallclock(const auto& f, Args&... a) requires (!vreturn<decltype(f), Args...>) {
return wallclock([&](){ return f(a...); });
}
This will correctly pattern-match void versus non-void functions... but why tho?