5

Say I have some wrapper function, in which I'd like to do some setup, call a callback (and save its result), do some cleanup, and then return what the callback returned:

#include <functional>
#include <utility>

template<class F, typename... Args>
decltype(auto) wrap(F&& func, Args&... args) {
    // Do some stuff before
    auto result = std::invoke(std::forward<decltype(func)>(func),
                              std::forward<Args>(args)...);
    // Do some stuff after
    return result;
}

A practical example of this is a timer utility function that returns the elapsed time of the function call as well as its return value (in a tuple, perhaps).

Such a function works fine for callables with return types:

void foo() {
    auto a = 1;
    wrap([](auto a) { return 1; }, a);
}

But with a callable with void return type, during the template specialization the compiler complains that auto result has incomplete type void:

void foo() {
    auto a = 1;
    wrap([](auto a) {}, a);
}

This makes sense of course, because while you can return void(), you can't store it in a variable.

I want wrap to work for both kinds of callables. I've attempted using std::function to give two signatures for wrap:

  1. template<class T, typename... Args> decltype(auto) wrap(std::function<T(Args...)>, Args&... args)
  2. template<typename... Args> decltype(auto) wrap(std::function<void(Args...)>, Args&... args)

The first of those will continue to match callables with a non-void return, but the latter fails to match those with return type void.

Is there a way to make wrap work in both the return type void and non-void callable cases?

Bailey Parker
  • 15,599
  • 5
  • 53
  • 91

2 Answers2

6

The way I like to solve this problem is with regular void. Well, we don't actually have proper regular void, but we can make our own type Void that is regular, that is kind of like void. And then provide a wrapper around invoke that understands that.

The shortest version (there's more detail in that blog):

struct Void { };

template <typename F, typename ...Args,
    typename Result = std::invoke_result_t<F, Args...>,
    std::enable_if_t<!std::is_void_v<Result>, int> = 0>
Result invoke_void(F&& f, Args&& ...args) {
    return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}

// void case
template <typename F, typename ...Args,
    typename Result = std::invoke_result_t<F, Args...>,
    std::enable_if_t<std::is_void_v<Result>, int> = 0>
Void invoke_void(F&& f, Args&& ...args) {
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    return Void();
}

This lets you, in your original code, do:

template<class F, typename... Args>
decltype(auto) wrap(F&& func, Args&... args) {
    // Do some stuff before
    auto result = invoke_void(std::forward<decltype(func)>(func),
                              std::forward<Args>(args)...);
    // Do some stuff after
    return result;
}

And this works, even if func returns void.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • That's clever. Although, the fact that you need to do such contortions for something so straightforward is really disappointing. I was about to ask about the (inconsequential but irksome) fact that for `void` `func`, wrap's return type is `Void` instead of `void`, but the article you linked to suggests an elegant way of casting out of that using `std::conditional_t`. Thanks for the advice! – Bailey Parker Jul 21 '18 at 08:30
  • 1
    @BaileyParker Yeah, [this](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html) would be better. But this is what we have today. – Barry Jul 21 '18 at 08:32
  • I just read your blog post. Very nice! However, I think you could just mark `Void` as `final` so you don't need the SFINAEing constructor. – Henri Menke Jul 21 '18 at 09:09
  • @Henri That's a pretty weak reason to use `final`, imo. If you're going to prevent potentially valuable uses of your type, you'd better get more value than "slightly less typing in one constructor" – Barry Jul 21 '18 at 18:22
  • @Barry But `Void` is just there to encapsulate `void` and since you can't derive from `void` why would you ever want to derive from `Void`? Deriving from something that represents “not a type” doesn't make a lot of sense to me, but I'm open to other opinions. – Henri Menke Jul 21 '18 at 22:12
  • @Henri Well, you can't have an object of type `void` either, so drawing comparisons from there doesn't get you anywhere. `void` does not represent "not a type" - it most certainly is a type. It is _the_ unit type. – Barry Jul 22 '18 at 11:26
4

If the “Do some stuff after” does not use the result variable you could avoid SFINAE by using your favourite scope guard library. I have provided a very naïve implementation here but you can find more elaborate ones online.

#include <functional>

template <typename F>
struct ScopeExit_impl {
    F f;
    ScopeExit_impl(F f) : f(std::move(f)) {}
    ~ScopeExit_impl() noexcept { f(); }
};

template <typename F>
ScopeExit_impl<F> ScopeExit(F &&f) {
    return ScopeExit_impl<F>{std::forward<F>(f)};
}

template <class F, typename... Args>
decltype(auto) wrap(F &&func, Args &... args) {
    // Do some stuff before
    auto exit = ScopeExit([&]() {
        // Do some stuff after
    });
    return std::invoke(std::forward<decltype(func)>(func),
                       std::forward<Args>(args)...);
}

int main() {
    auto a = 1;
    wrap([](auto a) { return 1; }, a);
    wrap([](auto a) {}, a);
}
Henri Menke
  • 10,705
  • 1
  • 24
  • 42