14.8.3. Вызываемые объекты и тип function
В языке С++ есть несколько видов вызываемых объектов: функции и указатели на функции, лямбда-выражения (см. раздел 10.3.2), объекты, созданные функцией bind() (см. раздел 10.3.4), и классы с перегруженным оператором вызова функции.
Подобно любому другому объекту, у вызываемого объекта есть тип. Например, у каждого лямбда-выражения есть собственный уникальный (безымянный) тип класса. Типы функций и указателей на функции зависят от типа возвращаемого значения, типа аргумента и т.д.
Однако два вызываемых объекта с разными типами могут иметь ту же сигнатуру вызова (call signature). Сигнатура вызова определяет тип возвращаемого значения вызываемого объекта и тип (типы) аргумента, которые следует передать при вызове. Сигнатура вызова соответствует типу функции. Например:
int(int, int)
Функция этого типа получает два числа типа int и возвращает значение типа int.
Разные типы могут иметь одинаковую сигнатуру вызова
Иногда необходимо использовать несколько вызываемых объектов с одинаковой сигнатурой вызова, как будто это тот же тип. Рассмотрим, например, следующие разные типы вызываемых объектов:
// обычная функция
int add(int i, int j) { return i + j; }
// лямбда-выражение, создающее безымянный класс объекта функции
auto mod = [](int i, int j) { return i % j; };
// класс объекта функции
struct div {
int operator()(int denominator, int divisor) {
return denominator / divisor;
}
};
Каждый из этих вызываемых объектов применяет арифметическую операцию к своим параметрам. Даже при том, что у каждого из них разный тип, сигнатура вызова у них одинакова:
int(int, int)
Эти вызываемые объекты можно использовать для написания простого калькулятора. Для этого следует определить таблицу функций (function table), хранящую "указатели" на вызываемые объекты. Когда программе понадобится выполнить некую операцию, она просмотрит таблицу и найдет соответствующую функцию.
В языке С++ таблицы функций довольно просто реализовать при помощи карт (map). В данном случае как ключ используем строку, соответствующую символу оператора; значение будет функцией, реализующей этот оператор. При необходимости выполнить заданный оператор индексируется карта и осуществляется вызов возвращенного элемента. Если бы все эти функции были автономными и необходимо было использовать только парные операторы для типа int, то карту можно было бы определить так:
// сопоставляет оператор с указателем на функцию, получающую два целых
// числа и возвращающую целое число
map<string, int(*)(int, int)> binops;
Указатель add можно поместить в карту binops следующим образом:
// ok: add - указатель на функцию соответствующего типа
binops.insert({"+", add}); // {"+", add} - пара раздел 11.2.3
Но сохранить в карте binops объекты mod или div не получится:
binops.insert({"%", mod}); // ошибка: mod - не указатель на функцию
Проблема в том, что mod — это лямбда-выражение, и у каждого лямбда-выражения есть собственный тип класса. Этот тип не соответствует типу значений, хранимых в карте binops.
Библиотечный тип function
Эту проблему можно решить при помощи нового библиотечного типа function, определенного в заголовке functional; возможные операции с типом function приведены в табл. 14.3.
Таблица 14.3. Операции с типом function
function<T> f; f — пустой объект класса function, способный хранить вызываемые объекты с сигнатурой вызова, эквивалентной типу функции T (т.е. Т — это retType(args)) function<T> f(nullptr); Явное создание пустого объекта класса function function<T> f(obj); Сохранение копии вызываемого объекта obj в объекте f f Когда f используется как условие; оно истинно, если содержит вызываемый объект, и ложно в противном случае f(args) Вызывает объект f с передачей аргументов args Типы, определенные как члены шаблона function<T> result_type Тип возвращаемого значения объекта функции этого типа argument_type first_argument_type second_argument_type Типы, определяемые, когда у типа T есть один или два аргумента. Если у типа T есть один аргумент, то argument_type — синоним его типа. Если у типа T два аргумента, то first_argument_type и second_argument_type — синонимы их типовТип function — это шаблон. Подобно другим шаблонам, при создании его экземпляра следует указать дополнительную информацию. В данном случае этой информацией является сигнатура вызова объекта, который сможет представлять данный конкретный тип function. Как и у других шаблонов, этот тип определяют в угловых скобках:
function<int(int, int)>
Здесь был объявлен тип function, способный представлять вызываемые объекты, возвращающие целочисленный результат и имеющие два параметра типа int. Этот тип можно использовать для представления любого из типов приложения калькулятора:
function<int(int, int)> f1 = add; // указатель на функцию
function<int(int, int)> f2 = div(); // объект класса объекта функции
function<int(int, int)> f3 = [](int i, int j) // лямбда-выражение
{ return i * j; };
cout << f1(4,2) << endl; // выводит 6
cout << f2(4,2) << endl; // выводит 2
cout << f3(4,2) << endl; // выводит 8
Теперь карту можно переопределить, используя тип function:
// таблица вызываемых объектов,
// соответствующих всем бинарным операторам
// все вызываемые объекты должны получать по два int и возвращать int
// элемент может быть указателем на функцию, объектом функции или
// лямбда-выражением
map<string, function<int(int, int)>> binops;
В эту карту можно добавить каждый из вызываемых объектов приложения, будь то указатель на функцию, лямбда-выражение или объект функции:
map<string, function<int(int, int)>> binops = {
{"+", add}, // указатель на функцию
{"-", std::minus<int>()}, // объект библиотечной функции
{"/", div()}, // пользовательский объект функции
{"*", [](int i, int j) { return i * j; }}, // безымянное
// лямбда-выражение
{"%", mod} }; // именованный объект лямбда-выражения
В карте пять элементов. Хотя все лежащие в основе вызываемые объекты имеют различные типы, каждый из них можно хранить в общем типе function<int(int, int)>.
Как обычно, при индексировании карты возвращается ссылка на ассоциированное значение. При индексировании карты binops возвращается ссылка на объект типа function. Тип function перегружает оператор вызова. Этот оператор вызова получает собственные аргументы и передает их хранимому вызываемому объекту:
binops["+"](10, 5); // вызов add(10, 5)
binops["-"](10, 5); // использует оператор; вызов объекта minus<int>
binops["/"](10, 5); // использует оператор; вызов объекта div
binops["*"](10, 5); // вызов объекта лямбда-функции
binops["%"](10, 5); // вызов объекта лямбда-функции
Здесь происходит вызов каждой из операций, хранимых в карте binops. В первом вызове возвращаемый элемент является указателем на функцию, указывающим на функцию add(). Вызов binops["+"](10, 5) использует этот указатель для вызова функции add с передачей ей значений 10 и 5. Следующий вызов, binops["-"], возвращает объект класса function, хранящий объект типа std::minus<int>. Затем можно вызвать оператор этого объекта и других.
Перегруженные функции и тип function
Нельзя непосредственно хранить имя перегруженной функции в объекте типа function:
int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); // ошибка: какой именно add?
Один из способов разрешения двусмысленности подразумевает хранение указателя на функцию (см. раздел 6.7) вместо имени функции:
int (*fp)(int, int) = add; // указатель на версию add,
// получающую два int
binops.insert({"+", fp}); // ok: fp указывает на правую версию add
В качестве альтернативы для устранения неоднозначности можно использовать лямбда-выражение:
// ok: использование лямбда-выражения
// для устранения неоднозначности при
// выборе используемой версии add
binops.insert({"+", [](int a, int b) {return add(a, b);} });
Вызов в теле лямбда-выражения передает два целых числа. Этому вызову может соответствовать только та версия функции add(), которая получает два целых числа, а следовательно, эта функция и применяется при выполнении лямбда-выражения.
Класс function в новой библиотеке никак не связан с классами unary_function и binary_function, которые были частью прежних версий библиотеки. Эти классы были заменены более общей функцией bind() (см. раздел 10.3.4).
Упражнения раздела 14.8.3
Упражнение 14.44. Напишите собственную версию простого калькулятора, способного выполнять бинарные операции.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК