Chciałbym uzyskać informacje o tym, jak poprawnie myśleć o domknięciach std::function
w C ++ 11 oraz o tym, jak są one implementowane i jak obsługiwana jest pamięć.
Chociaż nie wierzę w przedwczesną optymalizację, mam zwyczaj uważnego rozważania wpływu moich wyborów na wydajność podczas pisania nowego kodu. Zajmuję się również sporą ilością programowania w czasie rzeczywistym, np. Na mikrokontrolerach i systemach audio, gdzie należy unikać niedeterministycznych przerw w przydzielaniu / zwalnianiu pamięci.
Dlatego chciałbym lepiej zrozumieć, kiedy używać lambd języka C ++, a kiedy nie.
Obecnie rozumiem, że lambda bez przechwyconego zamknięcia jest dokładnie taka sama, jak wywołanie zwrotne w C. Jednak gdy środowisko jest przechwytywane przez wartość lub przez odniesienie, na stosie jest tworzony anonimowy obiekt. Kiedy zamknięcie wartości musi zostać zwrócone z funkcji, należy je opakować std::function
. Co dzieje się z pamięcią zamknięcia w tym przypadku? Czy jest kopiowany ze stosu na stos? Czy jest uwalniany za każdym razem, gdy std::function
zostanie uwolniony, tj. Czy jest liczony jako odniesienie std::shared_ptr
?
Wyobrażam sobie, że w systemie czasu rzeczywistego mógłbym ustawić łańcuch funkcji lambda, przekazując B jako argument kontynuacji do A, aby utworzyć potok przetwarzania A->B
. W takim przypadku zamknięcia A i B zostaną przydzielone raz. Chociaż nie jestem pewien, czy zostaną one przydzielone na stosie, czy na stercie. Jednak ogólnie wydaje się to bezpieczne w użyciu w systemie czasu rzeczywistego. Z drugiej strony, jeśli B konstruuje jakąś funkcję lambda C, którą zwraca, wówczas pamięć dla C byłaby wielokrotnie przydzielana i zwalniana, co byłoby nie do przyjęcia w przypadku użycia w czasie rzeczywistym.
W pseudokodzie, pętla DSP, która moim zdaniem będzie bezpieczna w czasie rzeczywistym. Chcę wykonać blok przetwarzania A, a następnie B, gdzie A wywołuje swój argument. Obie te funkcje zwracają std::function
obiekty, więc f
będzie to std::function
obiekt, którego środowisko jest przechowywane na stercie:
auto f = A(B); // A returns a function which calls B
// Memory for the function returned by A is on the heap?
// Note that A and B may maintain a state
// via mutable value-closure!
for (t=0; t<1000; t++) {
y = f(t)
}
I myślę, że może być zły w kodzie czasu rzeczywistego:
for (t=0; t<1000; t++) {
y = A(B)(t);
}
I taki, w którym myślę, że pamięć stosu jest prawdopodobnie używana do zamknięcia:
freq = 220;
A = 2;
for (t=0; t<1000; t++) {
y = [=](int t){ return sin(t*freq)*A; }
}
W tym drugim przypadku zamknięcie jest konstruowane przy każdej iteracji pętli, ale w przeciwieństwie do poprzedniego przykładu jest tanie, ponieważ jest podobne do wywołania funkcji, nie są dokonywane alokacje sterty. Co więcej, zastanawiam się, czy kompilator mógłby „podnieść” zamknięcie i wprowadzić optymalizacje wbudowane.
Czy to jest poprawne? Dziękuję Ci.
operator()
. Nie ma żadnego „podnoszenia” do wykonania, lambdy nie są niczym specjalnym. Są tylko krótką ręką dla obiektu funkcji lokalnej.std::function
przechowuje swój stan na stercie, czy nie, i nie ma nic wspólnego z lambdami. Czy to prawda?std::function
!!auto
zwracanym typie.Odpowiedzi:
Nie; jest to zawsze obiekt C ++ o nieznanym typie, utworzony na stosie. Lambda bez przechwytywania może zostać przekonwertowana na wskaźnik funkcji (chociaż to, czy jest odpowiednia dla konwencji wywoływania języka C, zależy od implementacji), ale to nie znaczy, że jest wskaźnikiem funkcji.
Lambda nie jest niczym specjalnym w C ++ 11. To obiekt jak każdy inny przedmiot. Wyrażenie lambda daje w wyniku tymczasowy, którego można użyć do zainicjowania zmiennej na stosie:
auto lamb = []() {return 5;};
lamb
jest obiektem stosu. Posiada konstruktora i destruktora. I będzie przestrzegać wszystkich reguł C ++. Typlamb
będzie zawierał przechwycone wartości / odniesienia; będą członkami tego obiektu, tak jak wszystkie inne składowe obiektu dowolnego innego typu.Możesz go przekazać
std::function
:auto func_lamb = std::function<int()>(lamb);
W takim przypadku otrzyma kopię wartości
lamb
. Gdybylamb
uchwycić cokolwiek według wartości, powstałyby dwie kopie tych wartości; jeden wlamb
i jeden wfunc_lamb
.Po zakończeniu bieżącego zakresu
func_lamb
zostanie zniszczony, a po nim nastąpilamb
, zgodnie z zasadami czyszczenia zmiennych stosu.Możesz równie łatwo przydzielić jeden na stercie:
auto func_lamb_ptr = new std::function<int()>(lamb);
Dokładnie miejsce, w którym pamięć na zawartość
std::function
przechodzi, jest zależne od implementacji, ale wymazywanie typu używane przezstd::function
zazwyczaj wymaga co najmniej jednej alokacji pamięci. Dlategostd::function
konstruktor może przyjąć alokator.std::function
przechowuje kopię swojej zawartości. Podobnie jak praktycznie każdy typ biblioteki standardowej C ++,function
używa semantyki wartości . Dlatego można go skopiować; kiedy jest kopiowany, nowyfunction
obiekt jest całkowicie oddzielny. Jest również przenośny, więc wszelkie wewnętrzne przydziały mogą być odpowiednio przenoszone bez potrzeby dalszego przydzielania i kopiowania.Dlatego nie ma potrzeby liczenia referencji.
Wszystko inne, co podasz, jest poprawne, zakładając, że „alokacja pamięci” oznacza „złe użycie w kodzie czasu rzeczywistego”.
źródło
std::function
jest punktem, w którym pamięć jest przydzielana i kopiowana. Wydaje się, że nie ma możliwości zwrócenia zamknięcia (ponieważ są one przydzielone na stosie) bez uprzedniego skopiowania do astd::function
, tak?std::function
obiekcie bez pamięci dynamicznej alokacja trwa.function
ma konstruktor przenoszenia noexcept. Cały sens powiedzenia „ogólnie wymaga” polega na tym, że nie mówię „ zawsze wymaga”: że istnieją okoliczności, w których nie zostanie dokonana alokacja.C ++ lambda jest po prostu lukrem składniowym wokół (anonimowej) klasy Functor z przeciążeniem
operator()
istd::function
jest po prostu opakowaniem wokół wywołań (tj. Funktorów, lambd, funkcji c, ...), które kopiuje według wartości "stały obiekt lambda" z bieżącej zakres stosu - do sterty .Aby sprawdzić liczbę rzeczywistych konstruktorów / relokatów, wykonałem test (używając innego poziomu zawijania do shared_ptr, ale tak nie jest). Sam zobacz:
#include <memory> #include <string> #include <iostream> class Functor { std::string greeting; public: Functor(const Functor &rhs) { this->greeting = rhs.greeting; std::cout << "Copy-Ctor \n"; } Functor(std::string _greeting="Hello!"): greeting { _greeting } { std::cout << "Ctor \n"; } Functor & operator=(const Functor & rhs) { greeting = rhs.greeting; std::cout << "Copy-assigned\n"; return *this; } virtual ~Functor() { std::cout << "Dtor\n"; } void operator()() { std::cout << "hey" << "\n"; } }; auto getFpp() { std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{} ); (*fp)(); return fp; } int main() { auto f = getFpp(); (*f)(); }
robi to wyjście:
Dokładnie taki sam zestaw ctors / dtors zostałby wywołany dla obiektu lambda przydzielonego stosowi! (Teraz wywołuje Ctor do alokacji stosu, Copy-ctor (+ alokacja sterty), aby skonstruować go w std :: function i inny do tworzenia alokacji sterty shared_ptr + konstrukcji funkcji)
źródło