Czy powinienem używać funkcji std :: czy wskaźnika funkcji w C ++?

142

Podczas implementacji funkcji zwrotnej w C ++, czy nadal powinienem używać wskaźnika funkcji w stylu C:

void (*callbackFunc)(int);

Czy powinienem użyć std :: function:

std::function< void(int) > callbackFunc;
Jan Swart
źródło
9
Jeśli funkcja wywołania zwrotnego jest znana w czasie kompilacji, zamiast tego rozważ szablon.
Baum mit Augen
4
Przy realizacji funkcji zwrotnej należy zrobić cokolwiek rozmówca wymaga. Jeśli twoje pytanie naprawdę dotyczy projektowania interfejsu wywołań zwrotnych, nie ma tu wystarczająco dużo informacji, aby na nie odpowiedzieć. Czego oczekujesz od odbiorcy Twojego oddzwonienia? Jakie informacje musisz przekazać odbiorcy? Jakie informacje odbiorca powinien Ci przekazać w wyniku połączenia?
Pete Becker

Odpowiedzi:

171

Krótko mówiąc, używaj,std::function chyba że masz powód, aby tego nie robić.

Wskaźniki funkcji mają tę wadę, że nie są w stanie uchwycić kontekstu. Nie będziesz mógł na przykład przekazać funkcji lambda jako funkcji zwrotnej, która przechwytuje niektóre zmienne kontekstowe (ale zadziała, jeśli nie przechwyci żadnych). Wywołanie zmiennej thisskładowej obiektu ( tj. Niestatycznej) również nie jest zatem możliwe, ponieważ obiekt ( -pointer) musi zostać przechwycony. (1)

std::function(od C ++ 11) służy przede wszystkim do przechowywania funkcji (przekazywanie jej nie wymaga przechowywania). Dlatego jeśli chcesz przechowywać wywołanie zwrotne na przykład w zmiennej składowej, prawdopodobnie jest to najlepszy wybór. Ale także jeśli go nie przechowujesz, jest to dobry „pierwszy wybór”, chociaż ma tę wadę, że wprowadza pewne (bardzo małe) narzuty podczas wywoływania (więc w bardzo krytycznej sytuacji może to być problem, ale w większości nie powinno). Jest bardzo „uniwersalny”: jeśli bardzo zależy Ci na spójnym i czytelnym kodzie, a także nie chcesz myśleć o każdym wyborze, którego dokonujesz (tj. Chcesz, aby był prosty), używaj go std::functiondla każdej funkcji, którą przekazujesz.

Pomyśl o trzeciej opcji: jeśli masz zamiar zaimplementować małą funkcję, która następnie zgłasza coś przez dostarczoną funkcję zwrotną, rozważ parametr szablonu , który może być dowolnym wywoływalnym obiektem , tj. Wskaźnikiem funkcji, funktorem, lambdą, a std::function, ... Wadą jest to, że twoja (zewnętrzna) funkcja staje się szablonem i dlatego musi być zaimplementowana w nagłówku. Z drugiej strony masz tę zaletę, że wywołanie zwrotne może być wbudowane, ponieważ kod klienta Twojej (zewnętrznej) funkcji „widzi” wywołanie wywołania zwrotnego będzie zawierał dokładną informację o typie.

Przykład wersji z parametrem szablonu (napisz &zamiast &&przed C ++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

Jak widać w poniższej tabeli, wszystkie mają swoje zalety i wady:

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1) Istnieją sposoby obejścia tego ograniczenia, na przykład przekazanie dodatkowych danych jako dalszych parametrów do (zewnętrznej) funkcji: myFunction(..., callback, data)wywoła callback(data). To jest "wywołanie zwrotne z argumentami" w stylu C, które jest możliwe w C ++ (a przy okazji bardzo często używane w WIN32 API), ale powinno się go unikać, ponieważ mamy lepsze opcje w C ++.

(2) Chyba że mówimy o szablonie klasy, tj. Klasa, w której przechowujesz funkcję, jest szablonem. Ale to oznaczałoby, że po stronie klienta typ funkcji decyduje o typie obiektu, który przechowuje wywołanie zwrotne, co prawie nigdy nie jest opcją w rzeczywistych przypadkach użycia.

(3) W przypadku wersji przed C ++ 11 użyj boost::function

leemes
źródło
9
wskaźniki funkcji mają narzut wywołania w porównaniu z parametrami szablonu. Parametry szablonu sprawiają, że wstawianie jest łatwe, nawet jeśli przechodzisz w dół o piętnaście poziomów, ponieważ wykonywany kod jest opisywany przez typ parametru, a nie wartość. Obiekty funkcji szablonu przechowywane w typach zwracanych przez szablony są typowym i użytecznym wzorcem (z dobrym konstruktorem kopiującym można utworzyć wydajną wywoływalną funkcję szablonu, którą można przekonwertować na funkcję std::functionz usuniętym typem, jeśli trzeba ją natychmiast przechowywać poza zwany kontekstem).
Yakk - Adam Nevraumont
1
@tohecz Wspominam teraz, czy wymaga C ++ 11, czy nie.
leemes
1
@Yakk Och, oczywiście, zapomniałem o tym! Dodałem, dzięki.
leemes
1
@MooingDuck Oczywiście zależy to od implementacji. Ale jeśli dobrze pamiętam, to ze względu na to, jak działa wymazywanie typów, zachodzi jeszcze jedno pośrednictwo? Ale teraz, kiedy myślę o tym ponownie, wydaje mi się, że tak nie jest, jeśli przypiszesz do niego wskaźniki funkcji lub
lambdy bez
1
@leemes: Tak, dla wskaźników funkcji lub lambd bez przechwytywania, powinno mieć taki sam narzut jak c-func-ptr. Co nadal jest przeciągnięciem rurociągu + nie trywialnie wbudowanym.
Mooing Duck
25

void (*callbackFunc)(int); może być funkcją zwrotną w stylu C, ale jest okropnie bezużyteczna o kiepskim projekcie.

Wygląda dobrze zaprojektowany callback w stylu C void (*callbackFunc)(void*, int);- ma on void*pozwolić kodowi wykonującemu wywołanie zwrotne na utrzymanie stanu poza funkcją. Nie wykonanie tego zmusza dzwoniącego do globalnego przechowywania stanu, co jest niegrzeczne.

std::function< int(int) >int(*)(void*, int)w większości implementacji jest nieco droższe niż wywołanie. Jednak niektórym kompilatorom trudniej jest wbudować. Istnieją std::functionimplementacje klonów, które rywalizują z narzutami wywołania wskaźnika funkcji (zobacz „najszybsi możliwe delegaci” itp.), Które mogą przedostać się do bibliotek.

Teraz klienci systemu wywołań zwrotnych często muszą konfigurować zasoby i pozbywać się ich, gdy wywołanie zwrotne jest tworzone i usuwane, a także mieć świadomość okresu istnienia wywołania zwrotnego. void(*callback)(void*, int)nie zapewnia tego.

Czasami jest to możliwe poprzez strukturę kodu (wywołanie zwrotne ma ograniczony czas życia) lub przez inne mechanizmy (wyrejestrowanie wywołań zwrotnych i tym podobne).

std::function zapewnia środki do ograniczonego zarządzania przez cały okres użytkowania (ostatnia kopia obiektu znika, gdy zostaje zapomniana).

Ogólnie użyłbym znaku, std::functionchyba że wydajność dotyczy manifestu. Gdyby tak było, najpierw szukałbym zmian strukturalnych (zamiast wywołania zwrotnego na piksel, co powiesz na wygenerowanie procesora linii skanowania w oparciu o przekazaną mi lambdę? Co powinno wystarczyć, aby zmniejszyć obciążenie wywołań funkcji do trywialnych poziomów. ). Następnie, jeśli problem będzie się powtarzał, zapisywałbym na delegatepodstawie najszybszych możliwych delegatów i sprawdzał, czy problem z wydajnością zniknie.

Przeważnie używałbym wskaźników funkcji tylko dla starszych interfejsów API lub do tworzenia interfejsów C do komunikacji między różnymi generowanymi kodami kompilatorów. Używałem ich również jako wewnętrznych szczegółów implementacji, kiedy implementuję tabele skoków, wymazywanie typów itp.: Kiedy tworzę i konsumuję je, i nie udostępniam ich zewnętrznie do użycia przez jakikolwiek kod klienta, a wskaźniki funkcji robią wszystko, czego potrzebuję .

Zauważ, że możesz napisać otoki, które zamieniają się std::function<int(int)>w int(void*,int)wywołanie zwrotne stylu, zakładając, że istnieje odpowiednia infrastruktura zarządzania okresem życia wywołania zwrotnego. Więc jako test dymny dla dowolnego systemu zarządzania czasem życia wywołań zwrotnych w stylu C, upewniłbym się, że opakowanie std::functiondziała w miarę dobrze.

Yakk - Adam Nevraumont
źródło
1
Skąd to się void*wzięło? Dlaczego miałbyś chcieć utrzymywać stan poza funkcją? Funkcja powinna zawierać cały kod, którego potrzebuje, całą funkcjonalność, wystarczy przekazać jej żądane argumenty, zmodyfikować i coś zwrócić. Jeśli potrzebujesz jakiegoś stanu zewnętrznego, dlaczego funkcja functionPtr lub callback miałaby przenosić ten bagaż? Myślę, że oddzwonienie jest niepotrzebnie skomplikowane.
Nikos
@ nik-lz Nie jestem pewien, w jaki sposób mógłbym nauczyć Cię użycia i historii wywołań zwrotnych w języku C w komentarzu. Lub filozofia proceduralna w przeciwieństwie do programowania funkcjonalnego. Więc zostawisz niewypełniony.
Yakk - Adam Nevraumont
Zapomniałem this. Czy to dlatego, że trzeba wziąć pod uwagę przypadek wywołania funkcji składowej, więc potrzebujemy thiswskaźnika, aby wskazywał na adres obiektu? Jeśli się mylę, czy mógłbyś podać mi link, gdzie mogę znaleźć więcej informacji na ten temat, ponieważ nie mogę znaleźć wiele na ten temat. Z góry dziękuję.
Nikos,
Funkcje członkowskie @ Nik-Lz nie są funkcjami. Funkcje nie mają stanu (runtime). Wywołania zwrotne mają void*zezwolenie na transmisję stanu środowiska wykonawczego. Wskaźnik funkcji z argumentem void*i void*argumentem może emulować wywołanie funkcji składowej do obiektu. Przepraszam, nie znam zasobu przechodzącego przez „projektowanie mechanizmów wywołania zwrotnego C 101”.
Yakk - Adam Nevraumont
Tak, właśnie o tym mówiłem. Stan środowiska wykonawczego to w zasadzie adres wywoływanego obiektu (ponieważ zmienia się on między uruchomieniami). Nadal chodzi this. O to mi chodziło. W każdym razie dzięki.
Nikos
17

Służy std::functiondo przechowywania dowolnych wywoływalnych obiektów. Pozwala użytkownikowi podać dowolny kontekst potrzebny do wywołania zwrotnego; zwykły wskaźnik funkcji nie.

Jeśli z jakiegoś powodu musisz użyć zwykłych wskaźników do funkcji (być może dlatego, że potrzebujesz interfejsu API zgodnego z C), powinieneś dodać void * user_contextargument, aby był przynajmniej możliwy (aczkolwiek niewygodny), aby uzyskać dostęp do stanu, który nie jest bezpośrednio przekazywany do funkcjonować.

Mike Seymour
źródło
Jaki jest typ p tutaj? czy będzie to typ std :: function? void f () {}; auto p = f; p ();
3
14

Jedynym powodem, dla którego należy unikać, std::functionjest obsługa starszych kompilatorów, które nie obsługują tego szablonu, który został wprowadzony w C ++ 11.

Jeśli obsługa języka pre-C ++ 11 nie jest wymagana, użycie std::functiondaje wywołującym większy wybór w implementacji wywołania zwrotnego, co czyni go lepszą opcją w porównaniu do „zwykłych” wskaźników funkcji. Daje użytkownikom twojego API większy wybór, jednocześnie wyodrębniając specyfikę ich implementacji dla twojego kodu, który wykonuje wywołanie zwrotne.

dasblinkenlight
źródło
1

std::function może w niektórych przypadkach wprowadzić VMT do kodu, co ma pewien wpływ na wydajność.

vladon
źródło
3
Czy możesz wyjaśnić, czym jest ten VMT?
Gupta