Home > Mobile >  Can you specify a return type (especially a void return type) for a functor in a concept?
Can you specify a return type (especially a void return type) for a functor in a concept?

Time:03-05

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());
}

Demo

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?

  • Related