将 SFINAE 与通用 lambda 一起使用
Using SFINAE with generic lambdas
通用 lambda 可以利用 "Substitution Failure Is Not An Error" 规则吗?示例
auto gL =
[](auto&& func, auto&& param1, auto&&... params)
-> enable_if_t< is_integral<
std::decay_t<decltype(param1)>
>::value>
{
// ...
};
auto gL =
[](auto&& func, auto&& param1, auto&&... params)
-> enable_if_t< !is_integral<
std::decay_t<decltype(param1)>
>::value>
{
// ...
};
是否有任何解决方法或计划将其包含在语言中?此外,由于通用 lambda 是引擎盖下的模板化函数对象,所以不能这样做是不是有点奇怪?
SFINAE 的用途是在解析给定函数或模板时从候选集中删除重载或特化。在您的例子中,我们有一个 lambda - 这是一个带有单个 operator()
的仿函数。没有重载,所以没有理由使用 SFINAE1。 lambda 是通用的,这使得它的 operator()
成为一个函数模板,这一事实并没有改变这一事实。
但是,您实际上不需要区分不同的 return 类型。如果给定参数 func
returns void
,你仍然可以 return
它。你只是不能将它分配给一个临时的。但你也不必这样做:
auto time_func = [](auto&& func, auto&&... params) {
RaiiTimer t;
return std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...);
};
只要写一个RaiiTimer
,它的构造函数启动一个定时器,它的析构函数停止它并打印结果。无论 func
的 return 类型如何,这都会起作用。
如果您需要比这更复杂的东西,那么这是您应该 prefer a functor over a lambda.
的情况之一
1实际上,正如 Yakk 指出的那样,SFINAE 仍然可以很方便地检查您的函数是否是可调用周期,这不是您要解决的问题- 所以在这种情况下,仍然不是很有帮助。
一个通用的 lambda 只能有一个体,所以 SFINAE 在这里用处不大。
一个解决方案是将调用打包成一个 class ,它可以存储结果并专门针对 void
return 类型,封装 void
特殊处理远离你的 lambda。只需很少的开销,您就可以使用线程库工具来完成此操作:
auto gL =
[](auto&& func, auto&&... params)
{
// start a timer
using Ret = decltype(std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...));
std::packaged_task<Ret()> task{[&]{
return std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...); }};
auto fut = task.get_future();
task();
// stop timer and print elapsed time
return fut.get();
};
如果你想避免packaged_task
和future
的开销,写你自己的版本很容易:
template<class T>
struct Result
{
template<class F, class... A> Result(F&& f, A&&... args)
: t{std::forward<F>(f)(std::forward<A>(args)...)} {}
T t;
T&& get() { return std::move(t); }
};
template<>
struct Result<void>
{
template<class F, class... A> Result(F&& f, A&&... args)
{ std::forward<F>(f)(std::forward<A>(args)...); }
void get() {}
};
auto gL =
[](auto&& func, auto&&... params)
{
// start a timer
using Ret = decltype(std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...));
Result<Ret> k{std::forward<decltype(func)>(func),
std::forward<decltype(params)>(params)...};
// stop timer and print elapsed time
return k.get();
};
Lambda 是底层的函数对象。通用 lambda 是具有模板 operator()
s.
的函数对象
template<class...Fs>
struct funcs_t{};
template<class F0, class...Fs>
struct funcs_t<F0, Fs...>: F0, funcs_t<Fs...> {
funcs_t(F0 f0, Fs... fs):
F0(std::move(f0)),
funcs_t<Fs...>(std::move(fs)...)
{}
using F0::operator();
using funcs_t<Fs...>::operator();
};
template<class F>
struct funcs_t<F>:F {
funcs_t(F f):F(std::move(f)){};
using F::operator();
};
template<class...Fs>
funcs_t< std::decay_t<Fs>... > funcs(Fs&&...fs) {
return {std::forward<Fs>(fs)...};
}
auto f_all = funcs( f1, f2 )
生成一个重载 f1
和 f2
的对象。
auto g_integral =
[](auto&& func, auto&& param1, auto&&... params)
-> std::enable_if_t< std::is_integral<
std::decay_t<decltype(param1)>
>{}>
{
// ...
};
auto g_not_integral =
[](auto&& func, auto&& param1, auto&&... params)
-> std::enable_if_t< !std::is_integral<
std::decay_t<decltype(param1)>
>{}>
{
// ...
};
auto gL = funcs( g_not_integral, g_integral );
并调用 gL
将对两个 lambda 进行 SFINAE 友好的重载解析。
上面在funcs_t
的线性继承中做了一些可以避免的虚假动作。在工业质量库中,我可能会将继承设为二进制而不是线性(以限制模板的实例化深度和继承树的深度)。
顺便说一句,我知道 SFINAE 启用 lambda 的原因有 4 个。
首先,使用新的 std::function
,您可以在多个不同的回调签名上重载一个函数。
二、以上技巧
第三,柯里化一个函数对象,当它具有正确的参数数量和类型时进行计算。
第四,元组自动拆包之类的。如果我使用延续传递样式,我可以询问传入的延续是否会接受解包的元组或未来的未绑定等等。
通用 lambda 可以利用 "Substitution Failure Is Not An Error" 规则吗?示例
auto gL =
[](auto&& func, auto&& param1, auto&&... params)
-> enable_if_t< is_integral<
std::decay_t<decltype(param1)>
>::value>
{
// ...
};
auto gL =
[](auto&& func, auto&& param1, auto&&... params)
-> enable_if_t< !is_integral<
std::decay_t<decltype(param1)>
>::value>
{
// ...
};
是否有任何解决方法或计划将其包含在语言中?此外,由于通用 lambda 是引擎盖下的模板化函数对象,所以不能这样做是不是有点奇怪?
SFINAE 的用途是在解析给定函数或模板时从候选集中删除重载或特化。在您的例子中,我们有一个 lambda - 这是一个带有单个 operator()
的仿函数。没有重载,所以没有理由使用 SFINAE1。 lambda 是通用的,这使得它的 operator()
成为一个函数模板,这一事实并没有改变这一事实。
但是,您实际上不需要区分不同的 return 类型。如果给定参数 func
returns void
,你仍然可以 return
它。你只是不能将它分配给一个临时的。但你也不必这样做:
auto time_func = [](auto&& func, auto&&... params) {
RaiiTimer t;
return std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...);
};
只要写一个RaiiTimer
,它的构造函数启动一个定时器,它的析构函数停止它并打印结果。无论 func
的 return 类型如何,这都会起作用。
如果您需要比这更复杂的东西,那么这是您应该 prefer a functor over a lambda.
的情况之一1实际上,正如 Yakk 指出的那样,SFINAE 仍然可以很方便地检查您的函数是否是可调用周期,这不是您要解决的问题- 所以在这种情况下,仍然不是很有帮助。
一个通用的 lambda 只能有一个体,所以 SFINAE 在这里用处不大。
一个解决方案是将调用打包成一个 class ,它可以存储结果并专门针对 void
return 类型,封装 void
特殊处理远离你的 lambda。只需很少的开销,您就可以使用线程库工具来完成此操作:
auto gL =
[](auto&& func, auto&&... params)
{
// start a timer
using Ret = decltype(std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...));
std::packaged_task<Ret()> task{[&]{
return std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...); }};
auto fut = task.get_future();
task();
// stop timer and print elapsed time
return fut.get();
};
如果你想避免packaged_task
和future
的开销,写你自己的版本很容易:
template<class T>
struct Result
{
template<class F, class... A> Result(F&& f, A&&... args)
: t{std::forward<F>(f)(std::forward<A>(args)...)} {}
T t;
T&& get() { return std::move(t); }
};
template<>
struct Result<void>
{
template<class F, class... A> Result(F&& f, A&&... args)
{ std::forward<F>(f)(std::forward<A>(args)...); }
void get() {}
};
auto gL =
[](auto&& func, auto&&... params)
{
// start a timer
using Ret = decltype(std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...));
Result<Ret> k{std::forward<decltype(func)>(func),
std::forward<decltype(params)>(params)...};
// stop timer and print elapsed time
return k.get();
};
Lambda 是底层的函数对象。通用 lambda 是具有模板 operator()
s.
template<class...Fs>
struct funcs_t{};
template<class F0, class...Fs>
struct funcs_t<F0, Fs...>: F0, funcs_t<Fs...> {
funcs_t(F0 f0, Fs... fs):
F0(std::move(f0)),
funcs_t<Fs...>(std::move(fs)...)
{}
using F0::operator();
using funcs_t<Fs...>::operator();
};
template<class F>
struct funcs_t<F>:F {
funcs_t(F f):F(std::move(f)){};
using F::operator();
};
template<class...Fs>
funcs_t< std::decay_t<Fs>... > funcs(Fs&&...fs) {
return {std::forward<Fs>(fs)...};
}
auto f_all = funcs( f1, f2 )
生成一个重载 f1
和 f2
的对象。
auto g_integral =
[](auto&& func, auto&& param1, auto&&... params)
-> std::enable_if_t< std::is_integral<
std::decay_t<decltype(param1)>
>{}>
{
// ...
};
auto g_not_integral =
[](auto&& func, auto&& param1, auto&&... params)
-> std::enable_if_t< !std::is_integral<
std::decay_t<decltype(param1)>
>{}>
{
// ...
};
auto gL = funcs( g_not_integral, g_integral );
并调用 gL
将对两个 lambda 进行 SFINAE 友好的重载解析。
上面在funcs_t
的线性继承中做了一些可以避免的虚假动作。在工业质量库中,我可能会将继承设为二进制而不是线性(以限制模板的实例化深度和继承树的深度)。
顺便说一句,我知道 SFINAE 启用 lambda 的原因有 4 个。
首先,使用新的 std::function
,您可以在多个不同的回调签名上重载一个函数。
二、以上技巧
第三,柯里化一个函数对象,当它具有正确的参数数量和类型时进行计算。
第四,元组自动拆包之类的。如果我使用延续传递样式,我可以询问传入的延续是否会接受解包的元组或未来的未绑定等等。