【问题标题】:Using SFINAE with generic lambdas将 SFINAE 与通用 lambda 一起使用
【发布时间】:2015-07-06 19:17:17
【问题描述】:

通用 lambda 可以利用 “替换失败不是错误” 规则吗?示例

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 似乎是 work just fine 所写的?
  • @KerrekSB 好的,这太棒了,我应该改写问题以关注 SFINAE 位吗?
  • 我不知道你需要解决什么问题...
  • Timer t; return f(args...);Timer::~Timer 打印结果。
  • sfinae 仅在第一个错误时有更好的过载选择时才起作用。由于您不能重载 lambda,因此尚不清楚 sfinae 将实现什么。

标签: c++ lambda c++14 sfinae generic-lambda


【解决方案1】:

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 ) 生成的对象是 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 有 4 个原因。

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

第二,上面的技巧。

第三,柯里化一个函数对象,当它具有正确数量和类型的参数时,它会在其中进行评估。

第四,自动元组解包和类似的。如果我使用 continuation 传递样式,我可以询问传入的 continuation 是否会接受未打包的元组或未捆绑的未来等。

【讨论】:

  • 现在我记得发过关于creating overload sets for lambda functions 的帖子(同样收到差评)。对不起,我应该从一开始就更好地表达我的问题,这是一个很好的话题,很遗憾它会被忽略
【解决方案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(); 
    };

【讨论】:

    【解决方案3】:

    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 的返回类型如何,这都会起作用。

    如果您需要比这更复杂的东西,那么这是您应该prefer a functor over a lambda 的情况之一。


    1实际上,正如 Yakk 所指出的,SFINAE 仍然可以非常方便地检查您的函数是否是可调用周期,这不是您要解决的问题 - 所以在这种情况下,仍然不是很有帮助。

    【讨论】:

    • 可以针对启用 SFINAE 的 lambda 进行测试(我可以用一些东西调用它吗?),所以它非常有用。例如,我可以采用 SFINAE 增强型 lambda 并将其传递给 curry(F) 函数,并在我传递了足够的参数时让它调用。或者我可以收集一堆 lambda,将它们聚合到一个对象中,然后使用 SFINAE 来确定我应该将调用调度到哪一个(lambda 的重载集)。 SFINAE lamdas 具有早期故障,即使在重载解决方案之外也很有用。
    • ... 你怎么知道 OP 试图解决什么问题?你能理解标题之外的问题吗?这怎么可能?
    • @Yakk 我想他只是想计时任何func 并让gL 返回结果(或不返回)?
    猜你喜欢
    • 1970-01-01
    • 2016-07-08
    • 2015-11-10
    • 2015-12-08
    • 2018-01-23
    • 1970-01-01
    • 1970-01-01
    • 2021-04-13
    • 1970-01-01
    相关资源
    最近更新 更多