Start Kontakt

Szablon std::function C++

W artykule Funkcje a kacze typowanie zostały zaprezentowane konstrukcje języka C++, które zachowują się jak funkcje. Są to wskaźniki do funkcji oraz obiekty funkcyjne (włącznie z wyrażeniami lambda). Na podstawie artykułu można wywnioskować, że funkcja, która jako parametru używa wskaźnika do funkcji, ma inną sygnaturę niż ta, która wykorzystuje obiekt funkcyjny. Nie ma się zresztą czemu dziwić - ten pierwszy jest wskaźnikiem, a ten drugi obiektem.

Czy nie byłoby pięknie, gdyby sposób przekazywania obiektu wywoływalnego był ujednolicony, tzn. funkcja mogła przyjmować zarówno wskaźniki, jak i obiekty? Twórcy języka C++ pomyśleli o tym i od wersji C++11 mamy dostępny szablon std::function.

Przyjrzyjmy się poniższemu kodowi:

class Add2Vals
{
public:
    int operator()(const char c, const int i) const
    {
        return c + i;
    }
};

int fun(char c, int i)
{
    return i + c;
}
      
void funCaller(std::function<int(char, int)> fun)
{
    std::cout << fun(10, 20) << std::endl;
}

int main()
{
    funCaller(fun); // parametr jako wskaźnik
    funCaller(Add2Vals()); // parametr jako obiekt funkcyjny
    funCaller([](const char a, const int b) -> int { return a + b; }); // parametr jako wyrażenie lambda
}

Oto uzyskane wyniki:

30
30
30

Najpierw deklarujemy obiekt funkcyjny Add2Vals przyjmujący dwa parametry char i int, następnie funkcję fun o takich samych parametrach, a wreszcie funkcję funCaller, której parametrem jest typ uzyskany za pomocą szablonu std::function. Jak widać, w szablonie std::function określamy, że obiekt wywoływalny ma zwracać typ int oraz używać parametrów o typach char i int.

W funkcji main wywołujemy po kolei funkcję funCaller ze wskaźnikiem, obiektem funkcyjnym, a wreszcie wyrażeniem lambda wyznaczanym "w miejscu". Wszystkie trzy wywołania zwracają zgodnie z oczekiwaniami ten sam wynik.

Pamiętajmy, że takie rozwiązanie zadziała wówczas, gdy przekazywane obiekty wywoływalne będą miały takie same typy zwracane i typy parametrów. Są one "wymuszane" poprzez odpowiedni zapis wewnątrz nawiasów trójkątnych w szablonie std::function.

Warto jednak zauważyć, że wymuszenie typów nie oznacza, iż powinny być dokładnie takie same, jak te, które podano w szablonie std::function. Chodzi jedynie o to, by były konwertowane do takich typów. W przypadku powyższego przykładu można byłoby na przykład jako parametr przekazać następujące wyrażenie lambda:

auto l = [](const int aa, const long bb) -> long { return aa + bb; };

Jak widać, przyjmuje ono dwa parametry typu int i long, a zwraca typ long. Różni się więc od poprzedniego wyrażenia lambda, które przyjmowało char i int, a zwracało int.

W wywołaniu zadziała jednak w taki sam sposób i zwróci identyczny wynik, jak poprzednie wyrażenie lambda.

Można byłoby nawet jako parametru użyć typu bool i wszystko byłoby w porządku. Oto kolejna odmiana wyrażenia lambda działająca z szablonem std::function<int(char, int)>:

auto l2 = [](const bool aaa, const long bbb) -> int { return aaa ? bbb : bbb + 1; };

W wyniku uzyskujemy wartość 20 (równą parametrowi drugiemu), ponieważ parametr pierwszy typu char o wartości 10 jest rozwiązywany jako true.

Przykład z życia wzięty

Kiedyś tworzyłem program z wykorzystaniem biblioteki FLTK służącej do obsługi interfejsu graficznego. Biblioteka zawiera szereg klas odpowiadających kontrolkom graficznym. Jedną z nich jest Fl_Box pozwalająca wyświetlać prostokątny obszar o odpowiednim kolorze, ewentualnie wypełniony jakąś grafiką. Musiałem sprawdzać, czy na obszarze kontrolki użytkownik nie tylko przyciska, ale i zwalnia przycisk myszy. Trzeba było samodzielnie obsłużyć zdarzenie zwolnienia przycisku, ponieważ za pomocą standardowych funkcji nie dało się tego zrobić. Polegało to na zdefiniowaniu własnej procedury obsługi i przeciążeniu oryginalnej metody handle. Aby mieć możliwość podstawienia pod procedurę obsługi dowolnego obiektu wywoływalnego, użyłem szablonu std::function. Oto odpowiedni fragment kodu:

class GUIImageButton : public Fl_Box
{
public:
    virtual int handle(int event) override; // przeciążamy oryginalny handle() z Fl_Box

    // definicja własnej funkcji obsługi, wywoływanej przez handle() - przyjmuje dowolny obiekt wywoływalny
    void setHandle(const std::function<int(int, Fl_Widget*)>& f);

private:
    // własna funkcja wywoływana przez handle()
    std::function<int(int, Fl_Widget*)> handleFun;

Klasa GUIImageButton dziedziczy z oryginalnej klasy Fl_Box. Aby mieć możliwość obsługi określonego zdarzenia, przeciążamy metodę handle. W metodzie tej wywołamy własną procedurę obsługi, zapisaną w zmiennej prywatnej handleFun. Ustawienie własnej procedury obsługi jest możliwe dzięki metodzie setHandle, która jako parametr przyjmuje obiekt wywoływalny - może to być np. wskaźnik do funkcji czy obiekt funkcyjny (włącznie z wyrażeniem lambda).

A oto metody handle i setHandle:

int GUIImageButton::handle(int event)
{
    if (handleFun)
        return handleFun(event, this); // wywołaj własną funkcję obsługi
    else
        return 0;
}

void GUIImageButton::setHandle(const std::function<int(int, Fl_Widget*)>& f)
{
    handleFun = f;
}        

W głównym kodzie po prostu wywołujemy metodę setHandle z odpowiednim obiektem wywoływalnym - w tym przypadku użyłem wyrażenia lambda. Oto fragment kodu:

buttons[i]->setHandle([&](int e, Fl_Widget* w) -> int
{
    if (Fl::event_button() == FL_LEFT_MOUSE) // lewy przycisk myszy
    {
        if (e == FL_PUSH) // naciśnięcie
        {
            // jakiś kod...
        }
        else if (e == FL_RELEASE) // zwolnienie
        {
            // jakiś kod...
        }