11.7 – Указатели на функции

Добавлено 15 июня 2021 в 00:25

В уроке «10.8 – Знакомство с указателями» вы узнали, что указатель – это переменная, которая содержит адрес другой переменной. Указатели на функции аналогичны, за исключением того, что они указывают не на переменные, а на функции!

Рассмотрим следующую функцию:

int foo()
{
    return 5;
}

Идентификатор foo – это имя функции. Но какого типа эта функция? Функции имеют свой собственный тип l-значения (l-value тип) – в данном случае тип функции, который возвращает целое число и не принимает параметров. Как и переменные, функции хранятся в памяти по назначенному адресу.

Когда функция вызывается (через оператор ()), выполнение переходит к адресу вызываемой функции:

int foo() // код для foo начинается с адреса памяти 0x002717f0
{
    return 5;
}
 
int main()
{
    foo(); // переходим к адресу 0x002717f0
 
    return 0;
}

В какой-то момент вашей карьеры программиста вы, вероятно, сделаете простую ошибку (если еще ее не сделали):

#include <iostream>
 
int foo() // код начинается с адреса памяти 0x002717f0
{
    return 5;
}
 
int main()
{
    std::cout << foo << '\n'; // мы хотели вызвать foo(), но вместо этого печатаем саму foo!
 
    return 0;
}

Вместо вызова функции foo() и печати возвращаемого значения мы непреднамеренно отправили функцию foo непосредственно в std::cout. Что происходит в этом случае?

На машине автора это напечатало:

0x002717f0

… но на вашем компьютере этот код может напечатать какое-то другое значение (например, 1), в зависимости от того, как ваш компилятор решит преобразовать указатель на функцию в другой тип для печати. Если ваш компьютер не печатает адрес функции, вы можете заставить его сделать это, преобразовав функцию в указатель void и распечатав его:

#include <iostream>
 
int foo() // код начинается с адреса памяти 0x002717f0
{
    return 5;
}
 
int main()
{
    // Сообщаем C++, интерпретировать функцию foo как указатель void
    std::cout << reinterpret_cast<void*>(foo) << '\n'; 
 
    return 0;
}

Так же, как можно объявить неконстантный указатель на обычную переменную, также можно объявить неконстантный указатель на функцию. В оставшейся части этого урока мы рассмотрим эти указатели на функции и их использование. Указатели на функции – довольно сложная тема, и тем, кто ищет только основы C++, оставшуюся часть этого урока можно смело пропустить или просмотреть по диагонали.

Указатели на функции

Синтаксис для создания неконстантного указателя на функцию – одна из самых уродливых вещей, которые вы когда-либо видели в C++:

// fcnPtr - указатель на функцию, которая не принимает аргументов и возвращает int
int (*fcnPtr)();

В приведенном выше фрагменте fcnPtr – это указатель на функцию, которая не имеет параметров и возвращает int. fcnPtr может указывать на любую функцию, соответствующую этому типу.

Скобки вокруг *fcnPtr необходимы из-за приоритета, поскольку int *fcnPtr() будет интерпретироваться как предварительное объявление для функции с именем fcnPtr, которая не принимает параметров и возвращает указатель на int.

Чтобы создать константный указатель на функцию, const идет после звездочки:

int (*const fcnPtr)();

Если вы поместите const перед int, это будет означать, что функция, на которую указывает указатель, возвращает const int.

Присваивание функции указателю на функцию

Указатели на функции могут быть инициализированы функцией (а неконстантным указателям на функции может быть присвоена функция). В приведенном выше примере мы использовали foo напрямую, и она была преобразована в указатель на функцию. Как и в случае с указателями на переменные, мы также можем использовать &foo для получения указателя на функцию foo. Для согласованности синтаксиса с указателями переменных и указателями на функции-члены, которые мы рассмотрим в следующем уроке, мы будем использовать синтаксис &foo, а не просто foo.

int foo()
{
    return 5;
}
 
int goo()
{
    return 6;
}
 
int main()
{
    int (*fcnPtr)(){ &foo }; // fcnPtr указывает на функцию foo
    fcnPtr = &goo;    // fcnPtr теперь указывает на функцию goo
 
    return 0;
}

Одна из распространенных ошибок – это сделать так:

fcnPtr = goo();

На самом деле это приведет к присвоению возвращаемого значения из вызова функции goo() переменной fcnPtr, а это не то, что мы хотим. Мы хотим, чтобы fcnPtr был присвоен адрес функции goo, а не возвращаемое значение из функции goo(). Поэтому скобки не нужны.

Обратите внимание, что тип (параметры и тип возвращаемого значения) указателя функции должен соответствовать типу функции. Вот несколько примеров этого:

// прототипы функций
int foo();
double goo();
int hoo(int x);
 
// присваивание указателям на функции
int (*fcnPtr1)(){ &foo };    // ok
int (*fcnPtr2)(){ &goo };    // неправильно - типы возвращаемых данных не совпадают
double (*fcnPtr4)(){ &goo }; // ok
fcnPtr1 = &hoo;              // неправильно - fcnPtr1 не имеет параметров, но hoo() имеет
int (*fcnPtr3)(int){ &hoo }; // ok

В отличие от базовых типов, C++ при необходимости неявно преобразует функцию в указатель на функцию (поэтому для получения адреса функции вам не нужно использовать оператор адреса (&)). Однако он не будет неявно преобразовывать указатели функций в обобщенные (void) указатели и наоборот.

Вызов функции с использованием указателя функции

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

int foo(int x)
{
    return x;
}
 
int main()
{
    int (*fcnPtr)(int){ &foo }; // инициализируем fcnPtr функцией foo
    (*fcnPtr)(5);               // вызов функции foo(5) через fcnPtr
 
    return 0;
}

Второй способ – неявное разыменование:

int foo(int x)
{
    return x;
}
 
int main()
{
    int (*fcnPtr)(int){ &foo }; // инициализируем fcnPtr функцией foo
    fcnPtr(5);                  // вызов функции foo(5) через fcnPtr
 
    return 0;
}

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

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

Передача функций в качестве аргументов другим функциям

Одна из наиболее полезных вещей, которую можно сделать с указателями на функции, – это передать функцию в качестве аргумента другой функции. Функции, используемые в качестве аргументов для другой функции, иногда называют функциями обратного вызова (callback function).

Рассмотрим случай, когда вы пишете функцию для выполнения задачи (например, сортировки массива), но хотите, чтобы пользователь мог определить, как будет выполняться конкретная часть этой задачи (например, будет ли массив отсортирован в порядке возрастания или убывания). Давайте в качестве примера, который можно обобщить на другие аналогичные задачи, подробнее рассмотрим эту тему применительно к сортировке.

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

Вот наша процедура сортировки выбором из предыдущего урока:

#include <utility> // для std::swap
 
void SelectionSort(int *array, int size)
{
    // Пройдемся по каждому элементу массива
    for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
    {
        // smallestIndex - это индекс самого маленького элемента,
        // с которым мы столкнулись до сих пор.
        int smallestIndex{ startIndex };
 
        // Ищем наименьший оставшийся элемент в массиве (начиная с startIndex+1)
        for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
        {
            // Если текущий элемент меньше нашего ранее найденного наименьшего
            if (array[smallestIndex] > array[currentIndex]) // СРАВНЕНИЕ ВЫПОЛНЯЕТСЯ ЗДЕСЬ
            {
                // Это новое наименьшее число для этой итерации
                smallestIndex = currentIndex;
            }
        }
 
        // Меняем местами наш начальный элемент с самым маленьким элементом
        std::swap(array[startIndex], array[smallestIndex]);
    }
}

Давайте заменим это сравнение функцией для сравнения. Поскольку наша функция сравнения будет сравнивать два целых числа int и возвращать логическое значение, чтобы указать, следует ли поменять местами элементы, это будет выглядеть примерно так:

bool ascending(int x, int y)
{
    return x > y; // поменять местами, если первый элемент больше второго
}

А вот наша процедура сортировки выбором с использованием для сравнения функции ascending():

#include <utility> // для std::swap
 
void SelectionSort(int *array, int size)
{
    // Пройдемся по каждому элементу массива
    for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
    {
        // smallestIndex - это индекс самого маленького элемента,
        // с которым мы столкнулись до сих пор.
        int smallestIndex{ startIndex };
 
        // Ищем наименьший оставшийся элемент в массиве (начиная с startIndex+1)
        for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
        {
            // Если текущий элемент меньше нашего ранее найденного наименьшего
            if (ascending(array[smallestIndex], array[currentIndex])) // СРАВНЕНИЕ ВЫПОЛНЯЕТСЯ ЗДЕСЬ
            {
                // Это новое наименьшее число для этой итерации
                smallestIndex = currentIndex;
            }
        }
 
        // Меняем местами наш начальный элемент с самым маленьким элементом
        std::swap(array[startIndex], array[smallestIndex]);
    }
}

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

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

bool (*comparisonFcn)(int, int);

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

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

#include <utility> // для std::swap
#include <iostream>
 
// Обратите внимание, что наше пользовательское сравнение является третьим параметром
void selectionSort(int *array, int size, bool (*comparisonFcn)(int, int))
{
    // Пройдемся по каждому элементу массива
    for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
    {
        // bestIndex - это индекс самого маленького / самого большого элемента,
        // с которым мы столкнулись до сих пор.
        int bestIndex{ startIndex };
 
        // Ищем самый маленький / самый большой элемент, оставшийся
        // в массиве (начиная с startIndex + 1))
        for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
        {
            // Если текущий элемент меньше/больше, чем наш ранее найденный
            if (comparisonFcn(array[bestIndex], array[currentIndex])) // СРАВНЕНИЕ ВЫПОЛНЯЕТСЯ ЗДЕСЬ
            {
                // Это новое наименьшее/наибольшее число для этой итерации
                bestIndex = currentIndex;
            }
        }
 
        // Меняем местами наш начальный элемент с
        // самым маленьким / самым большим элементом
        std::swap(array[startIndex], array[bestIndex]);
    }
}
 
// Вот функция сравнения, которая сортирует по возрастанию
// (Примечание: она точно такая же, как и предыдущая функция ascending())
bool ascending(int x, int y)
{
    return x > y; // поменять местами, если первый элемент больше второго
}
 
// Вот функция сравнения, которая сортирует в порядке убывания
bool descending(int x, int y)
{
    return x < y; // поменять местами, если второй элемент больше первого
}
 
// Эта функция распечатывает значения в массиве
void printArray(int *array, int size)
{
    for (int index{ 0 }; index < size; ++index)
    {
        std::cout << array[index] << ' ';
    }
    
    std::cout << '\n';
}
 
int main()
{
    int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };
 
    // Сортируем массив в порядке убывания с помощью функции descending()
    selectionSort(array, 9, descending);
    printArray(array, 9);
 
    // Сортируем массив в порядке возрастания с помощью функции ascending()
    selectionSort(array, 9, ascending);
    printArray(array, 9);
 
    return 0;
}

Эта программа дает следующий результат:

9 8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8 9

Круто ведь? Мы дали вызывающему возможность контролировать, как наша сортировка выбором выполняет свою работу.

Вызывающий может даже определить свои собственные «странные» функции сравнения:

bool evensFirst(int x, int y)
{
	// если x четное, а y нечетное, x идет первым (замена не требуется)
	if ((x % 2 == 0) && !(y % 2 == 0))
		return false;
 
	// если x нечетное, а y четное, y идет первым (требуется замена)
	if (!(x % 2 == 0) && (y % 2 == 0))
		return true;
 
    // в противном случае сортировать по возрастанию
	return ascending(x, y);
}
 
int main()
{
    int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };
 
    selectionSort(array, 9, evensFirst);
    printArray(array, 9);
 
    return 0;
}

Приведенный выше фрагмент дает следующий результат:

2 4 6 8 1 3 5 7 9

Как видите, использование указателя на функцию в этом контексте предоставляет прекрасный способ позволить вызывающему «привязать» свои собственные функции к чему-то, что вы ранее написали и протестировали, что помогает облегчить повторное использование кода! Раньше, если вы хотели отсортировать один массив в порядке убывания, а другой – в порядке возрастания, вам требовалось несколько версий процедуры сортировки. Теперь у вас может быть одна версия, которая может сортировать всё по желанию вызывающего!

Примечание. Если параметр функции относится к типу функции, он будет преобразован в указатель на тип функции. Это означает

void selectionSort(int *array, int size, bool (*comparisonFcn)(int, int))

можно эквивалентно записать как:

void selectionSort(int *array, int size, bool comparisonFcn(int, int))

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

Предоставление функций по умолчанию

Если вы собираетесь разрешить вызывающему передавать функцию в качестве параметра, часто бывает полезно предоставить вызывающему для его удобства какие-либо стандартные функции. Например, в приведенном выше примере сортировки выбором предоставление функций ascending() и descending() вместе с функцией selectionSort() упростило бы жизнь вызывающему, так как ему не пришлось бы переписывать ascending() или descending() каждый раз, когда он захочет их использовать.

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

// По умолчанию сортировка по возрастанию
void selectionSort(int *array, int size, bool (*comparisonFcn)(int, int) = ascending);

В этом случае, пока пользователь просто вызывает selectionSort (не через указатель на функцию), параметр compareFcn по умолчанию будет ascending.

Делаем указатели на функции красивее с помощью псевдонимов типов

Посмотрим правде в глаза – синтаксис указателей на функции уродлив. Однако можно использовать псевдонимы типов, чтобы указатели на функции больше походили на обычные переменные:

using ValidateFunction = bool(*)(int, int);

Это определяет псевдоним типа с именем "ValidateFunction", который является указателем на функцию, которая принимает два целых числа int и возвращает логическое значение.

Теперь вместо этого:

bool validate(int x, int y, bool (*fcnPtr)(int, int)); // уродливо

Вы можете сделать это:

bool validate(int x, int y, ValidateFunction pfcn) // чисто

Использование std::function

Альтернативный метод определения и хранения указателей на функции – использовать std::function, который является частью заголовка <functional> стандартной библиотеки. Чтобы определить указатель на функцию с помощью этого способа, объявите объект std::function следующим образом:

#include <functional>

// метод std::function, который возвращает логическое значение
// и принимает два параметра типа int
bool validate(int x, int y, std::function<bool(int, int)> fcn);

Как видите, и тип возвращаемого значения, и параметры заключены в угловые скобки, а параметры – внутри круглых скобок. Если параметров нет, круглые скобки можно оставить пустыми. Хотя это читается немного более подробно, оно также более явное, поскольку дает понять, какие ожидаются тип возвращаемого значения и параметры (тогда как способ с typedef скрывает их).

Обновление нашего предыдущего примера с помощью std::function:

#include <functional>
#include <iostream>
 
int foo()
{
    return 5;
}
 
int goo()
{
    return 6;
}
 
int main()
{
    // объявляем указатель на функцию, которая возвращает int
    // и не принимает параметров
    std::function<int()> fcnPtr{ &foo };
    fcnPtr = &goo;      // fcnPtr теперь указывает на функцию goo
    std::cout << fcnPtr() << '\n'; // вызываем функцию как обычно
 
    return 0;
}

Обратите внимание, что вы также можете ввести псевдоним std::function:

// псевдоним для чистого указателя на функцию
using ValidateFunctionRaw = bool(*)(int, int); 

// псевдоним для std::function
using ValidateFunction = std::function<bool(int, int)>;

Вывод типа для указателей на функции

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

#include <iostream>
 
int foo(int x)
{
	return x;
}
 
int main()
{
	auto fcnPtr{ &foo };
	std::cout << fcnPtr(5) << '\n';
 
	return 0;
}

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

Заключение

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

Небольшой тест

Вопрос 1

В этом тесте мы напишем версию нашего простейшего калькулятора, используя указатели на функции.

1a) Создайте короткую программу, предлагающую пользователю ввести два целых числа и математическую операцию ('+', '-', '*', '/'). Убедитесь, что пользователь вводит допустимую операцию.

#include <iostream>
 
int getInteger()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;
    return x;
}
 
char getOperation()
{
    char op{};
 
    do
    {   
        std::cout << "Enter an operation ('+', '-', '*', '/'): ";
        std::cin >> op;
    }
    while (op!='+' && op!='-' && op!='*' && op!='/');
 
    return op;
}
 
int main()
{
    int x{ getInteger() };
    char op{ getOperation() };
    int y{ getInteger() };
 
    return 0;
}

1b) Напишите функции с именами add(), subtract(), multiply() и division(). Они должны принимать два параметра типа int и возвращать значение int.

int add(int x, int y)
{
    return x + y;
}
 
int subtract(int x, int y)
{
    return x - y;
}
 
int multiply(int x, int y)
{
    return x * y;
}
 
int division(int x, int y)
{
    return x / y;
}

1c) Создайте псевдоним типа с именем ArithmeticFunction для указателя на функцию, которая принимает два параметра int и возвращает число int. Используйте std::function.

using ArithmeticFunction = std::function<int(int, int)>;

1d) Напишите функцию с именем getArithmeticFunction(), которая принимает символ оператора и возвращает соответствующую функцию как указатель на функцию.

ArithmeticFunction getArithmeticFunction(char op)
{
	switch (op)
	{
	default: // по умолчанию будет add
	case '+': return &add;
	case '-': return &subtract;
	case '*': return &multiply;
	case '/': return &division;
	}
}

1e) Измените функцию main(), чтобы она вызывала getArithmeticFunction(). Вызовите возвращаемое значение из этой функции с вашими входными данными и распечатайте результат.

#include <iostream>
 
int main()
{
    int x{ getInteger() };
    char op{ getOperation() };
    int y{ getInteger() };
 
    ArithmeticFunction fcn{ getArithmeticFunction(op) };
    std::cout << x << ' ' << op << ' ' << y << " = " << fcn(x, y) << '\n';
 
    return 0;
}

Вот полная программа:

#include <iostream>
#include <functional>
 
int getInteger()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;
    return x;
}
 
char getOperation()
{
    char op{};
 
    do
    {   
        std::cout << "Enter an operation ('+', '-', '*', '/'): ";
        std::cin >> op;
    }
    while (op!='+' && op!='-' && op!='*' && op!='/');
 
    return op;
}
 
int add(int x, int y)
{
    return x + y;
}
 
int subtract(int x, int y)
{
    return x - y;
}
 
int multiply(int x, int y)
{
    return x * y;
}
 
int division(int x, int y)
{
    return x / y;
}
 
using ArithmeticFunction = std::function<int(int, int)>;
 
ArithmeticFunction getArithmeticFunction(char op)
{
	switch (op)
	{
	default: // по умолчанию будет add
	case '+': return &add;
	case '-': return &subtract;
	case '*': return &multiply;
	case '/': return &division;
	}
}
 
int main()
{
    int x{ getInteger() };
    char op{ getOperation() };
    int y{ getInteger() };
 
    ArithmeticFunction fcn{ getArithmeticFunction(op) };
    std::cout << x << ' ' << op << ' ' << y << " = " << fcn(x, y) << '\n';
 
    return 0;
}

Теги

C++ / CppLearnCppstd::functionSTL / Standard Template Library / Стандартная библиотека шаблоновДля начинающихОбучениеПрограммированиеУказатель / Pointer (программирование)Указатель на функциюФункция обратного вызова / Callback function

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.