【问题标题】:Generic `erase_if` method for container types容器类型的通用 `erase_if` 方法
【发布时间】:2017-09-11 15:37:45
【问题描述】:

我正在尝试编写一个通用的erase_if 方法,该方法可用于任何容器类型以删除给定谓词的元素。如果容器允许,它应该使用erase-remove-idiom,或者循环通过容器并调用erase 方法。我也只想提供容器本身,而不是单独提供 beginend 迭代器。由方法处理。

但是,我无法让元模板通过 SFINAE 区分这两种情况。我正在尝试检查方法std::remove_if(或std::remove)是否可以为给定类型很好地定义,但vectormap 的值都是true(在这种情况下代码不会编译) 或false 为他们两个。我对模板元编程很陌生,所以我缺少什么吗?或者也许还有其他更好的解决方案?

下面是我的示例代码:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <map>
#include <type_traits>
#include <vector>


namespace my_std
{
    using std::begin;
    using std::end;

    namespace detail
    {
        template <typename T>
        // this is false for both vector and map
        auto is_remove_compatible(int) -> decltype(std::remove_if(begin(std::declval<T&>()), end(std::declval<T&>()),
                                                                  [](auto&&) { return false; }),
                                                   std::true_type{});

        // this is true for both vector and map
        // auto is_remove_compatible(int)
        //  -> decltype(std::remove(begin(std::declval<T&>()), end(std::declval<T&>()), std::declval<T::value_type&>()),
        //              std::true_type{});

        template <typename T>
        auto is_remove_compatible(...) -> std::false_type;
    }

    template <typename T>
    using is_remove_compatible = decltype(detail::is_remove_compatible<T>(0));

    template <typename T>
    constexpr bool is_remove_compatible_v = is_remove_compatible<T>::value;

    template <typename Cont, typename Pred>
    std::enable_if_t<is_remove_compatible_v<Cont>> erase_if(Cont& container, Pred pred)
    {
        std::cout << "Using erase-remove\n";
        container.erase(std::remove_if(begin(container), end(container), pred), end(container));
    }

    template <typename Cont, typename Pred>
    std::enable_if_t<!is_remove_compatible_v<Cont>> erase_if(Cont& container, Pred pred)
    {
        std::cout << "Using loop\n";
        for (auto it = begin(container); it != end(container);)
        {
            if (pred(*it))
                it = container.erase(it);
            else
                ++it;
        }
    }
}

template <typename T>
std::ostream& operator<<(std::ostream& out, std::vector<T> const& v)
{
    if (!v.empty())
    {
        out << '[';
        std::copy(v.begin(), v.end(), std::ostream_iterator<T>(out, ", "));
        out << "\b\b]";
    }
    return out;
}

template <typename K, typename V>
std::ostream& operator<<(std::ostream& out, std::map<K, V> const& v)
{
    out << '[';
    for (auto const& p : v)
        out << '{' << p.first << ", " << p.second << "}, ";
    out << "\b\b]";
    return out;
}

int main(int argc, int argv[])
{
    auto vp = my_std::is_remove_compatible_v<std::vector<int>>;
    auto mp = my_std::is_remove_compatible_v<std::map<int, int>>;

    std::cout << vp << ' ' << mp << '\n';

    std::vector<int> v = {1, 2, 3, 4, 5};
    auto v2 = v;
    my_std::erase_if(v2, [](auto&& x) { return x % 2 == 0; });

    std::map<int, int> m{{1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}};
    auto m2 = m;
    my_std::erase_if(m2, [](auto&& x) { return x.first % 2 == 0; });

    std::cout << v << " || " << v2 << '\n';
    std::cout << m << " || " << m2 << '\n';

    std::cin.ignore();
}

【问题讨论】:

  • [Off Topic] 如果您不希望它与所有标准容器一起使用,那么您需要为 std::forward_list 设置一个特殊的重载,因为它没有 erase。相反,它有erase_after
  • 使用的方法对 SFINAE 不友好 :-(,但会引发硬错误。
  • 更容易(部分)将is_remove_compatible&lt;container&lt;T&gt;&gt; 直接用于不同的支持容器。
  • 你用的是什么编译器? gcc & clang 无法编译您的示例,因为在 decltype 表达式和 int argv[] 中使用了 lambda。无论如何,std 库中没有太多容器,因此您可以为每个容器创建一个 erase_if 重载。
  • @Praetorian 我正在使用 VC2015,它编译得很好。我不会为每种容器类型创建重载,因为我还希望它适用于不属于标准库的自定义容器。

标签: c++ c++14 template-meta-programming


【解决方案1】:

这个想法是错误的有两个原因:

auto is_remove_compatible(int) -> decltype(std::remove_if(begin(std::declval<T&>()), end(std::declval<T&>()),
                                                          [](auto&&) { return false; }),
                                           std::true_type{});

首先,您不能在未评估的上下文中使用 lambda,因此代码格式不正确。其次,即使它不是格式错误的(这很容易修复),remove_if() 在任何事情上都不是 SFINAE 友好的。如果您传入的不是remove_if 的谓词或不可修改的迭代器,则标准并不要求这是硬错误。

我现在的做法是通过选择器习语。基本上,有基于等级排序的条件 sfinae:

template <std::size_t I> struct chooser : chooser<I-1> { };
template <> struct chooser<0> { };

template <class Cont, class Predicate>
void erase_if(Cont& container, Predicate&& predicate) {
    return erase_if_impl(container, std::forward<Predicate>(predicate), chooser<10>{});
}

然后我们有我们的各种erase_if_impl 条件,按顺序:

// for std::list/std::forward_list
template <class Cont, class Predicate>
auto erase_if_impl(Cont& c, Predicate&& predicate, chooser<3> )
    -> decltype(void(c.remove_if(std::forward<Predicate>(predicate))))
{
    c.remove_if(std::forward<Predicate>(predicate));
}

// for the associative containers (set, map, ... )
template <class Cont, class Predicate>
auto erase_if_impl(Cont& c, Predicate&& predicate, chooser<2> )
    -> decltype(void(c.find(std::declval<typename Cont::key_type const&>())))
{
    using std::begin; using std::end;
    for (auto it = begin(c); it != end(c); )
    {
        if (predicate(*it)) {
            it = c.erase(it);
        } else {
            ++it;
        }
    }
}

// for everything else, there's MasterCard
template <class Cont, class Predicate>
void erase_if_impl(Cont& c, Predicate&& predicate, chooser<1> )
{
    using std::begin; using std::end;
    c.erase(std::remove_if(begin(c), end(c), std::forward<Predicate>(predicate)), end(c));
}

【讨论】:

  • 感谢您的回答。我不太明白decltype(void(c.remove_if(std::forward&lt;Predicate&gt;(predicate))) 的语法。在这种情况下,void(...) 到底是什么意思?
  • @SebastianStern 他将返回类型转换为void,否则erase_if_impl 的返回类型将与c.remove_if 的返回类型相同
猜你喜欢
  • 1970-01-01
  • 2011-11-13
  • 1970-01-01
  • 1970-01-01
  • 2019-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多