Tuesday, November 26, 2019

Обратные вызовы и лямбда выражения в си++

Отец плюсов говорит - “Не совершенствуйте язык, а совершенствуйте его применение”. Давайте так и поступим. Нам пригодилось бы передавать функцию в другую функцию в качестве параметра, т.е. как переменную. Еще много чего, но давайте потерпим. Оказывается, так можно. Как и любая переменная, функция имеет свой тип, т.е. область в памяти, определенного размера, достаточного для хранения информации о том, как выглядит наша функция (что возвращает и какие параметры принимает). Имея указатель на эту область, мы можем выполнить наши ранее описанные задачи. Изменить значению такого указателя, можно просто присвоив ему имя функции, которая нам подходит. А выполнить функцию через указатель можно с помощью добавления к имени указателя круглых скобок.

Для примера функция void foo() имеет тип void(*)(), чтобы объявить и проинициализировать указатель такого типа, нужно написать void (*fPtr)() = nullptr, где fPtr и есть имя указателя. Далее можно отследить по коду. Погодите, мы углубились в язык, но совсем забыли про применение. Где же можно применить такие чудо-возможности? Например, для хранения коллекции разных действий (функций), или, например, для получения обратного вызова. Обратный вызовэто вызов функции, которая в свою очередь должна вызвать функцию, полученную в качестве параметра. Часто это может быть полезно, например, запуская метод click() для кнопки, и передавая ему действие, которое должно быть выполнено при нажатии на кнопку.

Еще одно полезное применение можно получить, для передачи в коллекцию действия, которое мы хотим выполнить над каждым элементом коллекции. В стандартной библиотеке есть множество вспомогательных алгоритмов, например, сортировка, которая уже реализует метод сравнения элементов для стандартных типов. Но что делать с пользовательскими типами? Мы сами можем создать метод сортировки и передать его в качестве функции. Но у такого решения есть один недостаток, функция имеет свое пространство имен, что требует передачу в нее других параметров, это часто не удобно, т.к. к примеру метод сортировки, ждет только определенные параметры, которые зафиксированы в типе (функции) параметра и если мы туда передадим еще что-то, то сломаем логику. Решить эту проблему призваны лямбда-выражения, они же лямбда-функции, они же замыкания. Это специальный синтаксис, позволяющий захватить внешние для самой лямбды переменные и использовать их внутри себя, оставляя привычным (для принимающей стороны) передачу параметров. С технической стороны, лямбды требуют отдельного рассмотрения, т.к. они имеют очень специфический синтаксис. Особенно в контексте работы со стандартной библиотекой. Как вариант, мы можем писать лямбду на месте самого параметра, принимающего функцию, что часто очень наглядно выражает логику программы. На текущий момент мы говорим о них, как о еще одной возможности передавать действие в качестве параметра (смотрите пример кода) и пока на этом остановимся.
 
#include <iostream>
#include <vector>

class Button {
public:
    explicit Button(void (*_fn)()) : fn{_fn} {    }

    void click() {
        fn();
    }

    void setFn(void (*_fn)()) {
        fn = _fn;
    }

private:
    void (*fn)() = nullptr;
};

static void foo1() {
    std::cout << "foo1 called!" << std::endl;
}

static void foo2() {
    std::cout << "foo2 called!" << std::endl;
}

void foo3(void (*_l)()) {
    _l();
}

int main() {

    typedef void(*CB)();
    //std::vector<void (*)()> calls = {foo1, foo2};
    std::vector<CB> calls = {foo1, foo2};

    for (auto i : calls) {
        i();
    }
    //Output:
    //>>foo1 called!
    //>>foo2 called!

    auto b1 = Button(foo1);
    auto b2 = Button(foo2);

    std::vector<Button> btns = {b1, b2};

    for (auto b : btns) {
        b.click();
    }
    //Output:
    //>>foo1 called!
    //>>foo2 called!

    btns[0].setFn(foo2);
    btns[1].setFn(foo1);

    for (auto b : btns) {
        b.click();
    }
    //Output:
    //>>foo2 called!
    //>>foo1 called!

    foo3(foo2);
    //Output:
    //>>foo2 called!

    foo3([]() -> void {
        std::cout << "lambda called!";
    });
    //Output:
    //>>lambda called!

    return 0;
}

Перегуд В.

No comments: