Пользовательские критерии сортировки

 

Одним из распространенных контекстов применения пользовательских функциональных объектов являются алгоритмы сортировки, где при помощи предикатов определяется порядок элементов.

 

Пусть имеется класс, описывающий сведения о сотруднике организации, вектор указателей на такие объекты, а также функция для удобства вывода содержимого такого вектора на экран:

 

#include <string>

#include <vector>

#include <functional>

#include <algorithm>

#include <iostream>

 

// Класс, описывающий информаицю о сотруднике организации

classEmployee

{

public:

 

Employee ( conststd::string & _name, int_age, double_salary )

: m_name( _name ),

m_age( _age ),

m_salary( _salary )

{}

 

conststd::string & getName () const{ returnm_name; }

intgetAge () const{ returnm_age; }

doublegetSalary () const{ returnm_salary; }

 

private:

std::string m_name;

intm_age;

doublem_salary;

};

 

// Функция вывода на экран содержимого набора сотрудников

voidprintEmployees ( conststd::vector< Employee * > & _employees )

{

for( autoit = _employees.begin(); it != _employees.end(); ++it )

std::cout << ( *it )->getName()

<< " age=" << ( *it )->getAge()

<< " salary=" << ( *it )->getSalary()

<< std::endl;

}

 

intmain ()

{

std::vector< Employee * > employees;

employees.push_back( newEmployee( "Ivanov", 25, 1000.0 ) );

employees.push_back( newEmployee( "Ivanov", 45, 2000.0 ) );

employees.push_back( newEmployee( "Petrov", 50, 750.0 ) );

employees.push_back( newEmployee( "Sidorov", 34, 1200.0 ) );

 

// ...

}

 

В зависимости от потребностей может понадобиться сортировка данного набора сотрудников по различным критериям, например, по возрасту, зарплате, фамилии либо комбинации этих факторов. Стандартная библиотека предоставляет алгоритм сортировки std::sort, по умолчанию использующий встроенный или перегруженный пользователем оператор <. Дополнительно, можно указать еще один необязательный аргумент данному алгоритму - бинарный предикат - компаратор, уточняющий правило сортировки, точнее, правило сравнения ключей.

 

Например, требуется отсортировать и вывести список сотрудников по убыванию возраста. Этого можно добиться следующим образом:

 

std::sort(

employees.begin(), employees.end(),

std::bind(

std::greater< int>(),

std::bind( & Employee::getAge, std::placeholders::_1 ),

std::bind( & Employee::getAge, std::placeholders::_2 )

)

);

 

std::cout << "Sorted by age descenting: " << std::endl;

printEmployees( employees );

std::cout << std::endl;

 

Если требуется обратный порядок, достаточно заменить предикат std::greater на std::less:

 

std::sort(

employees.begin(), employees.end(),

std::bind(

std::less< int>(),

std::bind( & Employee::getAge, std::placeholders::_1 ),

std::bind( & Employee::getAge, std::placeholders::_2 )

)

);

 

std::cout << "Sorted by age ascenting: " << std::endl;

printEmployees( employees );

std::cout << std::endl;

 

В другом случае может потребоваться сортировка по убыванию заработной платы:

 

std::sort(

employees.begin(), employees.end(),

std::bind(

std::greater< double>(),

std::bind( & Employee::getSalary, std::placeholders::_1 ),

std::bind( & Employee::getSalary, std::placeholders::_2 )

)

);

 

std::cout << "Sorted by salary descenting: " << std::endl;

printEmployees( employees );

std::cout << std::endl;

 

Могут определяться и более сложные способы сортировки (с соответствующей “расплатой” с точки зрения сложности предиката), например - сортировка по фамилии, а в случае равных фамилий - по возрасту. Такой предикат должен вернуть true, если имя первого сотрудника раньше по алфавиту имени второго сотрудника, а в случае равенства этих имен - сравнивать возраст сотрудников. Предикаты такой сложности следует составлять крайне осторожно, поскольку ошибки повлекут за собой неправильную работу алгоритмов сортировки:

 

std::sort(

employees.begin(), employees.end(),

std::bind(

std::logical_or< bool>(),

std::bind< bool>(

std::less< std::string >(),

std::bind( & Employee::getName, std::placeholders::_1 ),

std::bind( & Employee::getName, std::placeholders::_2 )

),

std::bind< bool>(

std::logical_and< bool>(),

std::bind< bool>(

std::equal_to< std::string >(),

std::bind( & Employee::getName, std::placeholders::_1 ),

std::bind( & Employee::getName, std::placeholders::_2 )

),

std::bind< bool>(

std::less< int>(),

std::bind( & Employee::getAge, std::placeholders::_1 ),

std::bind( & Employee::getAge, std::placeholders::_2 )

)

)

)

);

 

std::cout << "Sorted by name ascenting, then by age ascenting: " << std::endl;

printEmployees( employees );

std::cout << std::endl;

 

Ниже приведен вывод всех перечисленных сортировок:

 

Если компараторы используются в коде более 1 раза, имеет смысл оформить их в виде функциональных объектов или даже обыкновенных функций, что немного улучшит читабельность. Но в случае однократного использования делать отдельное выносное объявление часто не хочется.

 

Лямбда-выражения

 

До С++’11 для определения поведения вызываемой сущности у программистов был выбор только между сложным нечитабельным предикатом, располагаемым по месту вызова алгоритма, и отдельным объявлением функции или функтора, располагаемыми отдельно от вызова алгоритма. В стандарте С++11 появился еще один специальный вариант, радикально изменивший язык в сторону улучшения. Было введены так называемые ЛЯМБДА-выражения (lambda expressions), значительно упрощающее использование обычных вычислительных операций в качестве функциональных объектов для стандартных и пользовательских алгоритмов.

 

Прежде чем объяснить их природу, сразу приведем мотивирующий пример их использования, заменяющий сложную цепочку функциональных объектов и связывателей, где данные преобразовывались по формуле:

 

 

Написанную там несуразную “гору” кода на основе связывателей можно заменить элегантным компактным лямбда-выражением:

 

std::transform(

vX.begin(), vX.end(), vY.begin(),

std::back_inserter( vR ),

[] ( doublex, doubley )

{

return3.0 * x / ( y + 2.0 );

}

);

 

Такая запись одновременно читабельна, намного более явно повторяя исходную математическую формулу, также такой код эффективен и основан на стандартных алгоритмах.

 

Лямбда-выражение похоже на небольшую функцию без имени. Здесь имеются формальные аргументы и тело, также как и у обычных функций. Также лямбда имеет возвращаемый тип, в данном случае это тип double. Тип можно не указывать явно, если тело лямбды состоит из одной инструкции return, компилятор выводит его автоматически. В случае необходимости возвращаемый тип можно указать явно после списка формальных аргументов:

 

std::transform(

vX.begin(), vX.end(), vY.begin(),

std::back_inserter( vR ),

[] ( doublex, doubley ) -> double

{

return3.0 * x / ( y + 2.0 );

}

);

 

Что же из себя представляют лямбда-выражения? Когда компилятор встречает такой блок, автоматически генерируется внутренняя структура-функтор с перегруженным оператором функционального вызова. Тело этого оператора и представляет собой тело лямбда-выражения. Аналогично, формальные аргументы лямбда-выражения переносятся в формальные аргументы перегружаемого оператора. В месте вызова создается временный объект - экземпляр сгенерированной структуры. Этот объект передается алгоритму и ничем не отличается с его точки зрения от других функторов или функций. Т.е., по сути, работает обычный функциональный объект, сгенерированный и состыкованный компилятором в результате обработки компактного объявления.

 

Компилятор генерирует приблизительно следующее:

 

structLambda_1

: publicstd::binary_function< double, double, double>

// ^ ^ ^

// арг.№1 арг.№2 результат

{