Skip to content

Instantly share code, notes, and snippets.

@NickTikhomirov
Last active January 12, 2020 14:33
Show Gist options
  • Save NickTikhomirov/0d032752d256d5fee2673cdefd89ad85 to your computer and use it in GitHub Desktop.
Save NickTikhomirov/0d032752d256d5fee2673cdefd89ad85 to your computer and use it in GitHub Desktop.
questions for answer nomer 12

Cодержание

  • 12.1 - указатели на функции
  • 12.2 - functional
  • 12.3 - лямбда-функции
  • 12.4 - функторы
  • Стрельба по коленям

12.Х

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

(заголовочных файлов не требуется, пространства имён не нужны)

Указатель на функцию – это переменная, хранящая ссылку на функцию (место расположения её исполняемого кода в памяти компудахтера). Конкретно эта фича унаследована из замечательного языка С, поэтому работать с ней также "удобно" и "приятно" как c кактусом в заднице и с указателями на переменные*. В других вопросах типа 12.Х будут рассмотрены альтернативы.

Как правильно оформлять указатель на функцию? возвращаемый тип (*имя переменной)(аргументы)

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

//несколько функций, чтобы на их примере потом прогу гонять
int sum(int a, int b){ return a+b;} //могу реализовать через побитовые операции и рекурсию, но вы меня побьёте
int mult(int a, int b){return a*b;}

int main(){
  int (*bibilov)(int,int) = nullptr;   //у аргументов не обязательно должны быть имена, но типы нужны
  bibilov = sum;   //функцию называем по имени, но без скобок и аргументов - тогда это ссылка на функцию
  cout << bibilov(2,3)<< endl;  //5
  bibilov = mult;
  cout << bibilov(2,3) << endl; //6
}

Указатели на функции можно передавать в другие функции (вау) и хранить в контейнерах (нифига себе).

// пример сразу на оба приёма
int sum(int a, int b) { return a + b; }
int mult(int a, int b) { return a * b; }

int do_something(int a, int b, int (*ukaz)(int, int)) {
	return ukaz(a, b);
}

int main(){
  vector<int(*)(int, int)> jobs{sum,mult};
  for (int i = 0; i < jobs.size(); ++i)
  	cout << do_something(3, 5, jobs[i]) << endl;
}

//Программа выведет:
//8
//15

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

тип (*имя(аргументы возвращающей))(аргументы возвращаемой)

Пример:

int sum(int a, int b) { return a + b; }
int mult(int a, int b) { return a * b; }
int minus__(int a, int b) { return a - b; }

//функция, которая принимает int - номер запрашиваемой функции, а возвращает ссылку на функцию int(*)(int,int)
//как вы видите, рост сигнатуры будет происходить не вправо, а "вглубь" - в кроличью нору из скобок и функций
int (*getById(int a))(int, int) {
  switch(a){
  case 0: return sum;
  case 1: return mult;
  case 2: return minus__;
  //default не пишу, потому что мне лень и вообще отстаньте от меня все, я тут с функциями играюсь
  }
}

int main(){
  cout << getById(0)(1,1)<<endl;  //на данных параметрах это 1+1
}

//Программа выведет:
//2

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

Можете почитать дополнительный гист моего же авторства про с бонусными примерами.

12.2. std::functional

(заголовочный файл <functional>, пространство имён std)

Судя по всему, от нас хотят, чтобы мы рассказали про то, что живёт в этой библиотеке.

std::function - это обёртка над указателями на функции, упрощающающая... не знаю что. Ну то есть это такая же обёртка, как array над [], как vector над * = new [], но если в этих двух контейнерах реально есть потребность, потому что операции копирования и очистки за тебя прописаны, то функция просто эстетичнее выглядит. Класс шаблонный, поэтому оформление:

std::function<тип_возвр(тип_аргументов)> имя_переменной

Рассмотрим пример, который, на самом деле, абсолютно идентичен примеру с указателем на функцию в С-стиле:

int sum(int a, int b) { return a + b; }

int main() {
  function<int(int, int)> a;     //std:: опущено, но я в вас верю, вы додумаете
  a = sum;
  cout << a(10, 15);             //25 в консоли
}

Хранение в контейнере, передача в функцию - всё это строится так же, как указатель на функцию в С-стиле. Разве что функция, возвращающая функцию, выглядит более приятно:

int sum(int a, int b) { return a + b; }
int mult(int a, int b) { return a * b; }

function<int(int,int)> getById(int a){   //Выглядит удобнее, согласен
  if (a == 0) return sum;
  else return mult;
}

int main() {
  cout << getById(1)(10, 15);            //150
}

Отличия function от указателя:

  • Удобное оформление, благодаря чему ещё и удобное обращение с возвращением функции из функции
  • У function<> перегружен оператор приведения типа к bool. Он кастуется к false, если не указывает ни на какую функцию (в остальных случаях - к true)
  • По стандарту указатель на функцию не может указывать на лямбда-функцию с захватом контекста, но про function такого не сказано

Партия требует от нас примеры применения, но если вы внимательно читали вопрос 12.1, то без труда составите пример, потому что надо просто заменить int(*)(int,int) на function<int(int,int)>, а можно даже своё что-нибудь придумать, если голова на плечах есть.

std::bind - это способ изготавливать новые функции из уже существующих путём махинаций с параметрами. Оформление сразу на примере bind синуса на ноль:

int main() {
  double (*s)(double) = sin;
  //синус можно было бы передать в bind и без указателя, но он перегружен
  //и bind не понимает, какой синус я от него хочу
  //ещё можно static_cast синуса к этому же указателю :)

  function<double()> zero = bind(s, 0);                                          // Строчка, ради которой вы здесь
  //получили функцию, которая каждый раз возвращает синус нуля
  //обратите внимание, что в шаблоне<> у функции уже нет никаких аргументов - все заняты

  cout << zero(); // и да, zero - это именно функция, а не переменная
  
  //Программа выведет:
  //0
}

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

//Допустим, у нас есть функция расчёта расстояния двумя точками в 2D плоскости
double distance(int x1, int y1, int x2, int y2) {
  return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}


int main() {
  //И мы хотим сделать функцию, которая будет считать расстояние до конкретной фиксированной точки
  function<double(double, double)> utopiaDist = bind(distance, 10, 10, placeholders::_1, placeholders::_2);
  cout << utopiaDist(4,2); //10, я спициально по пифагору подгонял
}

Как ни обидно, пример нуждается в объяснении. std::placeholders - это пространство имён, в котором есть такие замечательные константы как _1, _2, _3 ... и так до какого-то числа, которое создатели решили сделать последним. Это номера переменных в новой функции. То есть мы передадим ей переменные номер 1 и номер 2, а она их засунет в исходную функцию на места 3 и 4. Вот без труда мы создали новую функцию. Ещё несколько примеров:

int main(){
  //Поменяли местами аргументы в степени
  //Функцию пришлось кастовать, но это не слишком больно, если читать по частям
  function<double(double, double)> wop = bind(static_cast<double(*)(double,double)>(pow), placeholders::_2, placeholders::_1);
  cout << pow(2, 3) << endl;   //8
  cout << wop(2, 3) << endl;   //9
  function<double(double)> quberoot = bind(wop, 1./3, placeholders::_1);   //изобрели кубический корень через степень (нашу)
  cout << quberoot(8)<<endl;   //2
}

Негаторы - это функторы-предикаты, которые создаются на основе пользовательских функторов-предикатов и возвращают их отрицание. Всего негаторов два - унарный и бинарный. Про функторы написано в вопросе 12.4.

В аргументы шаблона негатора передаётся класс функтора, а в конструктор негатора передаётся объект функтора.

struct is_Even: unary_function<int, bool> {        //Наследование от класса unary_function обязательно
  bool operator()(int i) const{
      return (i + 1) % 2;
  }
};

int main(){
  unary_negate<is_Even> d(is_Even{});              //Собственно, сам негатор
  vector<int> evens{ 2,4,6,8,10,11 };
  auto r = find_if(evens.begin(), evens.end(), d);
  cout << *r;
}

Шаблонные функторы на каждый оператор Про них написано в самом конце вопроса 12.4

mem_fn - это функция, которая, как и bind, принимает функцию и делает из неё другую функцию. Что же именно она делает? Она позволяет выломать метод класса из этого класса. Соответственно, для сохранения адекватности кода в эту функцию будет добавлен новый аргумент - объект этого класса, который передаётся в скобки первым.

Пример - выламывание из функтора-минуса (смотри 12.4) его операции и преобразование в декремент (без изменений на исходнойм числе):

int main() {
	minus<int> temp;
	function<int(minus<int>, int, int)> min = mem_fn(&minus<int>::operator());
	function<int(int)> decr = bind(min, temp, placeholders::_1, 1);
}

12.3. Лямбда-функции

(заголовочных файлов не требуется, пространства имён не нужны)

Лямбда-функции - это такие функции, которые могут быть написаны прямо посреди кода программы с минимальными усилиями на соответствующее оформление. Пример простейшей лямбда-функции типа void, которая ничего не принимает и ничего не делает:

[](){};

Давайте разберём, что по этой функции видно:

  1. Бросается в glaza, что у функции нет имени
  2. У функции нет явно указанного возвращаемого типа
  3. Круглые скобки мы в будущем будем использовать для аргументов
  4. Фигурные скобки мы будем использовать для кода функции
  5. Квадратные скобки - это не массив, и про них мы поговорим позже

Надо понимать, что это только объявление функции и описание того, что она делает. Чтобы она начала работать на благо цивиилизации, надо её вызвать. Вызываются функции по имени, поэтому лямбда-функцию вызвать нельзя, конец урока *заставка из ералаша*.

Шутка.

Как вызвать? Вызвать её можно, присвоив её (да, прям её, прям всё вот это выражение вместе с кодом в фигурных скобках) какому-нибудь указателю на функцию и, соответственно, вызвав с него. Можно использовать классический указатель, можно использовать обёртку function<>, можно использовать тип auto. Есть и альтернативный способ вызвать её - сразу же после фигурных скобок поставить снова круглые и засунуть в них всё, что требуется - тогда функция будет тут же вызвана.

Зачем нам такое надо? Допустим, у нас есть функция, которая принимает другую функцию - такие обитают, например, в алгоритмах stl. Скажем, какой-нибудь find_if, который вызывается от двух итераторов и функции-предиката, которая ищет закладки проверяет объект по каким-то критериям. И вот представьте, что вам впадлу вылезать из процесса кодинга в данном месте, идти куда-то, писать этот предикат, имя ему придумывать, вся херня... Ну вот тогда и надо вспомнить про лямбда-функции - такую можно легко всунуть в этот сраный find_if прям на месте вызова, и минимум проблем.

А что же там с возвращаемым значением? Кто внимательно читал тему указателей на функции, заметил, что уточнять возвращаемое значение - это ващет важно и нужно. А тут его нет. Как так? Всё просто, функция сама придумает себе значение на основе того, что ты в ней return. Соответственно, получаем тонкое место - два return с разными возвращаемыми типами вынесут функции мозг, и она не скомплириуется. Кроме того, если вы работаете с указателями, то учтите, что лямбда-функция не обучена гадать по nullptr, поэтому если хотя бы один такой у вас return, то идите читайте следующий абзац, как оформлять такие случаи.

Совсем-совсем два разных типа нельзя? На самом деле, немножко можно - функции можно всё-таки задать значение ручками, после чего она начинает нормально работать c преобразованием типов, с динамическим полиморфизмом на указателях и с nullptr. Это будет в следующем через один примере.

Поехали чёнить умное напишем

int main() {
    // допустим, мы хотим найти самый ранний комментарий к посту с >10 лайками на нём
    // мы прочитали комментарии в порядке публикации и положили в вектор (ранние в начале, поздние в конце)
    // каждый комментарий - это пара из числа (кол-во лайков) и текста комментария
	vector<pair<int, string>> thing{
		{0, "hello"},
		{5, "excuse me"},
		{2, "pardon"},
		{3, "90"},
		{21, "hochu arbuz"},
		{11, "society"}
	};
	// ну и вот собственно find_if, я его в столбик написал для читабельности
	// напомню, он принимает итератор начала поиска, итератор конца и функцию-предикат
	// функция применяется ко всем элементам и при первом получении true мы прекращаем поиск и возвращаем итератор на этот элемент 
	// это всё весной было, а сейчас мы будем собственно функцию писать прям на ходу
	auto t = find_if(
		thing.begin(), 
		thing.end(), 
		[](pair<int, string> r) {
			return r.first > 10;
		}
	);                            // это закрывающая скобка find_if-а, если кто не понял
	if (t != thing.end())
		cout << t->second; //нашли и вывели
}
//В консоли:
//hochu arbuz

А теперь вернёмся к теме явного объявления типа в лямбда-функции. В структуру написания оной он вклинивается между круглой и фигурной скобками, а перед именем типа ставится стрелочка из минуса и знака больше. После этого можно использовать nullptr, динамический полиморфизм и приведение типов.

int main() {
  int* arr =                //Будем использовать вызов по скобкам сразу после фигурных, поэтому сразу ловим значение
  [](int size) -> int* {    //Та строчка, ради которой вы здесь
      if (size == 0) 
          return nullptr;
      return new int[size];
  }(0);                     //Конец лямбда-функции и вызов её
  if (arr == nullptr) 
      cout << "it works";
}

А что там с квадратными скобками? Для начала расшарим терминологию. Это захват контекста. Раз уж наша функция написана в каком-то куске кода, то мы вполне можем хотеть, чтобы оно взаимодействовало с переменными из этого кода. Например, увеличивало какой-нибудь счётчик в локальной переменной. Для многопоточности полезно, опять же, можно и функцию для потока без труда написать, и результат работы потока без труда засунуть куда-нибудь. Но по умолчанию так нельзя. Если у вас в квадратных скобках не стоит ничего, то вы не видите локальные переменные (глобальные переменные видны всегда безо всяких захватов). Что можно поставить в скобки и что будет после этого видно:

  • [имя переменной] - захват переменной на чтение - можно только копировать её значение, никаких изменений вносить нельзя (можно несколько через запятую)
  • [&имя_переменной] - захват переменной - возможность свободно видеть и изменять её (можно несколько через запятую)
  • [&] - захват с правом изменения вообще всего, до чего дотягиваются руки
  • [=] - захват вообще всего, но только на чтение
  • [] - захват ничего никак

Пример захвата контекста на чтение

int main() {
    // суть примера - логарифмируем все элементы вектора по основанию, которое надо захватывать
	vector<double> contents{ 2,4,8,16,32,64 };
	double base = 2;
	for_each(                             //Кстати, тоже очень хороший пример функции, которая принимает другую функцию
		contents.begin(), 
		contents.end(), 
		[base](double& a){                //Лямбда-функция, мы захватили base на чтение
			a = log(a) / log(base);
		}
	);

	for (auto r : contents) {
		cout << r << endl;                // 1 2 3 4 5 6 в столбик ==> успех
	}
}

Прочие особенности:

  • Согласно стандарту, лямбда-функцию с захватом контекста нельзя класть в указатель на функцию, однако можно класть в function<>
  • Можно делать вложенные лямбды
  • Про исключения в лямбдах интернет говорит что-то невнятное, поэтому лучше не выпендриваться
  • Круглые скобки аргументов при объявлении лямбды можно убрать, если они пустые, то есть []{}; - тоже корректная лямбда
  • Захват по значению не только не допускает изменений в захваченной переменной, но и запрещает даже обращаться с ней как в обычных функциях - как угодно изменять с нулевым эффектом для оригинала
  • Все ваши лямбда-функции втайне от вас constexpr.
  • Чтобы работать с захваченной по значению переменной как с обычным параметром функции (изменять без эффекта для оригинала) надо вставить в объявлении лямбды слово mutable между круглыми скобками и стрелкой для возвращаемого типа
  • В статье о лямбда-функциях на cppreference есть ещё пара уточнений по захватам вроде того, что переменные типа enum с const значениями можно читать без захвата, но, я полагаю, это никому не интересно

12.4. Функторы

(заголовочных файлов не требуется, пространства имён не нужны)

В разных языках программирования этот термин означает разные явления, о чём можно почитать туть (хабр).

Функтор в С++ - это любой класс, у которого перегружен оператор(). Пример:

struct Logarithm{
  double base = 2;
  double operator()(double x){
    return log(x)/log(base);
  }
};
  
int main(){
  Logarithm l;
  l.base = 10;
  cout<<l(10)<<endl;
}

В чём отличие функтора от функции? Функтор - это объект, у него есть поля, с которыми взаимодействует оператор(), это открывает нам некоторые возможности.

  1. Мы можем задать все константы и коэффициенты функции в поля, и получать к ним доступ из основной программы
  2. Мы можем вынести часть кода функции во вспомогательные методы и пользоваться ими из основной программы
  3. Мы можем наследоваться от функтора и, например, использовать динамический полиморфизм
  4. Самое главное: Мы можем создать несколько объектов-функторов одного класса и через взаимодействие с их полями и методами привести их в разные состояния - в примере с логарифмом мы могли создать несколько логарифмов с разными основаниями
  5. При работе с объектами не возникает проблем, которые преследуют нас при работе с указателями
  6. Говорят, работает быстрее, чем указатель на функцию, но я не проверял

Почему именно круглые скобки? Почему нельзя перегрузить, например, квадратные и тоже называть такой объект функтором? Дело в том, что круглые скобки - единственный оператор в С++, который можно перегрузить на сколько угодно параметров.

//Темы примера: 
//множественные перегрузки, 
//перегрузки на >1 аргумента, 
//создание нескольких функторов в разных состояниях


//Что-то я второй пример подряд использую слово struct, 
//но мне просто лень возиться с модификаторами доступа.
//Слово class, конечно, тоже можно использовать.
struct Greeter {
  string time;
  void operator()(string f, string i, string o) {
      cout << "Good " << time << ", " << f <<" "<< i << " " << o << "!\n";
  }
  void operator()(string n) {
      cout << "Good " << time << ", " << n << "!\n";
  }
};

int main() {
  Greeter greeter1;
  Greeter greeter2;
  greeter1.time = "day";
  greeter2.time = "evening";

  greeter1("Tikhomirov", "Nikita", "Alexandrovich");
  greeter2("Tikhomirov", "Nikita", "Alexandrovich");
  greeter1("my beloved almighty creator");

//В консоли:
//Good day, Tikhomirov Nikita Alexandrovich!
//Good evening, Tikhomirov Nikita Alexandrovich!
//Good day, my beloved almighty creator!
}

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

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

int main(){
    minus<int> m;   //Создаём объект типа минус для типа int
    cout<< m(8, 3); //5
    
}

Стрельба по коленям

Как стрелять себе в колени с указателями на функции (12.1)

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

Во-первых, вы можете стрелять себе в колени, когда забываете топ-10 пранков с указателями

int main(){
  
  int(*bibilov)(int,int);
  cout<<bibilov(2, 3)<<endl; 
  // ну эта^ херня даже не скомпилируется, вас отругают за отсутствие инициализации в переменной
  
  int (*bibilov)(int,int) = nullptr;
  cout<<bibilov(2, 3)<<endl;
  // здесь^ всё скомпилируется, а уже потом вам сломают колени
}

Колени ломает именно вызов функции, поэтому никто не запрещает вам создать переменную, а спустя 3791 строку кода инициализировать её функцией, а уже потом вызвать.

Во-вторых, вы можете стрелять по коленям, когда пытаетесь впихнуть невпихуемое - то есть сигнатуры не совпадают

string concat(string a, string b) { return a + b; }
int sosat(string a, string b) { return (a + b).size(); }
short micro_sum(short a, short b){ return a+b};
int mass_sum(int a, int b, int c = 0) { return a + b + c };

int main(){
  int (*bibilov)(int,int);
  bibilov = concat;    //возвращает не int, принимает не (int, int)
  //тут вам вижуалка красным подчеркнёт, а CLion ещё и за орфографию названий переменных сломает ноги
  
  int (*bibilov)(int,int);
  bibilov = sosat;    //теперь тип тот же, отличаются только аргументы
  //абсолютно то же самое
  
  int (*bibilov)(int, int);
  bibilov = micro_sum;   //всё разное, но short кастуется к int
  //абсолютно то же самое
  
  int (*bibilov)(int, int);
  bibilov = mass_sum;   //почти всё хорошо, но есть ещё один параметр, который имеет значение по умолчанию
  //абсолютно то же самое
}

Однако, очевидно, вам никто не запрещает создать указатель наподобие int(int,int), инициализировать его такой же функцией, а потом засунуть в него шорты. Ну, не в смысле шорты которые штаны, а в смысле переменные типа short.

В-третьих, по коленям стреляют те, кто полагаются на параметры по умолчанию

int mass_sum(int a, int b, int c = 0) { return a + b + c; }; 

int main(){
  int (*bibilov)(int, int, int);
  bibilov = mass_sum;
  cout << bibilov(2, 5)<< endl;
  //так нельзя
  //и никак иначе такое тоже нельзя
  //нет, нельзя в определение bibilov дописать =0.
  //ВООБЩЕ НИКАК ЭТО НЕЛЬЗЯ
  //мне тоже обидно :(
  
  //В самих функциях с параметрами по умолчанию проблем нет, и передавать их можно свободно
  //но о фичах "умолчания" можете забыть
}

Кстати, эту проблему решают здесь (cyberforum) - оборачивают функцию в структуру с перегруженным оператором скобок. Но это уже для саморазвития.

И, неожиданно, "в-четвёртых" не будет - указатель на функции с переменным числом параметров работает. Ну, настолько, насколько работают сами функции с переменным количеством параметров.

//функция работает на параметрах 5,1,2,3,4,5 -- результат: 15
int super_summ(int amount, int a ...){
	int result = 0;
	int* j = &a;
	while (amount) {
		--amount;
		result += *j;
		++j;
	}
	return result;
}
// я пробовал создать класс без полей и объект этого класса засунуть в эту функцию (без указателя)
// она схавала и даже что-то насчитала
// вывод: функции с переменным количеством параметров - говно небезопасное


int main(){
  int (*bibilov)(int, int...);
  bibilov = super_summ;
  cout << bibilov(5, 1, 2, 3, 4, 5) << endl; // указатель замечательно работает
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment