Нужна помощь с синтаксисом функции, которая принимает замыкание

Мой C++ очень устарел. (более 20 лет, прежде чем в C++ были замыкания).

Я старший инженер ПО, и вчера я потратил несколько часов, пытаясь разобраться в этом, но так и не смог.

Фон:

Я пытаюсь создать класс ClosureButton, который является подклассом другого класса ArduinoObject. (ArduinoObject имеет setup(), start(), stop() и loop( ) методы, чтобы основная программа могла их вызывать.)

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

Конструктор ClosureButton будет принимать номер контакта и замыкание с логическим параметром (нажатие). Я бы сохранил это замыкание в переменной-члене. Когда состояние кнопки изменится, оно вызовет замыкание и передаст новое состояние кнопки замыканию.

Вопрос:

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

Конструктор будет выглядеть примерно так:

ClosureButton::ClosureButton(uint8_t pin, _unkonwn_syntax_ closure);

(Здесь мне нужна помощь с битом _unkonwn_syntax_.)

Затем мне нужно объявить переменную-член в моей ClosureButton, чтобы удерживать замыкание:

class ClosureButton: ArduinoObject {
  //другие вещи
  _other_unkonwn_syntax_ closure;
}

Опять же, мне нужна помощь с синтаксисом для объявления переменной-члена closure типа «замыкание, принимающее логический параметр». (Бит _other_unkonwn_syntax_.) На данный момент я не думаю, что мое замыкание будет захватывать переменные из охватывающей области, но когда-нибудь в будущем мне может понадобиться это сделать, поэтому помощь в этом будет оценена по достоинству. тоже.)

, 👍3

Обсуждение

В C/C++ всегда были указатели на функции., @Juraj

Да, я знаю (и мне всегда казалось, что синтаксис указателей на функции C очень сложно расшифровать. Я могу это понять, но нелегко.) Вы предлагаете мне использовать указатель на функцию C, а не замыкание? ?, @Duncan C


2 ответа


Лучший ответ:

2

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

Например:

void (*func)(bool b);

void setup() {
    func = [](bool b) {
        Serial.println(b);
    };

    Serial.begin(115200);
}

void loop() {
    func(digitalRead(0));
}

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

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

typedef void (*buttonCallback)(bool);

Тогда ваши функции будут выглядеть так:

ClosureButton::ClosureButton(uint8_t pin, buttonCallback closure);

и:

class ClosureButton: ArduinoObject {
  //другие вещи
  buttonCallback closure;
}

Основной синтаксис лямбды довольно прост:

[](bool foo, int bar) { ... }
^  ^                    ^
|  |                    +- Function body
|  +- Normal parameter list
+- Empty capture list

Если вы хотите вернуть значение и не хотите полагаться на то, что функция имеет «автоматический» тип возврата, вы можете указать тип возвращаемого значения после списка параметров:

[](bool foo, int bar)->int { ... }
^  ^                   ^     ^
|  |                   |     +- Function body
|  |                   +- Return type
|  +- Normal parameter list
+- Empty capture list

Если вы не указываете тип возвращаемого значения, он выводится из типа переменной/значения, используемого с return (как если бы функция была auto).


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

typedef int (*callback)(bool b);

class MyClass {
    private:
        callback func;
        int pin;

    public:
        MyClass(int pinno, callback myfunc) {
            func = myfunc;
            pin = pinno;
        }

        void doit() {
            Serial.println(func(digitalRead(pin)));
        }
};

MyClass foo(3, [](bool b)->int {
        Serial.println(b);
        return rand();
});


void setup() {
    pinMode(3, INPUT_PULLUP);
    Serial.begin(115200);
}

void loop() {
    foo.doit();
    delay(100);
}

Если вы хотите выполнить захват с помощью своих лямбда-выражений, то, боюсь, вам не повезло.

Тип данных для этого типа должен быть std::function<void>, но, к сожалению, урезанная библиотека C AVR-LIBC не поддерживает это. STL обычно не включается в небольшие микроконтроллеры, поскольку он использует слишком много ресурсов, чтобы быть практичным.

,

Хорошо, это похоже на то, что мне нужно. Спасибо. (Использование typedef определенно облегчает мне чтение). Надо будет попробовать, когда вернусь домой., @Duncan C

Однако это не сработает для захвата лямбда-выражений. Работа с ними – это совсем другая задача., @ratchet freak

@ratchetfreak Я не понимаю, что вы подразумеваете под «захватом» лямбды. Вы можете передать лямбду через функцию, присвоить ее переменной, чему угодно — точно так же, как указатель на функцию., @Majenko

Захватывающая лямбда — это лямбда, которая фиксирует переменные. Незахватывающая лямбда, подобная тем, что приведены в ваших примерах, — это просто анонимная функция, вряд ли заслуживающая названия «лямбда»., @Edgar Bonet

@EdgarBonet Ах, я понимаю, что ты имеешь в виду. Я новичок в лямбда-выражениях в C++., @Majenko

@EdgarBonet Насколько я могу судить, без STL невозможно передать захват лямбда-выражений. Там нет std::function., @Majenko

Функтор (объект с определенным operator()(bool)) может быть жизнеспособной альтернативой., @Edgar Bonet

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

@EdgarBonet, можешь поподробнее? Я нахожу синтаксис C++ довольно тупым, поэтому теряюсь. Я прочитал некоторую информацию о функторах, и мои глаза начали сходить на нет., @Duncan C


1

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

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

class ButtonCallback {
public:
    virtual void operator()(bool) = 0;
};

Чтобы разобрать синтаксис:

  • voidoperator()(bool) объявляет метод, который позволяет объекту быть вызывается так, как если бы это была функция, принимающая аргумент bool и возвращающая ничего. Другими словами, если вы выполняете вызов, например my_object(true), будет вызван этот метод.
  • virtual _method_declaration_ = 0; говорит, что метод не выполняется быть определен для этого класса (это «чисто виртуальный метод»), но он могут быть определены производными классами. Класс с таким методом называется «абстрактным классом»: он определяет интерфейс, его нельзя создается экземпляр, и он предназначен для использования в качестве родительского класса для классов, которые реализуйте недостающие методы.

Теперь класс Button можно определить следующим образом:

class Button {
public:
    Button(uint8_t pin, ButtonCallback &callback)
    : pin(pin), callback(callback) {}
    void push() {  // только для тестирования
        Serial.print("press: ");   callback(true);
        Serial.print("release: "); callback(false);
    }
private:
    uint8_t pin;
    ButtonCallback &callback;
};

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

Чтобы использовать этот класс, сначала необходимо создать обратный вызов в виде конкретный класс, который наследуется от ButtonCallback. Любые данные, которые вы хотите захватить, следует хранить как данные класса:

class CapturingLambda : public ButtonCallback {
public:
    CapturingLambda(int data) : some_data(data) {}
    virtual void operator()(bool pressed) {
        if (pressed)
            Serial.println(some_data);
        else
            Serial.println(0);
    }
private:
    int some_data;
};

При всем этом можно провести следующий тест:

void setup() {
    Serial.begin(9600);
    int data_to_capture = 42;
    CapturingLambda callback(data_to_capture);
    Button the_button(2, callback);
    the_button.push();
}

void loop(){}

результаты:

press: 42
release: 0

Вы сказали, что «находите синтаксис C++ довольно тупым»? я могу только согласен с вами!


Дополнение. Другой альтернативой является определение обратного вызова с помощью старая добрая идиома C: «лямбда» — это комбинация указателя на функцию и общий указатель данных. Указатель функции выглядит следующим образом:

typedef void (*ButtonCallback)(bool, void *);

Второй аргумент используется для предоставления обратного вызова, какие бы данные это ни были. якобы «захватил». Это общий указатель (void *). потому что библиотека не знает, какие данные могут понадобиться ее клиенту. Теперь класс Button должен хранить оба указатель на функцию и указатель данных и всегда предоставлять указатель данных для обратного вызова:

class Button {
public:
    Button(uint8_t pin, ButtonCallback callback, void *callback_data)
    : pin(pin), callback(callback), callback_data(callback_data) {}
    void push() {  // только для тестирования
        Serial.print("press: ");   callback(true, callback_data);
        Serial.print("release: "); callback(false, callback_data);
    }
private:
    uint8_t pin;
    ButtonCallback callback;
    void *callback_data;
};

Пользователю придется решить, какие данные ему нужны, и сделать явное приведение указателя к/из соответствующего типа указателя и void *. Для Например, если нам нужен int, обратный вызов может быть:

void callback(bool pressed, void *data) {
    int *actual_data = (int *) data;
    if (pressed)
        Serial.println(*actual_data);
    else
        Serial.println(0);
}

Вот тест с точно таким же результатом, как и раньше:

void setup() {
    Serial.begin(9600);
    int data_to_capture = 42;
    Button the_button(2, callback, (void *) &data_to_capture);
    the_button.push();
}

void loop(){}

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

Имейте в виду, что в обоих случаях ответственность за управлять хранением данных обратного вызова и убедиться, что соответствующие ссылка (или указатель) остается действительной на протяжении всего срока службы. Объект Кнопка.

,

Отличный ответ. Спасибо! (проголосовали). Единственная часть, которую мне не удалось разобрать, — это конструктор CapturingLambda. Можете ли вы объяснить это немного? Часть some_data(data) немного похожа на синтаксис вызова конструктора родительского класса, но не совсем., @Duncan C

@DuncanC: Это [список инициализаторов участников](https://en.cppreference.com/w/cpp/language/initializer_list). some_data(data) означает, что член класса some_data инициализируется значением параметра конструктора data. Более или менее эквивалентно написанию some_data = data; в фигурных скобках. В некоторых случаях (не здесь) список инициализаторов является единственным способом выполнить инициализацию., @Edgar Bonet

Хорошо, это помогает. Зачем использовать список инициализаторов членов вместо явного конструктора, который инициализирует переменные-члены? И в каких случаях список инициализаторов членов является единственным способом инициализации экземпляра?, @Duncan C

@DuncanC: 1. Это не имеет особого значения. 2. При вызове конструктора родительского класса и когда член является объектом без конструктора по умолчанию., @Edgar Bonet

Итак, можете ли вы одновременно вызвать конструктор родительского класса И использовать список инициализаторов членов? И если да, то как выглядит этот синтаксис?, @Duncan C

@DuncanC: конструктор родительского класса — это всего лишь особый вариант использования списка инициализаторов членов. Я не буду больше отвлекаться на эту тему., @Edgar Bonet