Need help with the syntax for a function that takes a closure
I suggested in a comment that a functor may be a viable alternative to a
capturing lambda. This answer is mostly meant to answer the OP's request
to elaborate on that suggestion.

Let's define a ButtonCallback as an object that can be called like a
function taking a single boolean parameter:

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


To break down the syntax:


void operator()(bool) declares a method that allows the object to be
called as if it were a function taking a bool argument and returning
nothing. In other words, if you issue a call such as
my_object(true), that method will be called.
virtual _method_declaration_ = 0; says that the method is not going
to be defined for this class (it's a “pure virtual method”), but it
can be defined by derived classes. A class with such a method is
called an “abstract class”: it defines an interface, it cannot be
instantiated, and it's meant to be used as a parent for classes that
do implement the missing methods.


Now the Button class can be defined like this:

class Button {
public:
    Button(uint8_t pin, ButtonCallback &amp;callback)
    : pin(pin), callback(callback) {}
    void push() {  // for testing only
        Serial.print("press: ");   callback(true);
        Serial.print("release: "); callback(false);
    }
private:
    uint8_t pin;
    ButtonCallback &amp;callback;
};


Notice that the callback is stored as a reference to a
ButtonCallback object. The object itself cannot be stored because it
has an abstract type. Alternatively, a pointer could be used, which
would practically be equivalent.

For using this class, you first have to create the callback as a
concrete class that inherits from from ButtonCallback. Any data you
wish to capture should be stored as class data:

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;
};


With all this in place, the following test:

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(){}


outputs:

press: 42
release: 0


Did you said you “find the syntax of C++ pretty obtuse”? I can only
concur with you!



Addendum: Another alternative is to define the callback using the
good old C idiom: a “lambda” is the combination of a function pointer
and a generic data pointer. The function pointer looks like this:

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


The second argument is used to provide the callback whatever data it is
supposed to have “captured”. It is a generic pointer (a void *)
because the library does not know what kind of data its client may need.
Now the Button class has to store both the function pointer and the
data pointer, and always provide the data pointer to the callback:

class Button {
public:
    Button(uint8_t pin, ButtonCallback callback, void *callback_data)
    : pin(pin), callback(callback), callback_data(callback_data) {}
    void push() {  // for testing only
        Serial.print("press: ");   callback(true, callback_data);
        Serial.print("release: "); callback(false, callback_data);
    }
private:
    uint8_t pin;
    ButtonCallback callback;
    void *callback_data;
};


The user would have to decide what kind of data it needs, and do
explicit pointer casts to/from the proper pointer type and void *. For
example, assuming we need an int, the callback may be:

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


Here is the test, with the exact same output as before:

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

void loop(){}


Notice that, syntactic differences apart, this solution does essentially
the same as the previous one. The main difference is that the data is
given to the callback as an explicit pointer instead of the implicit
this pointer. If you have to choose between these two solutions, I
would suggest going for whichever syntax you find easier to work with.

Beware also that in both cases it is the programmer's responsibility to
manage the storage of the callback data and make sure the corresponding
reference (or pointer) remains valid throughout the lifetime of the
Button object.