Читая учебники по программированию, убеждаюсь в том, как сильно усложняется восприятие материала, если автор подчиняет себя концепциям и инструментам, забывая о том, насколько важен контекст, в котором раскрывается материал. И, в то же время, насколько естественно и легко приходит понимание, когда ощущаешь реальное удобство от применения нового инструмента.

На мой взгляд, указатели на функции является как раз таким примером. Дело в том, что синтаксис объявления и использования указателей на функции не слишком очевиден, особенно для не очень опытных программистов, и, если сразу «сыпать» деталями и синтаксическими возможностями, то за деревьями не будет видно леса. В этом посте я хочу поделиться своим видением указателей на функции и привести простой пример, который, смею надеяться, порадует своей легкостью и убьет еще одного зайца — продемонстрирует страшного монстра под названием «полиморфный вызов».

Итак, пусть у нас есть массив строк (хотя никто не мешает ему быть и каким-нибудь другим массивом), который нужно сортировать. Именно сортировать, а не отсортировать. То есть планируем мы это делать регулярно и разными способами.

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)


  1. r3code
    08.08.2017 14:38
    +1

    Очень часто материал подается с точки зрения «ух ты посмотри как тут много винтиков, гаек, планок и отвертка с ключом!», но не приводится примера задачи куда это применить можно. Многие думают, что полиморфизм это свойство исключительно ООП.


  1. 4eyes
    08.08.2017 15:11

    С указателями разобрались, с пузырьком тоже, но чем вам std::sort не угодил?

    Кстати, лайфхак: если неохота читать и даже писать страшный тайпдефы:

    template <typename BinaryFunction>
    void sort(BinaryFunction comparator) { ... };
    // ...
    sort( [](int a, int b) { return true; /* ... */ } );
    


    1. pestunov Автор
      08.08.2017 16:42
      +3

      Мне кажется, что из текста достаточно ясно, что цель статьи — это не сделать дубликат std::sort, а проиллюстрировать концепцию. То же самое можно было бы продемонстрировать на многих других алгоритмах.


      1. 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;
        }
        


    1. boov
      08.08.2017 20:34

      К слову это не пузырек, а сортировка выбором.


  1. leshabirukov
    08.08.2017 15:15
    +1

    А это точно про полиморфизм? Вижу тут только функциональную переменную. Полиморфная функция должна сама переключаться на нужную реализацию в зависимости от параметра, а вы тут выбор руками делаете.


    1. pestunov Автор
      08.08.2017 15:19
      -3

      В контексте функции sortCities(CompareFunction compare) так и делается. Передается параметр и переключается на нужную реализацию. А ручной выбор в main() — это уже следующий слой.


      1. leshabirukov
        08.08.2017 18:04
        +2

        Нет, в общепринятом смысле полиморфное поведение управляется типом самих данных, а не дополнительным параметром.


        1. pestunov Автор
          08.08.2017 18:20

          Ну, это же не проблема. Давайте весь код (кроме main) завернем в какой-нибудь класс и будет то, о чем Вы говорите. Смысл-то абсолютно тот же, что в полиморфизме.
          Идея полиморфизма же заключается не в том, чтобы тип чем-то управлял, а в том, что одна и та же строка кода работала по-разному в зависимости от того, что туда «подложат». Перегрузка операторов — это тоже вид полиморфизма.


        1. playermet
          08.08.2017 18:35

          Я думаю автор имел в виду subtype polymorphism в вариации для функций первого класса. Вы же говорите про ad hoc polymorphism.


          1. pestunov Автор
            09.08.2017 05:07

            Действительно. Спасибо за замечание, я не придавал внимания этой классификации.


  1. Sklott
    08.08.2017 16:27
    +2

    «В языках программирования и теории типов полиморфизмом называется способность функции обрабатывать данные разных типов»
    В упор не вижу полиморфизма. Скорее тут можно говорить про паттерн «Стратегия».


    1. pestunov Автор
      08.08.2017 16:53

      Конечно, это и есть паттерн «стратегия» (в «не-ООП» варианте), который и основан на полиморфном вызове функции (метода).


  1. igormich88
    08.08.2017 17:43

    А можно увидеть вариант с кастомной сортировкой произвольного массива, а то пример получается слишком далёким от реальности.


  1. MadJackal
    08.08.2017 17:51

    Может я несколько выпал из времени, но, например, qsort так работает испокон веков...


  1. Zam_dev
    10.08.2017 10:19
    -1

    Для белого пояса самое то, спасибо!