C что из себя представляет std function
Перейти к содержимому

C что из себя представляет std function

  • автор:

C что из себя представляет std function

При использовании объектов-функций и лямбда-выражений следует помнить, что они не эквивалентны указателям на функции. Чтобы упростить совместное использование указателей на функции, объектов-функций и лямбда-выражений модуль определяет шаблон std::function<> . std::function представляет нечто, что может вызываться как функция — это может быть и указатель на функцию, и объект-функция, и лямбда-выражение. Рассмотрим, как мы можем его использовать:

#include #include // для std::function class Sum < public: int operator()(int a, int b)< return a + b;>>; int subtract(int a, int b) int main() < std::functionoperation; operation = Sum<>; // operation представляет объект-функцию Sum std::cout ; operation = multiply; // operation представляет лямбда-выражение multiply std::cout 

Для использования типа std::function прежде всего необходимо подключить заголовочный файл

#include

В функции main определяем переменную std::function :

std::function operation;

Переменная operation предсталяет некоторую функцию, которая имеет два параметра типа int и возвращает также значение типа int.

Мы можем присвоить этой переменной объект-функцию:

operation = Sum<>;
operation = subtract;
auto multiply = [](int a, int b); operation = multiply;

Для вызова все этих функций вызываем operation как функцию, передавая ее параметрам некоторые значения:

operation(10, 6)
#include #include // для std::function class Console < public: void operator()(const std::string& message) < std::cout >; void print(const std::string& message) < std::cout int main() < std::functionaction; action = Console<>; action("Word"); action = print; action("Work"); auto log = [](const std::string& mes); action = log; action("World"); >

Здесь переменная action представляет некоторую функцию, которая принимает строку и ничего не возвращает — std::function

Применения std::function

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

30 января 2019

| �� 12 мин

#Статья об использовании std::function

Поводом для написания данной статьи стало желание систематизировать всё то, чему я научился в процессе кодинга лаб по таким предметам, как Численные методы; Уравнения математической физики; Методы оптимизации. Надеюсь эта статья будет полезна не только людям, которые учатся в НГТУ на факультете ФПМИ, но и широкому кругу читателей.

#Введение

В Си/C++ имеется возможность передавать функцию в качестве аргумента функции как указатель на функцию. Вы наверняка знаете этот страшный синтаксис:

int add(int x, int y)  return x+y; > void call_operation(int (*operation)(int, int))  int c = operation(1, 2); > call_operation(add); 

Начиная с C++11 появился удобный интерфейс для описания функциональных объектов под названием std::function , он имеет более красивый синтаксис:

#include int add(int x, int y)  return x+y; > int call_operation(std::functionint(int, int)> operation)  return operation(1, 2); > int c = call_operation(add); // c = 3 

Причём, в отличие от Си, в C++ таким образом можно описывать любой функциональный объект, то есть объект, допускающий вызов операции () :

// . struct Adder   int operator()(int a, int b)  return state + a + b; > int state; >; Adder add = 1>; int d = call_operation(add); // d = 4 

Аналогичным образом можно использовать лямбды:

// . int state = 2; int e = call_operation([&](int a, int b) -> int   return state + a + b; >); // e = 5 

Таким образом, можно передавать функции в качестве аргументов функций.

#Численное вычисление производной

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

double f(double x); double derivative_f(double x)  double h = 0.0001; return (f(x+h)-f(x))/h; > 

Но писать для каждой функции другую функцию, вычисляющую её производную непрактично! Поэтому такую проблему можно решить с помощью std::function :

std::functiondouble(double)> derivative(std::functiondouble(double)> f)  return [f] (double x) -> double   double h = 0.0001; return (f(x+h)-f(x))/h; > > 

Данная функция получает функцию, и возвращает функцию, которая считает её производную. Пример использования:

double f(double x)  return x*x; > auto df = derivative(f); double a = f(1); // a == 1 double a = df(1); // a == 2 

А далее представлен код для вычисления первой и второй производных функций, взятый из курсовой по УМФ, смело используйте его в своих проектах.

typedef std::functiondouble(double)> function_1d_t; function_1d_t calc_first_derivative(const function_1d_t& f)  return [f](double x) -> double   const double h = 0.001; return (-f(x + 2 * h) + 8 * f(x + h) - 8 * f(x - h) + f(x - 2 * h)) / (12 * h); >; > function_1d_t calc_second_derivative(const function_1d_t& f)  return [f](double x) -> double   const double h = 0.001; return (-f(x+2*h) + 16*f(x+h) - 30*f(x) + 16*f(x-h) - f(x-2*h))/(12*h*h); >; > 

Может показаться, что если уменьшить величину 0.001 , то точность повысится, но это не так, на практике у меня точность только падала, а при этом значении уже очень хорошо расчитывается производная.

#Пример использования std::bind

Окей, у нас существует функция для расчета производной одномерной функции, но что если у нас функция принимает два аргумента, например f(x, y) . Как посчитать её производную по одному параметру?

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

double f(double x, double y)  return 2*x + 3*y; > // Производная f по x при y=0 auto fx = calc_first_derivative(std::bind(f, std::placeholders::_1, 0)); // Производная f по y при x=0 auto fy = calc_first_derivative(std::bind(f, 0, std::placeholders::_1)); double fx_value = fx(1); // fx_value = 2; double fy_value = fy(1); // fy_value = 3; 

Где std::placeholders::_1 показывает первый параметр внешней функции, которую возвратит std::bind .

К сожалению в этом случае функция fx получается функцией одной переменной, при фиксированном y=0 . Чтобы получить функцию fx , которая принимает два параметра, но при этом возвращает производную по x , можно воспользоваться следующим трюком:

auto fx = [] (double x, double y) -> double   return calc_first_derivative(std::bind(f, std::placeholders::_1, y))(x); >; double fx_value = fx(1, 0); // df(1, 0)/dx 

Наверное можно было бы его улучшить, чтобы не копипастить эти строки, и придумать новый std::bind2 , при помощи шаблонной магии C++, но я такое не умею. Если вы сможете это сделать, обязательно pull-request’ните сюда!

Или другой пример:

double f(double x, double y, double t); using namespace std::placeholders; std::functiondouble(double, double)> g = std::bind(f, _2, 5, _1); double gv = g(1, 2); double fv = f(2, 5, 1); // gv == fv 

#Автоматический расчет правой части

В таком предмете, как УМФ требуется решить следующее дифференциальное уравнение:

-div(lambda * grad u) + gamma * u + sigma * du/dt = f ,

где u = u(x, y, t) является неизвестной функцией. Для численного решения используется Метод Конечных Элементов.

Для декартовых координат это уравнение раскладывается в lambda*(d^2 u/dx^2 + d^2 u/dy^2) + gamma * u + sigma * du/dt = f

Для тестирования программы-решателя мы придумываем некоторую функцию u , например: u = x*x + y*y + t , придумываем значения констант lambda , gamma , sigma , и согласно верхнему уравнению можно вычислить функцию f , при которой дифференциальное уравнение превращается в тождество.

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

typedef std::functiondouble(double, double, double)> function_3d_t; /** Все константы решаемого уравнения. */ struct constants_t   double lambda; /// Коэффициент внутри div double gamma; /// Коэффициент при u double sigma; /// Коэффициент при du/dt >; function_3d_t calc_right_part( const function_3d_t& u, const constants_t& cs )  // f = -div(lambda * grad u) + gamma * u + sigma * du/dt return [=](double x, double y, double t) -> double   using namespace placeholders; auto ut = calc_first_derivative(bind(u, x, y, _1)); auto uxx = calc_second_derivative(bind(u, _1, y, t)); auto uyy = calc_second_derivative(bind(u, x, _1, t)); return -cs.lambda * (uxx(x) + uyy(y)) + cs.gamma * u(x, y, t) + cs.sigma * ut(t); >; > 

Код взят опять же из репозитория по УМФ.

Таким образом, мы получаем функцию f на основе известной нам функции u .

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

#Обертка для подсчета вызовов функции

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

Это можно сделать не вмешиваясь в код этого метода, явным образом выставляя count++ при каждом вызове функции, послав вместо функции обёртку над ней:

typedef std::functiondouble(const Vector&)> Function; Function setFunctionToCountCalls(int* where, const Function& f)  (*where) = 0; return [where, f](const Vector& v) -> double  (*where)++; return f(v); >; > double f(const Vector& v)  // . > int fCount = 0; auto result = optimize(/* . */, setFunctionToCountCalls(&fCount, f), /* . */); 

#Замер времени

У нас стоит задача замерить время работы какого-то кода. Можно поступить следующим образом:

#include using namespace chrono; auto start = high_resolution_clock::now(); // . // main code // . auto end = high_resolution_clock::now(); double time = duration_cast(end - start).count(); 

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

#include #include /** Считает время выполнения функции f в микросекундах. */ inline double calc_time_microseconds(const std::functionvoid(void)>& f)  using namespace std::chrono; auto start = high_resolution_clock::now(); f(); auto end = high_resolution_clock::now(); return duration_cast(end - start).count();; > double time = calc_time_microseconds([&]()  // . // main code // . >); 

Притом мы не теряем локальные переменные благодаря использованию лямбд.

#Двойной интеграл

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

#include typedef std::functiondouble(double)> function_1d_t; double calc_integral_gauss3( double a, double b, int n, // n - количество внутренных узлов const function_1d_t& f ); 

Тогда двойной интеграл двумерной функции можно вычислить следующим образом:

double calc_integral_gauss3( double ax, double bx, int nx, double ay, double by, int ny, const function_2d_t& f )  return calc_integral_gauss3(ax, bx, nx, [ay, by, ny, f](double x)->double   return calc_integral_gauss3(ay, by, ny, bind(f, x, _1)); >); > 

#Функции тестирования

Представим, у нас есть множество методов, решающих одну задачу, но каждый по-разному, при этом интерфейс у них одинаковый. И стоит задача протестировать их все единым образом, построить таблицы. Это тоже можно красиво сделать с помощью std::function , написать прототип функции метода:

typedef std::function/* Method args. */)> Method; MethodResult method1(/* Method args. */); MethodResult method2(/* Method args. */); MethodResult method3(/* Method args. */); MethodResult method4(/* Method args. */); MethodResult method5(/* Method args. */); void makeTableForMethod( const Method& method, const std::string& file, /* Method args. */ )  // . > std::vector> methodsToTest =   "table1.txt">,  "table2.txt">,  "table3.txt">,  "table4.txt">,  "table5.txt">, >; for (auto& i : methodsToTest) makeTableForMethod(i.first, i.second, /* Method args. */); 

Некоторые люди делают это с помощью копипаста, но думаю не стоит вам объяснять чем плох копипаст по сравнению с этим подходом.

#Одномерный поиск

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

typedef std::functiondouble(const double&)> OneDimensionFunction; typedef std::functiondouble(const OneDimensionFunction&, double, double, double)> OneDimenshionExtremumFinder; double oneDimensionOptimizator1(const OneDimensionFunction& f, double a, double b, double eps); double oneDimensionOptimizator2(const OneDimensionFunction& f, double a, double b, double eps); double oneDimensionOptimizator3(const OneDimensionFunction& f, double a, double b, double eps); double oneDimensionOptimizator4(const OneDimensionFunction& f, double a, double b, double eps); typedef std::functiondouble(const Vector&)> Function; MethodResult multiDimenshionOptimize1( const Function& f, const OneDimenshionExtremumFinder& argmin, const Vector& x0, const double& eps ); MethodResult multiDimenshionOptimize2( const Function& f, const OneDimenshionExtremumFinder& argmin, const Vector& x0, const double& eps ); 

#Паттерн listener

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

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

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

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

#Инкапсулируем с помощью std::function

В УМФ у нас есть задача получить конечно-элементную аппроксимацию функции на основе правой части дифференциального уравнения и сетки конечных элементов. Учитывая эту информацию, решатель МКЭ может выглядеть следующим образом:

vector_t solve_differential_equation( const function_3d_t& f, const grid_t& grid ); 

Возвращается вектор, потому что мы находим только аппроксимацию весов q , именно они и возвращаются в методе.

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

Поэтому мы должны каким-то образом внутри функции решения дифференциального уравнения выставлять краевые условия.

В нашем курсе УМФ мы не решаем реальные задачи, а лишь исследуем насколько хорошо метод справляется с известными нам функциями u , поэтому может появиться соблазн написать следующим образом:

vector_t solve_differential_equation( const function_3d_t& f, const grid_t& grid, const function_3d_t& true_function_u )  // doing smth // выставляем краевые условия с помощью функции u по краям области // делаем что-то дальше > 

Но это ужасный стиль! Зачем нам находить значение функции при помощи численных методов, которую мы уже знаем? Это выглядит как бред, поэтому более красивым может быть передавать функцию, которая выставляет краевые условия:

vector_t solve_differential_equation( const function_3d_t& f, const grid_t& grid, const boundary_setter_function_t& set_boundary_conditions )  // doing smth set_boundary_conditions(/* . */); // делаем что-то дальше > 

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

Так как вы можете использовать это на практике, более подробно смотрите код из курсовой по УМФ:

  • Прототип функции выставления краевых условий.
  • Сама функция выставления краевых условий на основе известной нам функции u.
  • Функция решения МКЭ.
  • Пример использования.

#Заключение

Было показано каким образом std::function позволит вам избавиться от лишнего копипаста и инкапсулировать код и данные и в принципе сделать код намного более абстрактным и красивым. Если вы придумали ещё одно интересное применение std::function , то пишите в комментариях!

Если вам нравится что я делаю, то подписывайтесь на меня в Telegram и YouTube.

C что из себя представляет std function

Для возвращения результата из функции применяется оператор return . Этот оператор имеет две формы:

return выражение; return;

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

Единственная функция, которая возвращает некоторое значение, и где можно не использовать оператор return — это функция main .

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

#include #include int sum(int, int); int main() < int result = sum(10, 6); // 16 std::cout int sum(int n, int m)

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

int sum(int, int);

И в этом случае в функции sum необходимо использовать оператор return , после которого идет возвращаемое значение:

int res = n + m; return res;

В данном случае возвращается значение переменной res . Хотя это могло бы быть сложное выражение, которое возвращало число int, например:

int sum(int n, int m)

Так как функция sum возвращает значение, то ее результат можно присвоить какой-нибудь переменной или константе:

int result = sum(10, 6); // 16 std::cout 

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

std::cout 

Рассмотрим еще один пример:

#include int calculate(int, int, char); int main() < std::cout int calculate(int n, int m, char op) < switch(op) < case '+': return n + m; case '-': return n - m; case '*': return n * m; default: return 0; >>

Здесь определена функция calculate , которая также принимает два числа и символ - знак операции. В конструкции switch в зависимости от знака операции с помощью оператора return возвращается результат определенной операции

return без возвращения значения

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

Например, функция принимает имя и возраст пользователя и выводит их на консоль:

#include #include void print(std::string, unsigned); int main() < print("Tom", 38); // Name: Tom Age: 38 print("Bob", 2500); // Incorrect age >void print(std::string name, unsigned age) < // если запредельный возраст if(age >120) < std::cout std::cout

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

Выведение типа результата

Компилятор С++ может автоматически выводить тип возвращаемого значения, если вместо возвращаемого типу используется оператор auto :

#include // автоматически выводится возвращаемый тип auto sum(int a, int b) < return a + b; >int main() < std::cout 

Здесь тип результата в функции sum выводится автоматически. Поскольку возвращется сумма a + b , результат которой будет представлять тип int , соответственно компилятор выведет, что функция возвращает тип int. Стоит отметить, что функция sum определена до того, как она вызывается в функции main.

В данном случае нет большого смысла использовать оператор auto вместо int . Обычно auto применяется, если название возвращаемого типа довольно большое и сложное, что позволит сократить код.

C++, std::function, шаблоны, что я делаю не так?

Первый вызов функции test компилируется, второй — нет, сообщение об ошибке:

test.cpp: В функции «int main()»: test.cpp:19:67: ошибка: нет соответствующей функции для вызова «test(std::vector::iterator, std::vector::iterator, )» test(data.begin(), data.end(), mysort); ^ test.cpp:19:67: замечание: candidate is: test.cpp:6:6: замечание: template void test(Iterator, Iterator, std::function) void test(Iterator begin, Iterator end, std::function func) ^ test.cpp:6:6: замечание: template argument deduction/substitution failed: test.cpp:19:67: замечание: mismatched types «std::function» and «void (*)(__gnu_cxx::__normal_iterator , __gnu_cxx::__normal_iterator )» test(data.begin(), data.end(), mysort); ^ test.cpp:19:67: замечание: could not resolve address from overloaded function «mysort» test.cpp: In instantiation of «void test(Iterator, Iterator, std::function) [with Iterator = __gnu_cxx::__normal_iterator ]»: test.cpp:18:70: required from here test.cpp:6:6: предупреждение: параметр «begin» не используется [-Wunused-parameter] void test(Iterator begin, Iterator end, std::function func) ^ 

std::sort объявлен так:

 template inline void sort(_RandomAccessIterator __first, _RandomAccessIterator __last) 

Кроме inline, нет никаких отличий от mysort, но дело не в inline (пробовал и его добавлять к mysort).

Заставить код компилироваться можно, например, так:

test::iterator>(data.begin(), data.end(), mysort::iterator>); 

Но почему в случае std::sort происходит разворачивание шаблона нормально, а в случае mysort нет?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *