Używanie koncepcji C ++ 20, aby uniknąć std :: function
W przeszłości, gdy potrzebowałem wywołania zwrotnego jako parametru funkcji, zwykle decydowałem się na użycie std::function. W rzadkich przypadkach, w których zdecydowanie nigdy nie używam przechwytywania, typedefzamiast tego użyłem jako deklaracji funkcji.
Tak więc zazwyczaj moja deklaracja z parametrem wywołania zwrotnego wygląda mniej więcej tak:
struct Socket
{
void on_receive(std::function<void(uint8_t*, unsigned long)> cb);
}
Jednak, o ile wiem, w std::functionrzeczywistości wykonuje trochę pracy w czasie wykonywania z powodu konieczności rozwiązania lambda z jej przechwytywaniami do std::functionszablonu i przeniesienia / skopiowania jej przechwyceń (?).
Czytając o nowych funkcjach C ++ 20, doszedłem do wniosku, że być może będę w stanie wykorzystać koncepcje, aby uniknąć używania std::functioni używać ograniczonych parametrów dla dowolnego realnego funktora.
I tu pojawia się mój problem: ponieważ chcę kiedyś w przyszłości pracować z obiektami funktora wywołania zwrotnego, muszę je przechowywać. Ponieważ nie mam określonego typu dla mojego wywołania zwrotnego, moją początkową myślą było skopiowanie (ewentualnie przesunięcie w pewnym momencie) funktora do sterty i użycie a std::vector<void*>do zapisania, gdzie je zostawiłem.
template<typename Functor>
concept ReceiveCallback = std::is_invocable_v<Functor, uint8_t*, unsigned long>
&& std::is_same_v<typename std::invoke_result<Functor, uint8_t*, unsigned long>::type, void>
&& std::is_copy_constructible_v<Functor>;
struct Socket
{
std::vector<void*> callbacks;
template<ReceiveCallback TCallback>
void on_receive(TCallback const& callback)
{
callbacks.push_back(new TCallback(callback));
}
}
int main(int argc, char** argv)
{
Socket* sock;
// [...] inialize socket somehow
sock->on_receive([](uint8_t* data, unsigned long length)
{
// NOP for now
});
// [...]
}
Chociaż działa to wystarczająco dobrze, implementując metodę, która ma wywoływać funktor, zauważyłem, że właśnie odłożyłem kwestię nieznanego / brakującego typu. O ile rozumiem, rzutowanie a void*do wskaźnika funkcji lub podobnego hackowania powinno dać UB - Skąd kompilator wiedziałby, że faktycznie próbuję wywołać operator () klasy, która jest całkowicie nieznana?
Myślałem o zapisaniu (skopiowanego) funktora wraz ze wskaźnikiem funkcji do jego operator()definicji, jednak nie mam pojęcia, jak mógłbym wstawić funktor jako thiswewnątrz funkcji i bez tego wątpię, że przechwytywania zadziałają.
Innym podejściem, które miałem, było zadeklarowanie czystego interfejsu wirtualnego, który deklaruje wymaganą operator()funkcję. Niestety mój kompilator zabronił mi rzutowania mojego funktora na interfejs i nie sądzę, że istnieje legalny sposób, aby pozwolić, aby lambda z niego pochodziła.
Czy jest więc sposób, aby to rozwiązać, czy też po prostu nadużywam funkcji wymagań / koncepcji szablonu?
Odpowiedzi
Twoja pierwotna wersja została użyta std::functionwłaśnie dlatego, że usuwa typ. Jeśli chcesz wymazać typy (i oczywiście to robisz, ponieważ chcesz, aby użytkownik mógł używać dowolnego typu bez twojego kodu, który wyraźnie wie, co to jest), potrzebujesz jakiejś formy wymazywania typu. Wymazywanie czcionek nie jest darmowe.
Ograniczenia dotyczą szablonów. Nie chcesz funkcji szablonu; potrzebujesz jednej funkcji, która zajmuje się wywoływalnym wymazywaniem typu.
W przypadku callbacków, które muszą przeżyć stos wywołań dostawcy, std::functionnarzut jest prawie tym, czego potrzebujesz. Oznacza to, że „koszty ogólne” nie są bezcelowe; to właśnie pozwala na przechowywanie obiektów dowolnego, nieznanego typu w procesorze wywołań zwrotnych.