Как стать автором
Обновить

[C++] Сравнение структур по набору полей

Время на прочтение3 мин
Количество просмотров7K

Вступление


Вероятно, всякий сталкивался с ситуацией, когда нужно написать 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, так как в нем меньше оверхеда и он проще.
Теги:
Хабы:
Всего голосов 29: ↑26 и ↓3+23
Комментарии36

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань