Funkcja std::bind()
dostępna w języku C++ jest bardzo przydatnym narzędziem w przypadku, gdy musimy "obejść" wymóg użycia określonej liczby argumentów. W rzeczywistości funkcja ta jest opakowaniem obiektu wywoływalnego, którym może być obiekt funkcyjny, wskaźnik do funkcji, referencja do funkcji, wskaźnik do metody. Pozwala także zmieniać kolejność argumentów oraz częściowo zastosować obiekt wywoływalny.
Zacznijmy od najprostszego przypadku. Mamy funkcję funA
z trzema parametrami, natomiast na jej podstawie chcielibyśmy utworzyć funkcję zeroparametrową. Tak to należy zrobić za pomocą std::bind()
:
#include <functional>
#include <iostream>
int funA(const int par1, const int par2, const int par3)
{
std::cout << "Wewnątrz funA: par1 = " << par1 << ", par2 = " << par2 << ", par3 = " << par3 << std::endl;
return par1 + par2 + par3;
}
int main()
{
setlocale(LC_ALL, ""); // polskie litery w konsoli
auto funB = std::bind(funA, 10, 20, 30);
std::cout << "Przed użyciem funkcji funB" << std::endl;
std::cout << "Wynik funB: " << funB() << std::endl;
}
Aby użyć funkcji std::bind()
, należy dołączyć nagłówek <functional>
. Widzimy, że pierwszym parametrem std::bind()
jest nazwa funkcji "oryginalnej", a kolejnymi trzy argumenty. Oto uzyskane wyniki:
Przed użyciem funkcji funB
Wewnątrz funA: par1 = 10, par2 = 20, par3 = 30
Wynik funB: 60
Wynik jest zgodny z intuicją - pierwszy parametr std::bind()
staje się pierwszym parametrem funA
, drugi drugim, a trzeci trzecim.
Pamiętajmy o tym, że najpierw jedynie przypisaliśmy std::bind()
do zmiennej funB
. Przez to utworzyliśmy obiekt funkcyjny, który nie został wówczas jeszcze wykonany. Jego uruchomienie nastąpiło dopiero po użyciu tego obiektu z określonymi parametrami. Ta cecha jest związana także z innymi rodzajami wywołań std::bind()
, które zostaną omówione w dalszej części artykułu.
A teraz coś ciekawszego - na podstawie funA
utworzymy funkcję jednoparametrową. Pozostałe dwa parametry zostaną "zaszyte" w wywołaniu funkcji std::bind()
.
using namespace std::placeholders;
auto funC = std::bind(funA, _1, 200, 300);
std::cout << "Wynik funC: " << funC(100) << std::endl;
Zauważmy, że w kodzie pojawił się nowy element - użycie przestrzeni nazw std::placeholders
. Pozwala ona na zastosowanie tak zwanych symboli zastępczych, które odpowiadają argumentom przekazanym do docelowej funkcji utworzonej za pomocą std::bind()
. Symbole zastępcze mają nazwy _1
, _2
, _3
itd. (jeśli nie zadeklarowalibyśmy przestrzeni nazw std::placeholders
, musielibyśmy za każdym razem podawać ją przed danym symbolem zastępczym).
W kodzie widzimy, że symbol zastępczy został użyty jako pierwszy argument funkcji std::bind()
. Drugi i trzeci zostały zdefiniowane jako 200
i 300
. Wywołanie funkcji docelowej funC
jest proste: podajemy po prostu jedyny argument, który zostanie następnie przypisany do parametru _1
w funkcji std::bind()
.
Oto uzyskane wyniki:
Wewnątrz funA: par1 = 100, par2 = 200, par3 = 300
Wynik funC: 600
Jak widać, parametr 100
został zgodnie z oczekiwaniami użyty jako argument _1
w funkcji std::bind()
.
Równie dobrze możemy parametrem _1
zastąpić inny argument. Wystarczy w funkcji std::bind()
umieścić symbol zastępczy w odpowiednim miejscu:
auto funD = std::bind(funA, 1000, _1, 3000);
std::cout << "Wynik funD: " << funD(2000) << std::endl;
W powyższym kodzie zastąpiliśmy drugi argument. Pierwszy i trzeci zostały predefiniowane. Oto uzyskane wyniki:
Wewnątrz funA: par1 = 1000, par2 = 2000, par3 = 3000
Wynik funD: 6000
Zastąpmy teraz dwa argumenty. W tym celu użyjemy symboli zastępczych _1
i _2
.
auto funE = std::bind(funA, 5, _1, _2);
std::cout << "Wynik funF: " << funE(7, 9) << std::endl;
Pierwszy argument funkcji funE
, czyli 7
, odpowiada symbolowi zastępczemu _1
, natomiast drugi (równy 9
), odpowiada symbolowi zastępczemu _2
. Oto otrzymane wyniki:
Wewnątrz funA: par1 = 5, par2 = 7, par3 = 9
Wynik funE: 21
Za pomocą funkcji std::bind()
możemy także zmieniać kolejność argumentów. Rozważmy funkcję funF
, której dwa parametry są identyczne, jak we wcześniejszym fragmencie kodu, lecz zostaną zamienione miejscami w funkcji std::bind()
:
auto funF = std::bind(funA, 5, _2, _1);
std::cout << "Wynik funF: " << funF(7, 9) << std::endl;
Jak widać, pierwszy parametr funkcji funF
(czyli symbol zastępczy _1
) stał się ostatnim parametrem funkcji std::bind()
, natomiast drugi parametr funkcji funF
(czyli symbol zastępczy _2
) stał się przedostatnim parametrem funkcji std::bind()
. Oto uzyskane wyniki:
Wewnątrz funA: par1 = 5, par2 = 9, par3 = 7
Wynik funF: 21
Rezultat otrzymany po wywołaniu funkcji funF
jest oczywiście taki sam, jak w przypadku funkcji funE
(co wynika z prawa przemienności dodawania), jednak widzimy, że parametrem par2
jest obecnie wartość 9
, a parametrem par3
jest 7
.
Powyższe rozważania są dość teoretyczne. Jak w takim razie moglibyśmy zastosować funkcję std::bind()
w praktyce?
Czasami mamy do czynienia z sytuacją, gdy formalnie jakaś procedura obsługi (handler) wymaga określonej liczby parametrów, a chcielibyśmy przekazać ich więcej, ponieważ są potrzebne do wykonania pewnych działań. Taka okoliczność ma miejsce na przykład w przypadku wykorzystywania biblioteki boost::asio służącej do obsługi komunikacji sieciowej. Metoda taka jak socket::async_write_some
wymaga podania procedury obsługi, która zostanie wywołana w momencie, gdy bieżąca operacja zapisu się zakończy. Oczekiwana sygnatura tej procedury jest następująca:
void handler(
const boost::system::error_code& error,
std::size_t bytesTransferred
);
Jak widać, procedura wykorzystuje tylko dwa parametry. Załóżmy, że do tej procedury obsługi chcielibyśmy również przekazać kontekst klasy zawierającej łańcuch z treścią, która jest wysyłana, liczbę wysłanych znaków do tej pory, a także obiekt gniazda - inaczej mówiąc, obiekt sesji. Jak to zrobić? Oto przykładowe rozwiązanie:
class connectionSession // klasa sesji
{
public:
size_t dataWritten; // liczba bajtów zapisanych do tej pory
std::string strBuffer; // dane do wysłania
std::shared_ptr<boost::asio::ip::tcp::socket> socket; // gniazdo połączenia
// ... poniżej inne niezbędne pola
};
// procedura obsługi
void writeHandler(const boost::system::error_code& error, const std::size_t bytesTransferred, std::shared_ptr<connectionSession> session)
{
// gdzieś w kodzie procedury obsługi używamy trzeciego parametru, czyli session
session->dataWritten += bytesTransferred; // sumaryczna liczba bajtów wysłana do tej pory
}
// ...
// gdzieś w głównym kodzie programu wywołujemy metodę socket::async_write_some
connectionSession->socket->async_write_some(asio::buffer(socket->strBuffer), std::bind(writeHandler, _1,_2, connectionSession));
W powyższym kodzie pojawiają się już znane nam konstrukcje - symbole zastępcze. Dwa wymagane argumenty, czyli _1
i _2
spełniają wymagania sygnatury oryginalnej procedury obsługi handler
, a trzeci dodatkowy (connectionSession
) jest przekazywany do naszej funkcji writeHandler
dzięki std::bind()
.
W tym przypadku procedura obsługi była zwykłą funkcją. Co jednak uczynić w sytuacji, gdybyśmy chcieli jako procedurę obsługi wykorzystać metodę klasy? Jak wiadomo, do metody jako pierwszy argument jest zawsze niejawnie przekazywany wskaźnik do klasy, więc takie rozwiązanie jak powyższe po prostu nie zadziała.
Załóżmy, że rozbudowaliśmy naszą klasę sesji o procedurę obsługi, która w tym momencie jest metodą o nazwie writeHandler
. Dzięki temu mogliśmy po pierwsze uczynić pola prywatnymi, a po drugie utworzyć oddzielną metodę asyncWrite
, która jest wywoływana podczas rozpoczęcia wysyłania danych i sama wywołuje metodę async_write_some()
wskazującą procedurę obsługi w postaci wspomianej metody klasy.
class connectionSession : public std::enable_shared_from_this<connectionSession> // klasa sesji
{
private:
size_t dataWritten; // liczba bajtów zapisanych do tej pory
std::string strBuffer; // dane do wysłania
std::shared_ptr<boost::asio::ip::tcp::socket> socket; // gniazdo połączenia
// ... poniżej inne niezbędne pola
// metoda rozpoczynająca transmisję danych
void asyncWrite(void)
{
socket->async_write_some(boost::asio::buffer(strBuffer), std::bind(&connectionSession::writeHandler, shared_from_this(), _1, _2));
}
// procedura obsługi
void writeHandler(const boost::system::error_code& error, const std::size_t bytesTransferred)
{
// różne działania
}
};
Pewnego wyjaśnienia wymaga element std::enable_shared_from_this
. Jest to klasa, która pozwala na uzyskanie dodatkowego, poprawnego wskaźnika std::shared_ptr
do danego obiektu (czyli do this
), który już jest obsługiwany przez std::shared_ptr
. Taki wskaźnik otrzymuje się automatycznie poprzez użycie metody shared_from_this()
. Rozwiązanie to jest wymagane na przykład w przypadkach, gdy obiekt sesji musi istnieć cały czas aż do momentu wysłania wszystkich pakietów danych.
Jak widzimy, funkcja std::bind()
wywoływana podczas wykonania metody async_write_some
jako pierwszy argument przekazuje adres metody connectionSession::writeHandler
będącej procedurą obsługi (callbackiem), natomiast drugim parametrem jest adres obiektu sesji. Dzięki temu procedura obsługi jest poprawnie wywoływana w trakcie działania programu, ponieważ jako pierwszy argument zawsze otrzymuje wskaźnik do swojego obiektu. Pozostałe dwa parametry są standardowe i odpowiadają parametrom error
i bytesTransferred
.