C++/STL中是否支持按属性对对象进行排序?

22

我想知道STL中是否支持这个功能:

假设我有一个这样的类:

class Person
{
public:
  int getAge() const;
  double getIncome() const;
  ..
  ..
};

以及一个向量:

vector<Person*> people;

我想对人的向量按年龄进行排序:我知道我可以这样做:

class AgeCmp
{
public:
   bool operator() ( const Person* p1, const Person* p2 ) const
   {
     return p1->getAge() < p2->getAge();
   }
};
sort( people.begin(), people.end(), AgeCmp() );

有没有更简洁的方法来做到这一点?只是想按照“属性”排序就需要定义整个类似乎过于冗长了。像这样的方式可能会更好吧?

sort( people.begin(), people.end(), cmpfn<Person,Person::getAge>() );

1
C++0x有lambda函数,TR1添加了<tr1/functional>头文件,使得这个过程不再冗长。 - Omnifarious
2
+1非常好的提问和回答。这应该是将来重复问题的链接帖子。 - John Dibling
@Omnifarious:你确定TR1/functional中有些东西可以在这个例子中帮助你做到C++03中已经能做到的吗? - Manuel
@Manuel,嗯,你可以手动完成。但是,如果我没记错的话,TR1/functional使得使用更广泛的东西作为函数对象更容易。 - Omnifarious
9个回答

20

通用适配器,基于成员属性进行比较。虽然第一次可能会更冗长,但它可以重复使用。

// Generic member less than
template <typename T, typename M, typename C>
struct member_lt_type 
{
   typedef M T::* member_ptr;
   member_lt_type( member_ptr p, C c ) : ptr(p), cmp(c) {}
   bool operator()( T const & lhs, T const & rhs ) const 
   {
      return cmp( lhs.*ptr, rhs.*ptr );
   }
   member_ptr ptr;
   C cmp;
};

// dereference adaptor
template <typename T, typename C>
struct dereferrer
{
   dereferrer( C cmp ) : cmp(cmp) {}
   bool operator()( T * lhs, T * rhs ) const {
      return cmp( *lhs, *rhs );
   }
   C cmp;
};

// syntactic sugar
template <typename T, typename M>
member_lt_type<T,M, std::less<M> > member_lt( M T::*ptr ) {
   return member_lt_type<T,M, std::less<M> >(ptr, std::less<M>() );
}

template <typename T, typename M, typename C>
member_lt_type<T,M,C> member_lt( M T::*ptr, C cmp ) {
   return member_lt_type<T,M,C>( ptr, cmp );
}

template <typename T, typename C>
dereferrer<T,C> deref( C cmp ) {
   return dereferrer<T,C>( cmp );
}

// usage:    
struct test { int x; }
int main() {
   std::vector<test> v;
   std::sort( v.begin(), v.end(), member_lt( &test::x ) );
   std::sort( v.begin(), v.end(), member_lt( &test::x, std::greater<int>() ) );

   std::vector<test*> vp;
   std::sort( v.begin(), v.end(), deref<test>( member_lt( &test::x ) ) );
}

12

你不需要创建一个类 - 只需编写一个函数:

#include <vector>
#include <algorithm>
using namespace std;

struct Person {
    int age;
    int getage() const {
        return age;
    }
};

bool cmp( const Person * a, const Person * b ) {
    return a->getage() < b->getage() ;
}

int main() {
    vector <Person*> v;
    sort( v.begin(), v.end(), cmp );
}

请注意,在许多(大多数?)情况下,使用函数进行排序最终会导致运行速度变慢。 - Jerry Coffin
4
比什么慢?没有排序? - leeeroy
3
比使用函数对象运行速度慢。 - Jerry Coffin
1
@Jeffry 我使用VC2008进行了许多测试,它像函数对象一样内联函数。当然,这不是一个规则,我同意内联函数对编译器来说是一项更困难的工作。 - Khaled Alshaya
@AraK:看看我的回答——那里的代码展示了我所说的内容。别误会:也许有编译器标志可以使两者匹配,但如果有的话,我不确定大多数人知道该使用哪些标志,或者大多数时候是否这样做。 - Jerry Coffin

5

这不算是一个完整的答案,而是对AraK对我的评论的回复,我评论了使用函数进行排序而不是functor可能会更慢。下面是一些(确实有点丑 - 太多CnP)测试代码,用于比较各种排序:qsort,vector与数组的std::sort,以及使用模板类、模板函数或普通函数进行比较的std::sort:

#include <vector>
#include <algorithm>
#include <stdlib.h>
#include <time.h>

int compare(void const *a, void const *b) {
    if (*(int *)a > *(int *)b)
        return -1;
    if (*(int *)a == *(int *)b)
        return 0;
    return 1;
}

const int size = 200000;

typedef unsigned long ul;

void report(char const *title, clock_t ticks) { 
    printf("%s took %f seconds\n", title, ticks/(double)CLOCKS_PER_SEC);
}

void wait() { 
    while (clock() == clock())
        ;
}

template <class T>
struct cmp1 { 
    bool operator()(T const &a, T const &b) { 
        return a < b;
    }
};

template <class T>
bool cmp2(T const &a, T const &b) { 
    return a<b;
}

bool cmp3(int a, int b) { 
    return a<b;
}

int main(void) {
    static int array1[size];
    static int array2[size];

    srand(time(NULL));

    for (int i=0; i<size; i++) 
        array1[i] = rand();

    const int iterations = 100;

    clock_t total = 0;

    for (int i=0; i<iterations; i++) { 
        memcpy(array2, array1, sizeof(array1));
        wait();
        clock_t start = clock();
        qsort(array2, size, sizeof(array2[0]), compare);
        total += clock()-start;
    }
    report("qsort", total);

    total = 0;
    for (int i=0; i<iterations; i++) {
        memcpy(array2, array1, sizeof(array1));
        wait();
        clock_t start = clock();
        std::sort(array2, array2+size);
        total += clock()- start;
    }
    report("std::sort (array)", total);

    total = 0;
    for (int i=0; i<iterations; i++) {
        memcpy(array2, array1, sizeof(array1));
        wait();
        clock_t start = clock();
        std::sort(array2, array2+size, cmp1<int>());
        total += clock()- start;
    }
    report("std::sort (template class comparator)", total);

    total = 0;
    for (int i=0; i<iterations; i++) {
        memcpy(array2, array1, sizeof(array1));
        wait();
        clock_t start = clock();
        std::sort(array2, array2+size, cmp2<int>);
        total += clock()- start;
    }
    report("std::sort (template func comparator)", total);

    total = 0;
    for (int i=0; i<iterations; i++) {
        memcpy(array2, array1, sizeof(array1));
        wait();
        clock_t start = clock();
        std::sort(array2, array2+size, cmp3);
        total += clock()- start;
    }
    report("std::sort (func comparator)", total);

    total = 0;
    for (int i=0; i<iterations; i++) {
        std::vector<int> array3(array1, array1+size);
        wait();
        clock_t start = clock();
        std::sort(array3.begin(), array3.end());
        total += clock()-start;
    }
    report("std::sort (vector)", total);

    return 0;
} 

使用cl /O2b2 /GL sortbench3.cpp在VC++ 9/VS 2008中编译,得到以下结果:

qsort took 3.393000 seconds
std::sort (array) took 1.724000 seconds
std::sort (template class comparator) took 1.725000 seconds
std::sort (template func comparator) took 2.725000 seconds
std::sort (func comparator) took 2.505000 seconds
std::sort (vector) took 1.721000 seconds

我相信这些可以清晰地分成三类:使用默认比较排序,使用模板类生成最快的代码。使用函数或模板函数显然更慢。令人惊讶的是,使用qsort是所有方法中最慢的,大约是其他方法的2倍。

cmp2和cmp3之间的差异似乎完全源于按引用传递还是按值传递 -- 如果将cmp2更改为通过值传递其参数,则其速度将与cmp3完全匹配(至少在我的测试中是如此)。区别在于,如果您知道类型将是int,则几乎肯定会传递该值,而对于通用T,通常会通过const引用传递(以防万一它是更昂贵的复制对象)。


@Jerry 我同意你的观点,使用函数对象是正确的选择。当然我更喜欢这个解决方案,但正如我所说,我的(糟糕)测试并不具有普适性。看到你的测试结果我给你点赞。顺便说一下,在我第一条评论中拼错了你的名字,非常抱歉 :) - Khaled Alshaya
@AraK:我并不是在强调函数子是“正确”的做法,而是存在一种权衡,因此函数子的“冗长”可能是值得的。关于拼写没问题——这似乎经常发生(虽然我的姓氏更常见——当我还是个孩子时,我送报纸,人们用支票支付时总是有创意地拼错它……) - Jerry Coffin
cmp3和之前的比较器还有一个区别。cmp3按值传递参数。您能否尝试编写一个通过引用传递int的函数cmp4? - Maciej Hehl
@Maciej H: 当然。普通函数采用引用取参数会稍微减慢速度,因此比采用引用取参数的函数模板略慢一些(差距不大,但是看起来非常可靠 -- 我得到的结果是普通函数用时约为2.8秒,而函数模板用时约为2.7秒,两者都采用引用取参数)。 - Jerry Coffin
谢谢。我必须承认,起初我没有意识到函数模板实际上就是我所要求的测试。现在我对它们的区别感到惊讶 :)。好吧,我学到了教训。 - Maciej Hehl

3
如果只有一件事情你要根据人分类(或者如果有一个合理的默认值,你大部分时间都会使用),就覆盖People类的operator<以按此属性排序。没有明确的比较器,STL排序功能(和任何隐式使用排序的东西,如集合和地图)将使用operator<
当您想按除了operator<之外的其他内容进行排序时,您所描述的方式是当前C++版本唯一的方法(尽管比较器可以是普通函数;它不必是函数对象)。 C++0x标准将通过允许lambda函数使其更少冗长。
如果您不想等待C++0x,另一种选择是使用boost::lambda

6
对一个包含 People * 指针的向量进行排序,永远不会使用 People::operator<() 函数。 - anon
2
如果你想要更加技术化,标准库中的比较并不直接使用 operator< -- 它们使用 std::less<T>,该函数默认使用 operator<。但是,你可以为某个类型专门定制 std::less<T> 并在其实现中使用任何你想要的内容。 - Jerry Coffin
我不认为它会那么可怕:[]( Person* l, Person* r ) => bool { return l->age() < r->age(); }。嗯...没关系... - David Rodríguez - dribeas
@dribeas - David Rodríguez:我同意你的看法,这看起来一点也不糟糕。如果编译器能够推断出l和r的类型以及返回类型就好了。 - Manuel
它无法:在将参数传递给std::sort()之前,它需要知道参数类型,但是它只有在std::sort()传递它们时才知道实际参数。 - MSalters
显示剩余2条评论

2

从C++20开始,您可以直接使用投影来对范围内的每个元素进行排序:

#include <algorithm>
#include <vector>

std::vector<Person> persons;
std::ranges::sort(persons, {}, &Person::getAge);

std::vector<Person*> personPtrs;
std::ranges::sort(personPtrs, {}, &Person::getAge);

谢谢,我一直在等待现代答案出现在旧答案之间 :D - Chroma Funk

1

这些答案都非常冗长,虽然我喜欢模板的想法!只需使用lambda函数,它可以使事情变得更加简单!

你可以直接使用以下内容:

sort( people.begin(), people.end(), []( Person a, Person b ){ return a.age < b.age; } );

1

我看到dribeas已经提出了这个想法,但既然我已经写了,那么这就是如何编写一个通用比较器来使用getter函数的方法。

#include <functional>

template <class Object, class ResultType>
class CompareAttributeT: public std::binary_function<const Object*, const Object*, bool>
{
    typedef ResultType (Object::*Getter)() const;
    Getter getter;
public:
    CompareAttributeT(Getter method): getter(method) {}
    bool operator()(const Object* lhv, const Object* rhv) const
    {
        return (lhv->*getter)() < (rhv->*getter)();
    }
};

template <class Object, class ResultType>
CompareAttributeT<Object, ResultType> CompareAttribute(ResultType (Object::*getter)() const)
{
    return CompareAttributeT<Object, ResultType>(getter);
}

使用方法:

std::sort(people.begin(), people.end(), CompareAttribute(&Person::getAge));

我认为对于非指针类型来重载operator()可能是一个好主意,但这样一来就不能通过继承binary_function来定义argument_types了。不过这也许并不是什么大损失,因为在需要使用它们的地方很难使用,例如,无论如何都不能否定比较函数。


由于标准库已经有了“mem_fn”和“mem_fun”,将其命名为“mem_fn_cmp”而不是“CompareAttribute”可能是一个好主意。 - Manuel
无论你喜欢什么(虽然这是一件好事,但我通常只会使用boost.bind或类似的东西)。 - UncleBens

0

我根据UncleBens和david-rodriguez-dribeas的想法尝试了一下。

在我的当前编译器g++ 3.2.3中似乎可以(原样)工作。如果在其他编译器上也能行,请告诉我。

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

using namespace std;

class Person
{
public:
    Person( int _age )
        :age(_age)
    {
    }
    int getAge() const { return age; }
private:
    int age;
};

template <typename T, typename ResType>
class get_lt_type
{
    ResType (T::*getter) () const;
public:
    get_lt_type(ResType (T::*method) () const ):getter(method) {}
    bool operator() ( const T* pT1, const T* pT2 ) const
    {
        return (pT1->*getter)() < (pT2->*getter)();
    }
};

template <typename T, typename ResType>
get_lt_type<T,ResType> get_lt( ResType (T::*getter) () const ) {
    return get_lt_type<T, ResType>( getter );
}

int main() {
    vector<Person*> people;
    people.push_back( new Person( 54 ) );
    people.push_back( new Person( 4 ) );
    people.push_back( new Person( 14 ) );

    sort( people.begin(), people.end(), get_lt( &Person::getAge) );

    for ( size_t i = 0; i < people.size(); ++i )
    {
        cout << people[i]->getAge() << endl;
    }
    // yes leaking Persons
    return 0;
}

0

你可以只有一个全局函数或静态函数。这些全局或静态函数中的每一个都与属性进行比较。不需要创建类。一种将参数保留用于比较的方法是使用boost bind,但bind仅适用于查找所有类或将所有类与某个绑定参数进行比较。跨多个元素存储数据是制作functor的唯一原因。

编辑:还可以查看boost lambda函数,但它们仅适用于简单函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接