使用带泛型的lambda表达式实现SFINAE

9

通用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在底层是模板化的函数对象,这样做不是有点奇怪吗?


你的lambda看起来写得很好,能正常工作? - Kerrek SB
6
我不知道你需要解决什么问题... - Kerrek SB
@Praetorian 我正好在写这个东西。修改了一下示例。问题是关于如何使用通用lambda表达式的SFINAE。它能做到吗?会做到吗?为什么不能,不应该吗?... - Nikos Athanasiou
1
定时器 t; 返回 f(args...); 和 Timer::~Timer 打印结果。 - Kerrek SB
1
sfinae只有在第一个重载错误时才能起作用,因为你无法重载lambda表达式,所以不清楚sfinae会实现什么。 - Daniel
显示剩余9条评论
3个回答

10

在幕后,Lambda是函数对象。通用的Lambda是带有模板operator()的函数对象。

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 ) 生成一个同时包含 f1f2 两个函数的重载对象。

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 有四个原因。

第一,使用新的 std::function 可以在多个不同的回调签名上重载函数。

第二,上述技巧。

第三,柯里化功能对象,当它具有正确数量和类型的参数时,它将被求值。

第四,自动元组解包等类似功能。如果我使用 continuation passing style,则可以询问传入的 continuation 是否接受元组解包或未捆绑的 future 等。


现在我记得发布有关为lambda函数创建重载集的帖子(同样受到了不好的反响)。对于造成的混淆,我很抱歉,我应该从一开始就更好地表达我的问题。这是一个很好的话题,可惜它会被忽视。 - Nikos Athanasiou

2
使用SFINAE的目的是在解决给定函数或模板时从候选集中删除重载或特化。在您的情况下,我们有一个 lambda 表达式 - 一个带有单个 operator() 的仿函数。没有重载,因此没有理由使用 SFINAE1。lambda 是通用的,这使得它的 operator() 成为一个函数模板,但这并不改变这个事实。
然而,您实际上不需要区分不同的返回类型。如果对于给定的参数,func 返回 void,你仍然可以将其 return。你只是不能将它赋值给一个临时变量。但是你也没有必要这样做:
auto time_func = [](auto&& func, auto&&... params) {
    RaiiTimer t;
    return std::forward<decltype(func)>(func)(
        std::forward<decltype(params)>(params)...); 
};

只需编写一个RaiiTimer,其构造函数启动计时器,析构函数停止它并打印结果。这将适用于任何func的返回类型。
如果你需要比这更复杂的东西,那么这是那些情况之一,你应该优先选择一个函数对象而不是一个lambda表达式
实际上,正如Yakk所指出的那样,SFINAE仍然可以非常有用,以检查您的函数是否可调用,这并不是您试图解决的问题,因此,在这种情况下,仍然没有太大帮助。

一个启用了SFINAE的lambda表达式可以进行测试(我能否调用它并传递一些内容),因此非常有用。例如,我可以取一个SFINAE增强的lambda,将其传递给curry(F)函数,并在传递足够的参数时调用它。或者我可以收集一堆lambda表达式,聚合它们到一个对象中,然后使用SFINAE来确定我应该将调用分派给哪一个(一组lambda表达式的重载集)。SFINAE lambda具有早期失败,即使在重载解析之外也很有用。 - Yakk - Adam Nevraumont
你怎么知道提问者想要解决什么问题?你能理解问题背后的意思吗?这有可能吗? - Yakk - Adam Nevraumont
@Yakk 我认为他只是想计时任何 func 并使 gL 返回结果(或不返回)? - Barry

2

通用lambda只能有一个主体,因此SFINAE在这里没有什么用。

一种解决方案是将调用封装到一个类中,该类可以存储结果并针对void返回类型进行特化,将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_taskfuture的开销,那么自己编写一个版本很容易:
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(); 
    };

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接