Handling void assignment in C++ generic programming

Another trick might be to exploit the comma operator, something like:

struct or_void {};

template<typename T>
T&& operator,( T&& x, or_void ){ return std::forward<T>(x); }

template <typename F>
auto wrapAndRun(F fn) -> decltype(fn()) {
    // foo();
    auto result = ( fn(), or_void() );
    // bar();
    return decltype(fn())(result);
}

The new C++17 if constexpr addition may be helpful here. You can choose whether to return fn()'s result at compile-time:

#include <type_traits>

template <typename F>
auto wrapAndRun(F fn) -> decltype(fn())
{
    if constexpr (std::is_same_v<decltype(fn()), void>)
    {
        foo();
        fn();
        bar();
    }
    else
    {
        foo();
        auto result = fn();
        bar();
        return result;
    }
}

As you said C++2a is an option as well, you could also make use of concepts, putting a constraint on the function:

template <typename F>
  requires requires (F fn) { { fn() } -> void }
void wrapAndRun(F fn)
{
    foo();
    fn();
    bar();
}

template <typename F>
decltype(auto) wrapAndRun(F fn)
{
    foo();
    auto result = fn();
    bar();
    return result;
}

You can write a simple wrapper class that handles this part of it:

template <class T>
struct CallAndStore {
    template <class F>
    CallAndStore(F f) : t(f()) {}
    T t;
    T get() { return std::forward<T>(t); }
};

And specialize:

template <>
struct CallAndStore<void> {
    template <class F>
    CallAndStore(F f) { f(); }
    void get() {}
};

You can improve usability with a small factory function:

template <typename F>
auto makeCallAndStore(F&& f) -> CallAndStore<decltype(std::declval<F>()())> {
    return {std::forward<F>(f)};
}

Then use it.

template <typename F>
auto wrapAndRun(F fn) {
    // foo();
    auto&& result = makeCallAndStore(std::move(fn));
    // bar();
    return result.get();
}

Edit: with the std::forward cast inside get, this also seems to handle returning a reference from a function correctly.


You can have a look at the ScopeGuard by Alexandrescu: ScopeGuard.h It executes code only when there was no exception.

template<class Fn>
decltype(auto) wrapAndRun(Fn&& f) {
  foo();
  SCOPE_SUCCESS{ bar(); }; //Only executed at scope exit when there are no exceptions.
  return std::forward<Fn>(f)();
}

So in case of no errors, the execution order is: 1. foo(), 2. f(), 3. bar(). And in case of an exception the order is: 1. foo(), 2. f()

Tags:

C++