Вступление
Вероятно, всякий сталкивался с ситуацией, когда нужно написать operator== или operator< для своей структуры. Раньше я делал это как-то так:
struct data
{
unsigned int a_ ;
int b_ ;
int c_ ;
int d_ ;
} ;
bool operator<(const data & a1, const data & a2)
{
// Сравнение по a_, b_ и d_
if (a1.a_ != a2.a_)
return a1.a_ < a2.a_ ;
if (a1.b_ != a2.b_)
return a1.b_ < a2.b_ ;
return a1.d_ < a2.b_ ;
}
Копипаст меня удручал, но придумать ничего путного я не мог.
Хороший способ
Недавно нашелся хороший способ это делать:
#include <boost/tuple/tuple_comparison.hpp>
bool s(foo const &l, foo const &r)
{
return boost::tie(l.a, l.b, l.c, l.d) < boost::tie(r.a, r.b, r.c, r.d);
}
(отсюда)
Этот способ почти всем хорош, на нем надо было и остановиться.
Безумный способ
В хорошем способе оставался один мелкий недочет — повторение списка полей. Чтобы избавиться от него, прошлось обратиться к Boost.Fusion.
Набор полей структуры можно представить в виде набора указателей на них. Для нашего случая это будет выглядеть так:
&data::a_, &data::b_, &data::d_
Составить из них массив не получается, потому что они имеют разные типы:
unsigned int data::*, int data::*, int data::*
В Boost.Fusion есть свой vector, который представляет из себя кортеж из полей разного типа, например:
vector<int, char, double> v(1, '1', 1.0) ;
Для наших указателей получится следующий кортеж:
vector<unsigned int data::*, int data::*, int data::*> v2(&data::a_, &data::b_, &data::d_) ;
Выглядит страшненько, но для создания временного кортежа можно воспользоваться функцией
fusion::make_vector
:make_vector(&data::a_, &data::b_, &data::d_) ;
Ура! Теперь оператор сравнения можно реализовать без повторений кода:
bool operator<(const data & a1, const data & a2)
{
return less_members(boost::fusion::make_vector(&data::a_, &data::b_, &data::d_), a1, a2) ;
}
Осталось только придумать, как получить из кортежа указателей на поля два кортежа значений полей объектов
a1
и a2
. В Boost.Fusion есть функция fusion::transform
, аналогичная std::transform
, — она применяет указанное преобразование к каждому элементу кортежа. Отличие состоит в том, что в fusion::transform
на выходе может получиться кортеж из совершенно других типов! Совпадать будет только число элементов.Итак, нам нужно из кортежа
(&data::a_, &data::b_, &data::d_)
получить кортеж (a1.a_, a1.b_, a1.d_)
, причем состоящий из константных ссылок. Получается, что преобразование должно принимать значение типа R T::*
и возвращать значение типа const R &
. Кроме того, оно должно «помнить», с каким объектом оно имеет дело, из которого оно достает эти значения.
template <class S>
struct member_getter
{
member_getter(const S & obj): obj_(obj) {}
template <class R>
const R & operator()(R S::* pmemb) const
{
return obj_.*pmemb ;
}
const S & obj_ ;
} ;
member_getter
представляет из себя функтор (объект с определенным оператором «круглые скобки»), который конструируется с указанием объекта, из которого предстоит извлекать значения по указателям на поля. Он работает следующим образом:
const data d = { 1U, 2, 3, 4 } ;
member_getter<data> getter(d) ;
const unsigned int & ra = getter(&data::a_) ;
const int & rb = getter(&data::b_) ;
Но как будет
fusion::transform
определять тип элементов выходного кортежа? Для этого существует механизм result_of
, который широко применяется в Boost. Для того, чтобы result_of<member_getter<S>(R T::*)>::type
было равно const R &
, нужно добавить в наш member_getter
определение метафункции result
, к которой будет обращаться стандартная реализация result_of
:
template <class S>
struct member_getter
{
member_getter(const S & obj): obj_(obj) {}
template <class T> struct result ;
template <class R>
struct result<member_getter(R S::*)>
{
typedef const R & type ;
} ;
template <class R>
struct result<member_getter(R S::* const &)>
{
typedef const R & type ;
} ;
template <class R>
const R & operator()(R S::* pmemb) const
{
return obj_.*pmemb ;
}
const S & obj_ ;
} ;
С таким определением
result
поддерживается только работа с указателями на член. При попытке применить member_getter
для преобразования элемента какого-то другого типа произойдет ошибка компиляции.В результате получается простейшая реализация
less_members
:
template <class T, class S>
bool less_members(const T & membs, const S & a1, const S & a2)
{
using boost::fusion::transform ;
return transform(membs, member_getter<S>(a1)) < transform(membs, member_getter<S>(a2)) ;
}
Для кортежей в Boost.Fusion уже определены основные операторы сравнения.
Заключение
В принципе, если нужно сравнивать большое количество структур по набору полей, вышенаписанное может пригодиться. Для меня это послужило просто упражнением и поводом разобраться с
result_of
и fusion::transform
. В обычной жизни скорее пригоден способ с boost::tie
, так как в нем меньше оверхеда и он проще.