На мой взгляд, указатели на функции является как раз таким примером. Дело в том, что синтаксис объявления и использования указателей на функции не слишком очевиден, особенно для не очень опытных программистов, и, если сразу «сыпать» деталями и синтаксическими возможностями, то за деревьями не будет видно леса. В этом посте я хочу поделиться своим видением указателей на функции и привести простой пример, который, смею надеяться, порадует своей легкостью и убьет еще одного зайца — продемонстрирует страшного монстра под названием «полиморфный вызов».
Итак, пусть у нас есть массив строк (хотя никто не мешает ему быть и каким-нибудь другим массивом), который нужно сортировать. Именно сортировать, а не отсортировать. То есть планируем мы это делать регулярно и разными способами.
const int n = 15;
string cities[n] = {
"Красноярск", "Москва", "Новосибирск", "Лондон", "Токио",
"Пекин", "Волгоград", "Минск", "Владивосток", "Париж",
"Манчестер", "Вашингтон", "Якутск", "Екатеринбург", "Омск"
};
Пусть нам требуется иметь возможность сортировки по алфавиту (вверх/вниз) и по длине строки (вверх/вниз). В других случаях это может быть по росту, по весу, по населению, по зарплате. Конечно, мы могли бы реализовать какой-нибудь метод сортировки и оттиражировать его с изменением способа сравнения. Но, как мы понимаем, возникает дублирование кода со всеми вытекающими из него последствиями.
А как же иначе быть, если мы не можем передать функцию в качестве аргумента в функцию сортировки? Или, все-таки, можем? Здесь и приходят на помощь указатели на функции. Это как раз то средство, которое позволяет работать с функциями так же, как и с переменными. Ну, или почти так же.
Итак, вот он указатель на функцию. Встречайте:
typedef bool(*CompareFunction) (int i, int j);
Не пытайтесь разобраться в синтаксисе. Просто знайте, что после этой команды мы можем использовать тип CompareFunction, чтобы объявлять переменные-функции, правда с фиксированной сигнатурой. В нашем случае это должны быть логические функции с двумя целочисленными аргументами. Как вы могли догадаться, это будут различные способы сравнения. Вот и они:
bool compareUp(int i, int j) {
return cities[i] < cities[j];
}
bool compareDown(int i, int j) {
return cities[i] > cities[j];
}
bool compareLengthUp(int i, int j) {
return cities[i].length() < cities[j].length();
}
bool compareLengthDown(int i, int j) {
return cities[i].length() > cities[j].length();
}
Теперь помещаем их в массив:
CompareFunction compares[] = {compareUp, compareDown, compareLengthUp, compareLengthDown};
А затем создаем функцию сортировки:
void sortCities(CompareFunction compare) {
for (int i = 0; i < n - 1; i++) {
int j_min = i;
for (int j = i+1; j < n; j++) {
if (<b>compare(j, j_min)</b>) {
j_min = j;
}
}
string temp = cities[i];
cities[i] = cities[j_min];
cities[j_min] = temp;
}
}
А что же там выделено жирным? Это и есть полиморфный вызов: переменной compare можно подсунуть любую функцию сравнения, и вызываемая функция будет определяться динамически, обеспечивая требуемый способ сортировки.
И, наконец, нужен какой-нибудь простенький user interface, чтобы все протестировать.
int _tmain(int argc, _TCHAR* argv[]) {
setlocale(LC_ALL, "Russian");
for (;;) {
int choice;
cout << "Ваш выбор: ";
cin >> choice;
sortCities(compares[choice]);
printCities();
}
system("pause");
return 0;
}
Теперь все прекрасно. Один программист штампует логические функции, второй — оптимизирует алгоритм сортировки, а третий — работает над юзабельностью пользовательского интерфейса. И никто никому не мешает — даже дублирование кода.
P.S. Функция printCities() виртуальная, ее реализация выпадает на долю читателя.
Комментарии (16)
4eyes
08.08.2017 15:11С указателями разобрались, с пузырьком тоже, но чем вам std::sort не угодил?
Кстати, лайфхак: если неохота читать и даже писать страшный тайпдефы:
template <typename BinaryFunction> void sort(BinaryFunction comparator) { ... }; // ... sort( [](int a, int b) { return true; /* ... */ } );
pestunov Автор
08.08.2017 16:42+3Мне кажется, что из текста достаточно ясно, что цель статьи — это не сделать дубликат std::sort, а проиллюстрировать концепцию. То же самое можно было бы продемонстрировать на многих других алгоритмах.
4eyes
08.08.2017 17:03Как по мне, эту концепцию отлично иллюстрируют custom deter-ы:
using AutoFile = std::unique_ptr<FILE, decltype(&fclose)>; const auto openFileInC = [](const std::string& name) { return AutoFile(fopen(name.c_str(), "w"), fclose); }; using AutoHandle = std::unique_ptr<void, decltype(&CloseHandle)>; const auto openFileInApi = [](const std::string& name) { return AutoHandle(::CreateFileA(name.c_str(), FILE_ALL_ACCESS, 0, nullptr, CREATE_ALWAYS, 0, nullptr), CloseHandle); }; int main() { // RAII wrapper will auto close descriptor auto fileInC = openFileInC("1.txt"); fprintf(fileInC.get(), "Hello"); // RAII wrapper will auto close handle auto fileInApi = openFileInApi("2.txt"); WriteFile(fileInApi.get(), "World", 5, nullptr, nullptr); return 0; }
leshabirukov
08.08.2017 15:15+1А это точно про полиморфизм? Вижу тут только функциональную переменную. Полиморфная функция должна сама переключаться на нужную реализацию в зависимости от параметра, а вы тут выбор руками делаете.
pestunov Автор
08.08.2017 15:19-3В контексте функции sortCities(CompareFunction compare) так и делается. Передается параметр и переключается на нужную реализацию. А ручной выбор в main() — это уже следующий слой.
leshabirukov
08.08.2017 18:04+2Нет, в общепринятом смысле полиморфное поведение управляется типом самих данных, а не дополнительным параметром.
pestunov Автор
08.08.2017 18:20Ну, это же не проблема. Давайте весь код (кроме main) завернем в какой-нибудь класс и будет то, о чем Вы говорите. Смысл-то абсолютно тот же, что в полиморфизме.
Идея полиморфизма же заключается не в том, чтобы тип чем-то управлял, а в том, что одна и та же строка кода работала по-разному в зависимости от того, что туда «подложат». Перегрузка операторов — это тоже вид полиморфизма.
Sklott
08.08.2017 16:27+2«В языках программирования и теории типов полиморфизмом называется способность функции обрабатывать данные разных типов»
В упор не вижу полиморфизма. Скорее тут можно говорить про паттерн «Стратегия».pestunov Автор
08.08.2017 16:53Конечно, это и есть паттерн «стратегия» (в «не-ООП» варианте), который и основан на полиморфном вызове функции (метода).
igormich88
08.08.2017 17:43А можно увидеть вариант с кастомной сортировкой произвольного массива, а то пример получается слишком далёким от реальности.
MadJackal
08.08.2017 17:51Может я несколько выпал из времени, но, например, qsort так работает испокон веков...
r3code
Очень часто материал подается с точки зрения «ух ты посмотри как тут много винтиков, гаек, планок и отвертка с ключом!», но не приводится примера задачи куда это применить можно. Многие думают, что полиморфизм это свойство исключительно ООП.