基于成员字段或函数创建比较器的快捷方式

时间:2018-09-05 18:23:18

标签: c++ c++11 comparison

我经常发现自己想为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的解决方案感兴趣(因此,我可以在“升级理由”列表中添加一些内容)。

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

4 个答案:

答案 0 :(得分:7)

您真正要寻找的是允许将投影传递到算法中。 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);
    });

这种方法 super 在消除一般算法中的许多样板方面很有用。我有一个标头,上面充满了许多常用标准算法的基于投影的重载。

答案 1 :(得分:4)

您可以定义实用程序类

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在诠释中指出,可以改善实际比较(使用#include <functional>),这样

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 <进行比较。也许使用compare_less_then或类似的东西会更好,或者添加另一个可以指定为标准谓词之一的模板参数(std::less等)。

答案 2 :(得分: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中更通用。

答案 3 :(得分:3)

range-v3实现投影

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