包装可能无效的可赎回债券

时间:2018-07-21 08:14:50

标签: c++ closures c++17

说我有一些包装函数,我想在其中进行一些设置,调用回调(并保存其结果),进行一些清理,然后返回回调返回的内容:

#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;
}

一个实际的例子是一个计时器实用程序函数,该函数返回函数调用的经过时间及其返回值(也许在一个元组中)。

这样的功能对于具有返回类型的可调用对象来说很好用

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

但是对于具有void返回类型的可调用对象,在模板专门化期间,编译器会抱怨auto result具有不完整的void类型:

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

这当然很合理,因为while you can return void()不能将其存储在变量中。

我希望wrap为两种可调用对象工作。我尝试使用std::functionwrap给出两个签名:

  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)

其中第一个将继续匹配具有非void返回的可调用对象,但后者无法匹配具有返回类型void的那些可调用对象。

有没有一种方法可以使返回类型void和非{void可调用的情况下的包装工作正常?

2 个答案:

答案 0 :(得分:5)

我想解决这个问题的方法是使用regular void。嗯,我们实际上没有合适的常规void,但是我们可以使自己的常规类型Void类似于void。然后提供invoke周围的包装,以了解这一点。

最短的版本(该博客中有更多详细信息):

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

这使您可以使用原始代码执行以下操作:

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

即使func返回void,这仍然有效。

答案 1 :(得分:2)

如果“之后做一些事情”不使用result变量,则可以通过使用喜欢的范围保护库来避免SFINAE。我在这里提供了一个非常幼稚的实现,但是您可以在网上找到更多精致的实现。

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