我正在为将来的库编写一些通用代码。我在模板函数中遇到以下问题。考虑下面的代码:
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*/);
}
答案 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::invoke
和std::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> );
}