Adapter obiektu wywoływalnego zmienia kształt funkcji. Może związać argumenty, zamienić metodę składową w wolny obiekt wywoływalny, zanegować predykat albo ukryć typ obiektu za stałą sygnaturą. Celem jest składnia wywołania, której oczekuje klient. Adaptowanym obiektem jest oryginalna funkcja. Adapterem jest obiekt albo funkcja, która łączy obie strony.
std::bind
std::bind tworzy nowy obiekt wywoływalny, w którym część argumentów jest ustalona, a resztę zostawiasz jako placeholdery.
#include <functional>
#include <iostream>
int main() {
auto add = [](int a, int b) { return a + b; };
auto add_five = std::bind(add, 5, std::placeholders::_1);
std::cout << add_five(10) << '\n'; // 15
}
Adapter przechowuje add oraz wartość 5. Wywołanie add_five(10) uruchamia add(5, 10).
std::bind potrafi też przestawiać argumenty i wiązać metody składowe. Ta elastyczność sprawia, że starszy kod C++ używa go często. W nowoczesnym C++ zwykle lepsza jest lambda:
auto add_five = [add](int value) { return add(5, value); };
Lambda mówi, co przechwytuje, czyta się w kolejności i daje lepsze komunikaty błędów. Zostaw std::bind w kodzie, który już go używa, albo w kodzie generycznym, gdzie lista wiązanych argumentów powstaje przy kompilacji.
Argumenty są przechowywane przez wartość. Jeśli potrzebujesz referencji, opakuj ją w std::ref.
std::mem_fn
std::mem_fn zamienia wskaźnik do składowej w obiekt wywoływalny, który przyjmuje obiekt, referencję albo wskaźnik.
#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
struct Widget {
bool visible() const { return is_visible; }
bool is_visible = false;
};
int main() {
std::vector<Widget> widgets{{true}, {false}, {true}};
auto visible = std::mem_fn(&Widget::visible);
auto count = std::count_if(widgets.begin(), widgets.end(), visible);
std::cout << count << '\n'; // 2
}
To adapter do sytuacji, w której algorytm oczekuje predykatu, a dane wystawiają metodę składową. Nie potrzebujesz lambdy, która tylko wywołuje jeden getter.
std::not_fn
std::not_fn opakowuje predykat i neguje wynik.
#include <algorithm>
#include <functional>
#include <vector>
int main() {
auto is_even = [](int value) { return value % 2 == 0; };
auto is_odd = std::not_fn(is_even);
std::vector<int> values{1, 2, 3, 4, 5};
auto count = std::count_if(values.begin(), values.end(), is_odd);
// count == 3
}
Przed C++17 trzeba było użyć std::not1 albo std::not2 i odpowiedniej klasy bazowej std::unary_function. std::not_fn działa z lambdami, obiektami funkcji i wskaźnikami do składowych bez tego rusztowania.
std::not_fn przechowuje kopię obiektu wywoływalnego. Jeśli oryginał ma stan zmienny, kopia ma własny stan.
std::reference_wrapper
std::reference_wrapper<T> to kopiowalna wartość, która odwołuje się do obiektu. std::ref i std::cref tworzą takie opakowanie. To adapter dla kontenerów i algorytmów, które kopiują elementy, ale nie powinny kopiować samego obiektu.
#include <functional>
#include <iostream>
#include <vector>
int main() {
int a = 1;
int b = 2;
std::vector<std::reference_wrapper<int>> values{std::ref(a), std::ref(b)};
values[0].get() = 10;
std::cout << a << '\n'; // 10
}
Używaj go, gdy kopia byłaby błędna albo kosztowna. Nie używaj go, żeby uniknąć pytania o własność. Obiekt, do którego prowadzi referencja, nadal musi żyć dłużej niż opakowanie.
std::function
std::function<R(Args...)> to obiekt wywoływalny z wymazanym typem i stałą sygnaturą. Może przechowywać wskaźnik do funkcji, lambdę, związaną metodę składową albo obiekt funkcji.
#include <functional>
#include <iostream>
int main() {
auto add = [](int a, int b) { return a + b; };
std::function<int(int)> add_five = [add](int value) {
return add(5, value);
};
std::cout << add_five(10) << '\n'; // 15
}
Taki adapter przydaje się na granicy API, gdzie wywołujący nie powinien się zastanawiać, jaki rodzaj obiektu wywoływalnego przekazuje. Kosztuje jedno pośrednictwo i może alokować, gdy obiekt nie mieści się w buforze małych obiektów. W ciasnej pętli parametr szablonu albo zwykły wskaźnik do funkcji będzie tańszy.
std::invoke
std::invoke to funkcja adaptująca, nie opakowanie obiektu. Daje jedną składnię wywołania dla wskaźników do funkcji, wskaźników do metod, wskaźników do danych i zwykłych obiektów wywoływalnych.
#include <functional>
#include <iostream>
struct Widget {
void draw() const { std::cout << "draw\n"; }
};
int main() {
Widget widget;
std::invoke(&Widget::draw, widget);
}
Kod generyczny używa jej, żeby nie obsługiwać wskaźników do składowych osobnym przypadkiem.
Tabela adapterów
| Adapter | Adaptuje | Do czego |
|---|---|---|
std::bind |
obiekt wywoływalny i związane argumenty | obiekt wywoływalny z pozostałymi argumentami |
std::mem_fn |
wskaźnik do składowej | obiekt wywoływalny przyjmujący obiekt albo wskaźnik |
std::not_fn |
predykat | zanegowany predykat |
std::reference_wrapper |
referencję | kopiowalna wartość, która nadal odwołuje się do obiektu |
std::function |
dowolny obiekt wywoływalny z pasującą sygnaturą | jeden typ obiektu wywoływalnego z wymazanym typem |
std::invoke |
wskaźnik do składowej albo obiekt wywoływalny | jednolite wyrażenie wywołania |
Kiedy ich używać
Używaj adaptera obiektu wywoływalnego, gdy algorytm albo API oczekuje innego kształtu wywołania niż ten, który masz. std::mem_fn jest czystym wyborem dla getterów składowych. std::not_fn jest czystym wyborem dla przeciwnego predykatu. std::function należy na granicę, gdzie konkretny typ obiektu wywoływalnego nie powinien przeciekać do interfejsu. std::bind jest w większości spadkiem po starszym C++, a lambda zwykle czyta się lepiej.
Unikaj adapterów, które ukrywają własność albo czas życia. std::function przechwytujący referencję, std::reference_wrapper trzymany dłużej niż obiekt albo std::bind przechowujący wskaźnik skompilują się bez problemu i zawiodą później. Adapter zmienia kształt wywołania, nie zasady czasu życia.
Podsumowanie
Adaptery obiektów wywoływalnych sprawiają, że jedna funkcja wygląda jak inna. Część z nich to obiekty, na przykład std::bind, std::mem_fn i std::function. Część to funkcje, na przykład std::invoke. Są małe, można je składać i łatwo ich nadużyć. Sięgaj po ten, który wprost mówi, o co chodzi, i wybierz lambdę, gdy adapter tylko zaciemnia intencję.