Пользовательские критерии сортировки
Одним из распространенных контекстов применения пользовательских функциональных объектов являются алгоритмы сортировки, где при помощи предикатов определяется порядок элементов.
Пусть имеется класс, описывающий сведения о сотруднике организации, вектор указателей на такие объекты, а также функция для удобства вывода содержимого такого вектора на экран:
#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 результат
{