避免重复 SFINAE 区分 void 和 Non-void Return 类型
Avoiding Repetition For SFINAE Differentiating Between void and Non-void Return Types
一些通用代码操作函数,需要根据函数是否具有 return 值进行不同的操作。例如,从 this question 借用一个问题,假设我们需要编写一个 time_it
函数接受一个函数和一些参数,运行它,并打印经过的时间。下面的代码可以做到这一点:
#include <chrono>
#include <type_traits>
#include <cmath>
#include <iostream>
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) ->
typename std::enable_if<
!std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::type
{
const auto start = std::chrono::system_clock::now();
auto const res = fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return res;
}
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) ->
typename std::enable_if<
std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
void>::type
{
const auto start = std::chrono::system_clock::now();
fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
}
int main()
{
time_it([](double x){return std::cos(x);}, 3.0);
time_it([](double x){}, 3.0);
}
可以看出,函数return赋值和不赋值的情况是有区别的。在前一种情况下,必须存储该值,打印经过的时间,并将值returned;在后一种情况下,打印经过的时间后,无需再做任何事情。
问题是如何处理这两种情况:
上面的代码使用了std::enable_if
and is_void
, but the first (cumbersome in itself) argument to is_void
is repeated as the last argument to enable_if
- this is cumbersome and smells,特别是。 body 重复了多少。
上述答案通过将经过的时间打印为某些 elapsed-timer class 的析构函数的副产品来绕过问题。这是个好主意,但在更复杂的用途中会导致复杂的代码(大量工作是在一些单独的 class 的析构函数中完成的——这不是自然流程)。
有更好的方法吗?
有时您只需要一个简单的标签类型:
template <class > struct tag { };
您可以根据包装结果类型调度 time_it
:
template <class Fn, class... Args, class R = std::result_of_t<Fn&&(Args&&...)>>
R time_it(Fn fn, Args&&... args)
{
return time_it(tag<R>{}, fn, std::forward<Args>(args)...);
}
然后我们只有 void
和非 void
版本的重载:
template <class R, class Fn, class... Args>
R time_it(tag<R>, Fn fn, Args&&... args)
{
const auto start = std::chrono::system_clock::now();
auto const res = fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return res;
}
template <class Fn, class... Args>
void time_it(tag<void>, Fn fn, Args&&... args)
{
const auto start = std::chrono::system_clock::now();
fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
}
当然,如果 regular void 获得批准会特别好 - 到那时我们甚至根本不需要特殊情况!
也许一些辅助结构可以解决这个问题?
template <class T>
struct enable_if_not_void: enable_if<!is_void<T>::value, T> { };
和用法:
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&... args) -> typename enable_if_not_void<typename std::result_of<Fn(Args &&...)>::type>::type {
//...
}
您可以隔离调用和存储代码:
template<class R>
struct invoke_and_store_t {
std::experimental::optional<R> ret;
template<class F, class...Args>
invoker_t&& operator()(F&& f, Args&&...args)&& {
ret.emplace( std::forward<F>(f)(std::forward<Args>(args)...) );
return std::move(*this);
}
R&& get()&&{ return std::move( *ret ) ); }
template<class F>
auto chain(F&& f)&&{
return [r = std::move(*this).get(),f=std::move<F>(f)](auto&&...args)mutable
{
return std::move(f)(std::move(r), decltype(args)(args)...);
};
}
};
template<>
struct invoke_and_store_t<void> {
template<class F, class...Args>
invoker_t&& operator()(F&& f, Args&&...args)&& {
std::forward<F>(f)(std::forward<Args>(args)...);
return std::move(*this);
}
void get()&&{}
template<class F>
auto chain(F&& f)&&{
return [f=std::move<F>(f)](auto&&...args)mutable{
return std::move(f)(decltype(args)(args)...);
};
}
};
template<class F, class...Args, class R=std::decay_t<std::result_of_t<F(Args...)>>>
auto invoke_and_store(F&& f, Args&&...args) {
return invoke_and_store_t<R>{}(std::forward<F>(f), std::forward<Arg>(args)...);
}
现在您的代码变为:
template <class R, class Fn, class... Args>
R time_it(tag<R>, Fn&& fn, Args&&... args)
{
const auto start = std::chrono::system_clock::now();
auto&& res = invoke_and_store(
std::forward<Fn>(fn), std::forward<Args>(args)...
);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return std::move(res).get();
}
这两个案例现在具有相同的正文。我解决了将 return 值存储(或不存储)到助手中的问题,从而使想要处理它的代码不必担心它。
我还包括了 chain
,它接受一个函数对象并将前一个 return 值作为第一个参数传递给它,或者不传递,这取决于前一个 return 值无效。我发现这种模式在 monad/functor-like 代码中很常见。
template<class A, class B>
auto then( A&& a, B&& b ) {
return [a = std::forward<A>(a), B=std::forward<B>(b)](auto&&...args)mutable{
return
invoke_and_store(std::move(a))
.chain(std::move(b))(decltype(args)(args)...);
};
}
then(a,b)(...)
调用 a()
然后 b(a(),...)
或 a()
然后 b(...)
取决于 a()
returns.
一些通用代码操作函数,需要根据函数是否具有 return 值进行不同的操作。例如,从 this question 借用一个问题,假设我们需要编写一个 time_it
函数接受一个函数和一些参数,运行它,并打印经过的时间。下面的代码可以做到这一点:
#include <chrono>
#include <type_traits>
#include <cmath>
#include <iostream>
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) ->
typename std::enable_if<
!std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::type
{
const auto start = std::chrono::system_clock::now();
auto const res = fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return res;
}
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) ->
typename std::enable_if<
std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
void>::type
{
const auto start = std::chrono::system_clock::now();
fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
}
int main()
{
time_it([](double x){return std::cos(x);}, 3.0);
time_it([](double x){}, 3.0);
}
可以看出,函数return赋值和不赋值的情况是有区别的。在前一种情况下,必须存储该值,打印经过的时间,并将值returned;在后一种情况下,打印经过的时间后,无需再做任何事情。
问题是如何处理这两种情况:
上面的代码使用了
std::enable_if
andis_void
, but the first (cumbersome in itself) argument tois_void
is repeated as the last argument toenable_if
- this is cumbersome and smells,特别是。 body 重复了多少。上述答案通过将经过的时间打印为某些 elapsed-timer class 的析构函数的副产品来绕过问题。这是个好主意,但在更复杂的用途中会导致复杂的代码(大量工作是在一些单独的 class 的析构函数中完成的——这不是自然流程)。
有更好的方法吗?
有时您只需要一个简单的标签类型:
template <class > struct tag { };
您可以根据包装结果类型调度 time_it
:
template <class Fn, class... Args, class R = std::result_of_t<Fn&&(Args&&...)>>
R time_it(Fn fn, Args&&... args)
{
return time_it(tag<R>{}, fn, std::forward<Args>(args)...);
}
然后我们只有 void
和非 void
版本的重载:
template <class R, class Fn, class... Args>
R time_it(tag<R>, Fn fn, Args&&... args)
{
const auto start = std::chrono::system_clock::now();
auto const res = fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return res;
}
template <class Fn, class... Args>
void time_it(tag<void>, Fn fn, Args&&... args)
{
const auto start = std::chrono::system_clock::now();
fn(std::forward<Args>(args)...);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
}
当然,如果 regular void 获得批准会特别好 - 到那时我们甚至根本不需要特殊情况!
也许一些辅助结构可以解决这个问题?
template <class T>
struct enable_if_not_void: enable_if<!is_void<T>::value, T> { };
和用法:
template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&... args) -> typename enable_if_not_void<typename std::result_of<Fn(Args &&...)>::type>::type {
//...
}
您可以隔离调用和存储代码:
template<class R>
struct invoke_and_store_t {
std::experimental::optional<R> ret;
template<class F, class...Args>
invoker_t&& operator()(F&& f, Args&&...args)&& {
ret.emplace( std::forward<F>(f)(std::forward<Args>(args)...) );
return std::move(*this);
}
R&& get()&&{ return std::move( *ret ) ); }
template<class F>
auto chain(F&& f)&&{
return [r = std::move(*this).get(),f=std::move<F>(f)](auto&&...args)mutable
{
return std::move(f)(std::move(r), decltype(args)(args)...);
};
}
};
template<>
struct invoke_and_store_t<void> {
template<class F, class...Args>
invoker_t&& operator()(F&& f, Args&&...args)&& {
std::forward<F>(f)(std::forward<Args>(args)...);
return std::move(*this);
}
void get()&&{}
template<class F>
auto chain(F&& f)&&{
return [f=std::move<F>(f)](auto&&...args)mutable{
return std::move(f)(decltype(args)(args)...);
};
}
};
template<class F, class...Args, class R=std::decay_t<std::result_of_t<F(Args...)>>>
auto invoke_and_store(F&& f, Args&&...args) {
return invoke_and_store_t<R>{}(std::forward<F>(f), std::forward<Arg>(args)...);
}
现在您的代码变为:
template <class R, class Fn, class... Args>
R time_it(tag<R>, Fn&& fn, Args&&... args)
{
const auto start = std::chrono::system_clock::now();
auto&& res = invoke_and_store(
std::forward<Fn>(fn), std::forward<Args>(args)...
);
const auto end = std::chrono::system_clock::now();
std::cout << "elapsed " << (end - start).count() << std::endl;
return std::move(res).get();
}
这两个案例现在具有相同的正文。我解决了将 return 值存储(或不存储)到助手中的问题,从而使想要处理它的代码不必担心它。
我还包括了 chain
,它接受一个函数对象并将前一个 return 值作为第一个参数传递给它,或者不传递,这取决于前一个 return 值无效。我发现这种模式在 monad/functor-like 代码中很常见。
template<class A, class B>
auto then( A&& a, B&& b ) {
return [a = std::forward<A>(a), B=std::forward<B>(b)](auto&&...args)mutable{
return
invoke_and_store(std::move(a))
.chain(std::move(b))(decltype(args)(args)...);
};
}
then(a,b)(...)
调用 a()
然后 b(a(),...)
或 a()
然后 b(...)
取决于 a()
returns.