【问题标题】:Shortcut for creating a comparator based on a member field or function基于成员字段或函数创建比较器的快捷方式
【发布时间】:2018-09-05 18:23:18
【问题描述】:

我经常发现自己想为 structclass 创建一个比较器对象,它只是提取类的一个成员,并对其进行通常的 < 比较。

例如:

struct student {
   int id;
   std::string name;
};

// sort by ID
std::sort(students.begin(), students.end(), [](const student& l, const student& r){ return l.id < r.id; });

那里有很多样板,特别是因为我们必须重复 lr 的声明。标准库中是否有一种方法可以创建基于“提取器”函数的比较器,该函数返回要比较的对象?

类似:

std::sort(students.begin(), students.end(), compare_on([](const student& s){ return s.id; });

我正在使用 C++11,但我也对后续标准中不适用于 C++11 的解决方案感兴趣(因此我可以在我的“升级原因”列表中添加一些内容)。

我在这里询问有关使用单个成员作为 comprand 的问题,以及默认比较“小于”,但对于易于组合的技术的奖励积分,例如允许您按字典顺序使用两个字段,或更改比较运算符。

【问题讨论】:

  • 我听说过这个案例的一个术语是projection(看起来像sort(students.begin(), students.end(), &amp;student::id))。标准库不支持
  • 使用 C++14,您可以通过将 [](const student&amp; l, const student&amp; r) 更改为 [](const auto&amp; l, const auto&amp; r)稍微减轻痛苦。
  • @JesperJuhl - 是的,好点,它减少了声明样板,但它并没有摆脱我看到的主要问题:你仍然需要声明左右,并实现自己比较逻辑。由于比较函数的参数通常是对称的,所以第一个似乎是多余的。当它只是l.f &lt; r.f 时,自己实现“逻辑”并不算太糟糕,但考虑一下如果你想在两个字段上进行比较,那么你就会写一些像l.f1 &lt; r.f1 || (l.f1 == r.f1 &amp;&amp; l.f2 &lt; r.f2) 这样的可憎的东西。
  • @BeeOnRope 我并没有试图解决整个问题。只是简单的一点;-)
  • @BeeOnRope: std::tie 可能对几个成员有帮助:return std::tie(l.f1, l.f2) &lt; std::tie(r.f1, r.f2);,您甚至可以编写 lambda/function as_tuple 以避免重复 .f1, .f2

标签: c++ c++11 comparison


【解决方案1】:

您实际上正在寻找的是允许将 投影 传递到算法中。 N4128 为标准库提出了这个建议,C++20 将在许多算法中提供这些。

但在那之前,我们可以自己做这件事。写一个sort的新重载:

struct identity {
    template <typename T>
    T&& operator()(T&& t) const noexcept { return std::forward<T>(t); }
};

// because no std::less<> in C++11 yet
struct less {
    template <typename T, typename U>
    constexpr bool operator()(T const& lhs, U const& rhs) const {
        return lhs < rhs;
    }
};

template <typename Range, typename Comp=less, typename Proj=identity>
void sort_proj(Range& range, Comp comp={}, Proj proj={}) {
    using std::begin;
    using std::end;
    auto first = begin(range), last = end(range);
    using reference = typename std::iterator_traits<decltype(first)>::reference;

    std::sort(first, last,
        [&](reference lhs, reference rhs) {
            return comp(std::ref(proj)(lhs), std::ref(proj)(rhs));
        });
}

std::ref(f)(x) 是在 C++11 中获得 INVOKE 功能的技巧。它基本上允许您将指向成员的指针作为投影传递。这个实现可以让你写:

sort_proj(students, less{}, &student::id);

请注意,投影与排序无关。所以我可以很容易地做各种事情:

sort_proj(students);                            // sort on students, if they're ordered
sort_proj(students, greater{}, &student::name); // decreasing, by name
sort_proj(students, less{},                     // by name, then id
    [](student const& s) {
        return std::tie(s.name, s.id);
    });

这种方法超级对于从一般算法中消除大量样板文件非常有用。我有一个标题,其中充满了许多常用标准算法的基于投影的重载。

【讨论】:

    【解决方案2】:

    您可以定义一个实用程序类

    template <class Fct> class compare_on {
      public:
        compare_on(Fct&& get) : get(std::forward<Fct>(get)) {}
    
        template <class T> bool operator()(const T& lhs, const T& rhs)
        {
           return get(lhs) < get(rhs);
        }
    
      private:
        Fct get;
    };
    

    然后将它传递给std::sort,就像你描述的那样(使用 C++17 类模板参数推导)

    std::sort(students.begin(), students.end(),
        compare_on([](const student& s){ return s.id; }));
    

    从 C++17 开始,@Justin 在 cmets 中指出实际比较可以改进(使用#include &lt;functional&gt;),这样

    return std::invoke(get, lhs) < std::invoke(get, rhs);
    

    允许使用数据成员引用进行实例化:

    std::sort(students.begin(), students.end(), compare_on(&student::id));
    

    当绑定到 C++11 时,忘记 std::invoke 并使用 compare_on 的显式实例化。后者不适用于 lambda,因此通常使用参数推导 make_* 助手:

    template <class Fct> auto make_compare_on(Fct&& get)
        -> decltype(compare_on<Fct>(std::forward<Fct>(get)))
    {
        return compare_on<Fct>(std::forward<Fct>(get));
    }
    

    请注意,您可以在 C++14 中删除尾随返回类型。

    最后一点,这里的命名应该改进:compare_on 具有误导性,因为它隐藏了函数对象的真正作用——通过operator &lt; 进行比较。也许compare_less_then 或类似的东西会更好,或者添加另一个可以指定为标准谓词之一的模板参数(std::less 等)。

    【讨论】:

    • 考虑使用std::invoke 调用get,这样可以实现:std::sort(students.begin(), students.end(), compare_on(&amp;student::id))。在 C++11 中,有alternatives
    • 谢谢。这似乎很容易扩展,例如,通过传递 N 而不是 1 提取器方法并在内部实现“字典”比较 get1(lhs) &lt; get1(rhs) || (get1(lhs) == get1(rhs) &amp;&amp; get2(rhs) &lt; get2(rhs) ...)。是的,我同意原则上比较运算符本身不应硬编码为&lt;,而应是模板参数,可能默认为std::less
    • @BeeOnRope 类本身不需要扩展来实现,而是传递一个返回元组的 lamdba:compare_on([](const student&amp; s){ return std::make_tuple(s.id, s.name); })
    【解决方案3】:

    根据您的建议,我提出这样的compare_on

    template<class T>
    auto compare_on(T &&t){
        return [t](const auto &l, const auto &r){
            return l.*t < r.*t; 
        };
    }
    ... 
    std::sort(students.begin(), students.end(), compare_on(&student::id));
    

    它需要 C++14。它使用指向成员的指针来准确获取您要求的行为。

    在 C++11 中,使用类似的想法,它看起来像这样:

    template<class U>
    class comparison{
    public:
        comparison(const U &ptr) : memberPtr(ptr){}
    
        template<class T>
        int operator()(const T &l, const T &r){
            return l.*memberPtr < r.*memberPtr;
        }
    
    private:
        U memberPtr;
    };
    
    template<class T>
    comparison<T> compare_on(T &&t){
        return comparison<T>(std::forward<T>(t));
    }
    

    正如@Barry 建议的那样,您应该将所有l.*t 替换为std::invoke(t, l) 以使其在C++ 17 中更通用。

    【讨论】:

    • 与贾斯汀对另一个答案的评论类似(事实上,出于相反的原因),使用 invoke() 而不是明确要求指向成员数据的指针(此解决方案甚至不允许指向成员函数的指针,更不用说任意操作了)。
    【解决方案4】:

    range-v3 实现投影

    // sort by ID
    ranges::sort(students, std::less<>{}, &student::id);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-04-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多