Czy powinienem przekazać funkcję std :: przez odwołanie do stałej?

141

Powiedzmy, że mam funkcję, która przyjmuje std::function:

void callFunction(std::function<void()> x)
{
    x();
}

Czy powinienem xzamiast tego przejść przez const-reference ?:

void callFunction(const std::function<void()>& x)
{
    x();
}

Czy odpowiedź na to pytanie zmienia się w zależności od tego, co robi z nią funkcja? Na przykład, jeśli jest to funkcja składowa klasy lub konstruktor, który przechowuje lub inicjuje std::functionw zmiennej składowej.

Sven Adbring
źródło
1
Prawdopodobnie nie. Nie wiem na pewno, ale spodziewałbym sizeof(std::function)się, że nie będzie więcej niż 2 * sizeof(size_t), czyli najmniejszy rozmiar, jaki kiedykolwiek wziąłbyś pod uwagę jako odniesienie do stałej.
Mats Petersson
12
@Mats: Nie sądzę, aby rozmiar std::functionopakowania był tak ważny, jak złożoność jego kopiowania. Jeśli w grę wchodzą głębokie kopie, może to być znacznie droższe niż sizeofsugeruje.
Ben Voigt
Czy powinieneś movepełnić tę funkcję?
Yakk - Adam Nevraumont
operator()()jest constwięc odwołanie do const powinno działać. Ale nigdy nie użyłem std :: function.
Neel Basu
@Yakk Po prostu przekazuję lambdę bezpośrednio do funkcji.
Sven Adbring

Odpowiedzi:

79

Jeśli chcesz wydajności, podaj wartość, jeśli ją przechowujesz.

Załóżmy, że masz funkcję o nazwie „uruchom to w wątku interfejsu użytkownika”.

std::future<void> run_in_ui_thread( std::function<void()> )

który uruchamia kod w wątku „ui”, a następnie sygnalizuje zakończenie future. (Przydatne w strukturach UI, w których wątek UI jest miejscem, w którym powinieneś zadzierać z elementami UI)

Rozważamy dwa podpisy:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

Teraz prawdopodobnie użyjemy ich w następujący sposób:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

co utworzy anonimowe zamknięcie (lambda), skonstruuje z niego std::functionwyjście, przekaże je do run_in_ui_threadfunkcji, a następnie zaczeka, aż zakończy się działanie w głównym wątku.

W przypadku (A), std::functionjest on konstruowany bezpośrednio z naszej lambdy, która jest następnie używana w run_in_ui_thread. Lambda jest moved do std::function, więc każdy ruchomy stan jest skutecznie przenoszony do niej.

W drugim przypadku std::functiontworzony jest tymczasowy , lambda jest movew nim d, a następnie ten tymczasowy std::functionjest używany przez odniesienie w run_in_ui_thread.

Jak na razie dobrze - obaj grają identycznie. Tyle run_in_ui_threadże program utworzy kopię swojego argumentu funkcji do wysłania do wątku interfejsu użytkownika w celu wykonania! (wróci, zanim zostanie z nim zakończony, więc nie może po prostu użyć odniesienia do niego). W przypadku (A), po prostu do jego długotrwałego przechowywania. W przypadku (B) jesteśmy zmuszeni skopiować plik .movestd::functionstd::function

Ten sklep sprawia, że ​​przekazywanie wartości jest bardziej optymalne. Jeśli istnieje możliwość, że przechowujesz kopię pliku std::function, podaj wartość. W przeciwnym razie obie metody są z grubsza równoważne: jedyną wadą wartości bocznej jest to, że bierzesz tę samą masę std::functioni jedną metodę podrzędną po drugiej. Poza tym, a movebędzie tak samo wydajne jak const&.

Teraz są pewne inne różnice między tymi dwoma, które najczęściej pojawiają się, jeśli mamy trwały stan w obrębie std::function.

Załóżmy, że std::functionprzechowuje jakiś obiekt z a operator() const, ale ma też kilka mutableczłonków danych, które modyfikuje (jakie niegrzeczne!).

W takim std::function<> const&przypadku mutablezmodyfikowane elementy składowe danych będą propagowane poza wywołanie funkcji. W takim std::function<>przypadku nie będą.

To stosunkowo dziwny przypadek narożny.

Chcesz traktować std::functionjak każdy inny potencjalnie ciężki, tani przenośny typ. Przenoszenie jest tanie, kopiowanie może być kosztowne.

Yakk - Adam Nevraumont
źródło
Semantyczna zaleta „przekazywania wartości, jeśli ją przechowujesz”, jak mówisz, polega na tym, że zgodnie z umową funkcja nie może zachować adresu przekazanego argumentu. Ale czy to naprawdę prawda, że ​​„Jeśli tego nie zrobisz, ruch będzie tak samo skuteczny jak const &”? Zawsze widzę koszt operacji kopiowania plus koszt operacji przenoszenia. Po przejściu const&widzę tylko koszt operacji kopiowania.
ceztko
2
@ceztko W obu przypadkach (A) i (B) zmienna tymczasowa std::functionjest tworzona z lambda. W (A), tymczasowy jest usuwany do argumentu run_in_ui_thread. W (B) odniesienie do wspomnianego tymczasowego jest przekazywane do run_in_ui_thread. Dopóki twoje std::functions są tworzone z lambd jako tymczasowych, ta klauzula obowiązuje. Poprzedni akapit dotyczy przypadku, w którym std::functionproblem się utrzymuje. Jeśli nie przechowujemy, po prostu tworzymy z lambda function const&i functionmamy dokładnie ten sam narzut.
Yakk - Adam Nevraumont
O, rozumiem! To oczywiście zależy od tego, co dzieje się na zewnątrz run_in_ui_thread(). Czy jest tylko podpis mówiący „Przekaż przez odniesienie, ale nie będę przechowywać adresu”?
ceztko
@ceztko Nie, nie ma.
Yakk - Adam Nevraumont
1
@ Yakk-AdamNevraumont, jeśli byłoby bardziej kompletne, aby objąć inną opcję przejścia przez rvalue ref:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P
33

Jeśli martwisz się o wydajność i nie definiujesz wirtualnej funkcji składowej, najprawdopodobniej w ogóle nie powinieneś jej używać std::function.

Uczynienie typu funktora parametrem szablonu pozwala na większą optymalizację niż std::functionwłączenie logiki funktora. Efekt tych optymalizacji prawdopodobnie znacznie przeważy nad obawami o kopiowanie kontra pośrednie, jak przejść std::function.

Szybciej:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}
Ben Voigt
źródło
1
Właściwie nie martwię się wcale o wydajność. Po prostu pomyślałem, że używanie odwołań do stałych tam, gdzie powinny być używane, jest powszechną praktyką (przychodzą na myśl łańcuchy i wektory).
Sven Adbring
13
@Ben: Myślę, że najnowocześniejszym przyjaznym dla hipisów sposobem implementacji tego jest użycie std::forward<Functor>(x)();, aby zachować kategorię wartości funktora, ponieważ jest to odniesienie „uniwersalne”. Nie będzie to jednak miało znaczenia w 99% przypadków.
GManNickG,
1
@Ben Voigt, więc w twoim przypadku, czy nazwałbym tę funkcję ruchem? callFunction(std::move(myFunctor));
arias_JC
2
@arias_JC: Jeśli parametr to lambda, to już jest rvalue. Jeśli masz lwartość, możesz użyć, std::movejeśli nie będziesz już jej potrzebować w inny sposób, lub przekazać bezpośrednio, jeśli nie chcesz wyjść z istniejącego obiektu. Reguły zwijania odwołań zapewniają, że callFunction<T&>()parametr ma typ T&, a nie T&&.
Ben Voigt,
1
@BoltzmannBrain: Zdecydowałem się nie wprowadzać tej zmiany, ponieważ jest ona ważna tylko w najprostszym przypadku, gdy funkcja jest wywoływana tylko raz. Moja odpowiedź brzmi: „jak powinienem przekazać obiekt funkcji?” i nie ogranicza się do funkcji, która nie robi nic poza bezwarunkowym wywołaniem tego funktora dokładnie raz.
Ben Voigt
25

Jak zwykle w C ++ 11, przekazywanie przez wartość / referencję / stałą-referencję zależy od tego, co zrobisz z argumentem. std::functionnie jest inaczej.

Przekazywanie przez wartość umożliwia przeniesienie argumentu do zmiennej (zwykle jest to zmienna składowa klasy):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

Jeśli wiesz, że Twoja funkcja przeniesie swój argument, jest to najlepsze rozwiązanie, w ten sposób użytkownicy mogą kontrolować sposób wywoływania funkcji:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

Wydaje mi się, że znasz już semantykę (nie) odniesień stałych, więc nie będę się nad tym rozwodzić. Jeśli chcesz, żebym dodał więcej wyjaśnień na ten temat, po prostu zapytaj, a zaktualizuję.

syam
źródło