【问题标题】:Negate a lambda without knowing the argument type?在不知道参数类型的情况下否定 lambda?
【发布时间】:2015-06-02 15:06:29
【问题描述】:

我正在尝试编写一个与 Python 的过滤器类似的就地过滤器函数。例如:

std::vector<int> x = {1, 2, 3, 4, 5};
filter_ip(x, [](const int& i) { return i >= 3; });
// x is now {3, 4, 5}

首先我尝试了这个:

template <typename Container, typename Filter>
void filter_ip(Container& c, Filter&& f)
{
  c.erase(std::remove_if(c.begin(), c.end(), std::not1(f)), c.end());
}

但是,这不起作用,因为 lambdas don't have an argument_type field

以下变体does work

template <typename Container, typename Filter>
void filter_ip(Container& c, Filter&& f)
{
  c.erase(std::remove_if(c.begin(), c.end(), 
                         [&f](const typename Container::value_type& x) { 
                            return !f(x); 
                         }), 
          c.end());
}

但是,这似乎不太理想,因为以前,它只需要 Container 具有 beginenderase,而现在它还要求它定义一个 value_type。而且它看起来有点笨拙。

这是this answer 中的第二种方法。第一个将使用 std::not1(std::function&lt;bool(const typename Container::value_type&amp;)&gt;(f)) 而不是 lambda,它仍然需要类型。

我还尝试将 arg func 指定为具有已知参数类型的 std::function

template <typename Container, typename Arg>
void filter_ip(Container& c, std::function<bool(const Arg&)>&& f)
{
  c.erase(std::remove_if(c.begin(), c.end(), std::not1(f)), c.end());
}

然后我得到:

'main()::<lambda(const int&)>' is not derived from 'std::function<bool(const Arg&)>'

有没有办法解决这个问题?直觉上看起来应该很简单,因为您需要做的就是将 not 应用于您已经知道 f 返回的布尔值。

【问题讨论】:

标签: c++ templates c++11 types lambda


【解决方案1】:

如果您不能使用 C++14 通用 lambda,那么如何使用模板化的 operator() 委托给经典仿函数:

#include <utility>
#include <vector>
#include <algorithm>
#include <iostream>

template <class F>
struct negate {
    negate(F&& f)
    : _f(std::forward<F>(f)) {}

    template <class... Args>
    bool operator () (Args &&... args) {
        return !_f(std::forward<Args>(args)...);
    }

private:
    F _f;
};

template <typename Container, typename Filter>
void filter_ip(Container& c, Filter&& f)
{
    c.erase(std::remove_if(
        c.begin(),
        c.end(),
        negate<Filter>(std::forward<Filter>(f))),
        c.end()
    );
}

int main() {
    std::vector<int> v {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    filter_ip(v, [](int i) {return bool(i%2);});
    for(auto &&i : v)
        std::cout << i << ' ';
    std::cout << '\n';
}

输出:

1 3 5 7 9 

Live on Coliru

【讨论】:

  • 最好推断出operator() 的返回类型,以便通过SFINAE 检测可调用。此外,negate 可能不是最好的名称,因为 std::negate 使用一元 -(减号),而不是 !(逻辑非)。 -- 哦,而且习惯上加工厂函数模板进行类型推导。
【解决方案2】:
template<class F>
struct not_f_t {
  F f;
  template<class...Ts>
  decltype(!std::declval<typename std::result_of<F&(Ts...)>::type>())
  operator()(Ts&&...ts) {
    return !f(std::forward<Ts>(ts)...);
  }
};
template<class F, class dF=typename std::decay<F>::type>
not_f_t<dF> not_f(F&& f){
  return {std::forward<F>(f)};
}

或者在 C++14 中,我们可以省去 not_f_t 类并执行以下操作:

template<class F,class dF=std::decay_t<F>>// dF optional
auto not_f(F&& f){
  return [f=std::forward<F>(f)](auto&&...args)mutable
  ->decltype(!std::declval<std::result_of_t<dF&(decltype(args)...)>>()) // optional, adds sfinae
  {
    return !f(decltype(args)(args)...);
  };
}

然后,因为它摇摆不定:

template<class C, class F>
void erase_remove_if( C&& c, F&& f ) {
  using std::begin; using std::end;
  c.erase( std::remove_if( begin(c), end(c), std::forward<F>(f) ), end(c) );
}

我们得到:

std::vector<int> x = {1, 2, 3, 4, 5};
erase_remove_if(x, not_f([](int i){return i>=3;}));

【讨论】:

  • 多态 lambda 缺乏 not_f_t::operator() 的 SFINAE 友好性。 (你当然知道,但不是每个人都知道。)
  • @dyp nod:添加了 1 行并修改了 1 行以添加对 14 的 sfinae 支持。仍然更简洁。
  • o.O 如果我的眼睛没有欺骗我,那你就错过了not_f_t 中的!。此外,在返回类型的确定中包含! 可能更合适一些。想到的一个例子是一个返回指针(比如迭代器)的函数,它可以通过! 调整为谓词。 OTOH,限制 not_f 仅对谓词进行操作可能更合理。
  • 同意@dyp,但这肯定会让 sfinae 变得丑陋。让我想拥有struct not_t 以使其更简单。也许还有一个聪明的递归result_of
  • 确实,它现在比重复的decltype(!f(..)) 更长,如果它不比返回表达式的重复短,有什么理由更喜欢result_of_t
【解决方案3】:

在我看来,如果您已经需要 beginenderase,那么还需要 value_type 是一个非常小的补充。如果您可以摆脱对erase 的要求,那您至少会获得一些真正的容器,但消除对value_type 的要求并没有多大效果。

不过,如果您有一个容器确实定义了erase,但没有定义value_type,您可以通过从迭代器中获取 value_type 来回避直接定义 value_type 的要求:

template <typename Container, typename Filter>
void filter_ip(Container& c, Filter&& f) {
    using It = decltype(c.begin());

    c.erase(std::remove_if(c.begin(), c.end(),
        [&f](const std::iterator_traits<It>::value_type& x) {
        return !f(x);
    }),
        c.end());
}

使用iterator_traits&lt;T&gt;::value_type,您可以(例如)在迭代器实际上是指针时获取指针类型。我不知道在这种情况下有什么实际优势,尽管当您已经需要begin()end() 和(尤其是)erase 时。我们可以通过使用std::begin(c)std::end(c) 来消除对begin()end() 作为成员的要求,但是(再次)这并不能真正为我们带来任何有意义的东西(比如使用数组的能力),当我们仍然需要erase 成员。

更简单的方法是改用std::partition

template <typename Container, typename Filter>
void filter_ip(Container& c, Filter&& f) {
    c.erase(std::partition(c.begin(), c.end(), f), c.end());
}

这确实有一个缺点,它可以(将)重新排列它保留的元素,所以如果你真的需要保留原始顺序,它就行不通了。如果复制/移动构造比交换便宜得多(但这种情况相当少见),这也可能效率较低。

最后一种可能性是自己实现算法,而不是委托给另一个算法:

template <typename Container, typename Filter>
void filter2(Container& c, Filter&& f) {
    auto dst = c.begin();

    for (auto src = dst; src != c.end(); ++src)
        if (f(*src)) {
            *dst = *src;
            ++dst;
        }
    c.erase(dst, c.end());
}

如果您希望避免自我分配,您可以添加:

while (f(*dst))
    ++dst;

...在上面的for 循环之前。

【讨论】:

  • 非常明智,先生。在确定它是否适用于数组时,我进行了类似的推理 - 也就是说,数组没有 erase 方法。我可能只依赖value_type,但如果我不这样做,我最喜欢std::iterator_traits&lt;It&gt;::value_type 方法。
猜你喜欢
  • 2012-09-19
  • 1970-01-01
  • 2016-05-18
  • 1970-01-01
  • 2017-06-10
  • 2013-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多