【问题标题】:Wrapping a callable that may be void return包装一个可能是无效的可调用返回
【发布时间】:2018-07-21 08:14:50
【问题描述】:

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

#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&lt;class T, typename... Args&gt; decltype(auto) wrap(std::function&lt;T(Args...)&gt;, Args&amp;... args)
  2. template&lt;typename... Args&gt; decltype(auto) wrap(std::function&lt;void(Args...)&gt;, Args&amp;... args)

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

有没有办法让 wrap 在返回类型 void 和非void 可调用情况下都起作用?

【问题讨论】:

  • 我没有时间给出完整的答案,但这听起来像是 std::optional 用例。

标签: c++ closures c++17


【解决方案1】:

我喜欢解决这个问题的方法是使用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,这也有效。

【讨论】:

  • 这很聪明。虽然,你需要为如此简单的事情做这样的扭曲,这确实令人失望。我正要问一个(无关紧要但令人讨厌的)事实,即对于 void func,wrap 的返回类型是 Void 而不是 void,但是您链接到的文章提出了一种优雅的方式来摆脱这种情况使用std::conditional_t。感谢您的建议!
  • @BaileyParker 是的,this 会更好。但这就是我们今天所拥有的。
  • 我刚刚阅读了您的博文。非常好!但是,我认为您可以将 Void 标记为 final,这样您就不需要 SFINAEing 构造函数了。
  • @Henri 这是使用final 的一个非常薄弱的​​理由,imo。如果您要阻止对您的类型进行潜在有价值的使用,那么您最好获得比“在一个构造函数中输入更少的内容”更多的价值
  • @Barry 但是Void 只是用来封装void,既然你不能从void 派生,为什么还要从Void 派生?从代表“非类型”的东西派生出来对我来说没有多大意义,但我对其他意见持开放态度。
【解决方案2】:

如果“Do some stuff after”不使用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);
}

【讨论】:

    猜你喜欢
    • 2013-08-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多