函数参数返回void或non-void类型

时间:2019-05-22 12:18:10

标签: c++ templates c++14

我正在为将来的库编写一些通用代码。我在模板函数中遇到以下问题。考虑下面的代码:

template<class F>
auto foo(F &&f) {
    auto result = std::forward<F>(f)(/*some args*/);
    //do some generic stuff
    return result;
}

它将正常工作,除非我将返回void的函数传递给它,如:

foo([](){});

现在,当然,我可以使用一些std::enable_if魔术来检查返回类型并为返回void的函数执行特殊化,如下所示:

template<class F, class = /*enable if stuff*/>
void foo(F &&f) {
    std::forward<F>(f)(/*some args*/);
    //do some generic stuff
}

但是,这将完全复制实际上在逻辑上等效的功能的代码。能否以一种通用的方式轻松地以一种优雅的方式同时对void返回和非void返回函数进行处理?

编辑: 函数f()与我要执行的一般操作之间存在数据依赖关系,因此我不考虑这样的代码:

template<class F>
auto foo(F &&f) {
    //do some generic stuff
    return std::forward<F>(f)(/*some args*/);
}

4 个答案:

答案 0 :(得分:14)

如果您可以将“一些通用的东西”放在bar类的析构函数中(在安全性try / catch块中,如果您不确定是否不会引发异常,如Drax所指出的那样) ),您只需编写

template <typename F>
auto foo (F &&f)
 {
   bar b;

   return std::forward<F>(f)(/*some args*/);
 }

因此,编译器计算f(/*some args*/),执行b的析构函数并返回计算值(或不返回任何值)。

观察到return func();(其中func()是返回void的函数)完全合法。

答案 1 :(得分:9)

某些专业化是必要的。但是这里的目标是避免专门化函数本身。但是,您可以专门研究一个助手类。

已在带有-std=c++17的gcc 9.1中进行了测试。

#include <type_traits>
#include <iostream>

template<typename T>
struct return_value {


    T val;

    template<typename F, typename ...Args>
    return_value(F &&f, Args && ...args)
        : val{f(std::forward<Args>(args)...)}
    {
    }

    T value() const
    {
        return val;
    }
};

template<>
struct return_value<void> {

    template<typename F, typename ...Args>
    return_value(F &&f, Args && ...args)
    {
        f(std::forward<Args>(args)...);
    }

    void value() const
    {
    }
};

template<class F>
auto foo(F &&f)
{
    return_value<decltype(std::declval<F &&>()(2, 4))> r{f, 2, 4};

    // Something

    return r.value();
}

int main()
{
    foo( [](int a, int b) { return; });

    std::cout << foo( [](int a, int b) { return a+b; }) << std::endl;
}

答案 2 :(得分:5)

我认为,做到这一点的最佳方法是实际上改变调用可能无效的函数的方式。基本上,我们将返回void的类更改为返回某种类类型Void的类,即出于所有目的和目的,同一类东西并且没有用户真正关心它。

struct Void { };

我们要做的就是包装调用。以下使用C ++ 17名称(std::invokestd::invoke_result_t),但是它们都可以在C ++ 14中实现,而不必大惊小怪:

// normal case: R isn't void
template <typename F, typename... Args, 
    typename R = std::invoke_result_t<F, Args...>,
    std::enable_if_t<!std::is_void<R>::value, int> = 0>
R invoke_void(F&& f, Args&&... args) {
    return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}

// special case: R is void
template <typename F, typename... Args, 
    typename R = std::invoke_result_t<F, Args...>,
    std::enable_if_t<std::is_void<R>::value, int> = 0>
Void invoke_void(F&& f, Args&&... args) {
    // just call it, since it doesn't return anything
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);

    // and return Void
    return Void{};
}

这样做的好处是,您可以直接以要编写的方式编写要编写的代码:

template<class F>
auto foo(F &&f) {
    auto result = invoke_void(std::forward<F>(f), /*some args*/);
    //do some generic stuff
    return result;
}

您不必将所有逻辑推到析构函数中,也不必通过专门化来复制所有逻辑。以foo([]{})的代价返回Void而不是void,这并不是很多费用。

然后如果曾经采用Regular Void,那么您要做的就是将invoke_void换成std::invoke

答案 3 :(得分:-1)

如果您需要在“一些通用的东西”中使用result(在非无效情况下),我提出了一个基于if constexpr的解决方案(因此,不幸的是,不在C ++ 17之前)

说实话,这不是很优雅。

首先,检测f的“真实返回类型”(根据参数)

using TR_t = std::invoke_result_t<F, As...>;

接下来是一个constexpr变量,以查看返回的类型是否为void(只是为了简化以下代码)

constexpr bool isVoidTR { std::is_same_v<TR_t, void> };

现在,我们定义(可能)“假返回类型”:int,当真实返回类型为void时,否则为TR_t

using FR_t = std::conditional_t<isVoidTR, int, TR_t>;

然后,我们将指向结果值的智能指针定义为“假返回类型”的指针(在无效情况下为int

std::unique_ptr<FR_t>  pResult;

通过指针而不是类型为“假返回类型”的简单变量,我们也可以在TR_t不是默认可构造或不可分配的情况下进行操作(限制,由Barry指出(谢谢),该答案的第一版)。

现在,使用if constexpr,执行f的两种情况(恕我直言,这是最丑的部分,因为我们必须编写两次相同的f调用)

if constexpr ( isVoidTR )
   std::forward<F>(f)(std::forward<As>(args)...);
else
   pResult.reset(new TR_t{std::forward<F>(f)(std::forward<As>(args)...)});

此后,可以使用result(在非无效情况下)以及`isVoidTR)的“一些通用材料”。

最后,另外一个if constexpr

if constexpr ( isVoidTR )
   return;
else
   return *pResult;

正如Barry指出的那样,此解决方案具有一些重要的缺点,因为(不是void个案例)

  • 需要分配
  • 需要与return对应的额外副本
  • 如果TR_t(由f()返回的类型)是引用类型,则
  • 根本不起作用

无论如何,以下是完整的C ++ 17示例

#include <memory>
#include <type_traits>

template <typename F, typename ... As>
auto foo (F && f, As && ... args)
 {
   // true return type
   using TR_t = std::invoke_result_t<F, As...>;

   constexpr bool isVoidTR { std::is_same_v<TR_t, void> };

   // (possibly) fake return type
   using FR_t = std::conditional_t<isVoidTR, int, TR_t>;

   std::unique_ptr<FR_t>  pResult;

   if constexpr ( isVoidTR )
      std::forward<F>(f)(std::forward<As>(args)...);
   else
      pResult.reset(new TR_t{std::forward<F>(f)(std::forward<As>(args)...)});

   // some generic stuff (potentially depending from result,
   // in non-void cases)

   if constexpr ( isVoidTR )
      return;
   else
      return *pResult;
 }

int main ()
 {
   foo([](){});

   //auto a { foo([](){}) };  // compilation error: foo() is void

   auto b { foo([](auto a0, auto...){ return a0; }, 1, 2l, 3ll) };

   static_assert( std::is_same_v<decltype(b), int> );

 }