Czy w C ++ lepiej jest przekazywać wartość lub przechodzić przez stałe odwołanie?
Zastanawiam się, która jest lepsza praktyka. Zdaję sobie sprawę, że przekazywanie przez stałe odwołanie powinno zapewnić lepszą wydajność w programie, ponieważ nie tworzysz kopii zmiennej.
c++
variables
pass-by-reference
constants
pass-by-value
Matt Pascoe
źródło
źródło
Odpowiedzi:
Zwykło się na ogół zaleca najlepszych praktyk 1 do użytku przejściu przez const ref dla wszystkich typów , z wyjątkiem typów (wbudowane
char
,int
,double
itp), na iteratory i obiektów funkcyjnych (lambda, zajęcia wynikające zstd::*_function
).Było to szczególnie prawdziwe przed istnieniem semantyki ruchów . Powód jest prosty: jeśli przeszedłeś przez wartość, musisz wykonać kopię obiektu i, z wyjątkiem bardzo małych obiektów, zawsze jest to droższe niż przekazanie referencji.
Dzięki C ++ 11 uzyskaliśmy semantykę ruchów . W skrócie, semantyka przenoszenia pozwala, aby w niektórych przypadkach obiekt mógł być przekazywany „przez wartość” bez kopiowania go. W szczególności dzieje się tak, gdy obiekt, który mijasz, jest wartością .
Samo poruszanie się obiektu jest nadal co najmniej tak samo drogie, jak przejście przez odniesienie. Jednak w wielu przypadkach funkcja i tak wewnętrznie kopiuje obiekt - tzn. Przejmuje własność argumentu. 2)
W takich sytuacjach mamy następującą (uproszczoną) kompromis:
„Przekaż przez wartość” nadal powoduje skopiowanie obiektu, chyba że obiekt jest wartością zmienną. W przypadku wartości można zamiast tego przesuwać obiekt, aby drugi przypadek przestał nagle „kopiować, następnie przesuwać”, ale „przesuwać, a następnie (potencjalnie) przesuwać ponownie”.
W przypadku dużych obiektów, które implementują odpowiednie konstruktory ruchów (takie jak wektory, łańcuchy…), drugi przypadek jest wtedy znacznie wydajniejszy niż pierwszy. Dlatego zaleca się użycie parametru pass by value, jeśli funkcja przejmuje własność argumentu i jeśli typ obiektu obsługuje wydajne przenoszenie .
Uwaga historyczna:
W rzeczywistości każdy współczesny kompilator powinien być w stanie dowiedzieć się, kiedy przekazywanie wartości jest drogie, i jeśli to możliwe, pośrednio przekonwertować wywołanie, aby używało stałej referencji.
W teorii. W praktyce kompilatory nie zawsze mogą to zmienić bez zerwania binarnego interfejsu funkcji. W niektórych szczególnych przypadkach (gdy funkcja jest wbudowana) kopia zostanie pominięta, jeśli kompilator może stwierdzić, że oryginalny obiekt nie zostanie zmieniony poprzez działania w funkcji.
Ale generalnie kompilator nie może tego ustalić, a pojawienie się semantyki ruchu w C ++ sprawiło, że ta optymalizacja jest mniej istotna.
1 Np. W Scott Meyers, Effective C ++ .
2 Jest to szczególnie często prawdziwe w przypadku konstruktorów obiektów, które mogą przyjmować argumenty i przechowywać je wewnętrznie, aby były częścią stanu zbudowanego obiektu.
źródło
Edytuj: Nowy artykuł Dave'a Abrahamsa na temat CPP-next:
Chcesz prędkość? Przekaż według wartości.
Przekazywanie wartości dla struktur, w których kopiowanie jest tanie, ma tę dodatkową zaletę, że kompilator może założyć, że obiekty nie mają aliasu (nie są tymi samymi obiektami). Kompilator nie może zakładać, że zawsze będzie używany kompilator pass-by-referencyjny. Prosty przykład:
kompilator może to zoptymalizować
ponieważ wie, że f i g nie mają tej samej lokalizacji. jeśli g był odnośnikiem (foo &), kompilator nie mógł tego założyć. ponieważ gi może być wtedy aliasowane przez f-> i i musi mieć wartość 7., więc kompilator musiałby ponownie pobrać nową wartość gi z pamięci.
Dla bardziej praktycznych reguł, oto dobry zestaw reguł znalezionych w artykule Move Constructors (wysoce zalecane czytanie).
„Prymitywne” powyżej oznacza w zasadzie małe typy danych, które mają kilka bajtów długości i nie są polimorficzne (iteratory, obiekty funkcyjne itp.) Ani kosztowne do skopiowania. W tym dokumencie jest jeszcze jedna zasada. Chodzi o to, że czasami ktoś chce zrobić kopię (na wypadek, gdyby argument nie mógł zostać zmodyfikowany), a czasami nie chce (na wypadek, gdyby ktoś chciał użyć samego argumentu w funkcji, jeśli i tak argument był tymczasowy , na przykład). Artykuł szczegółowo wyjaśnia, jak to zrobić. W C ++ 1x tę technikę można stosować natywnie z obsługą języka. Do tego czasu przestrzegałbym powyższych zasad.
Przykłady: Aby utworzyć ciąg wielkimi literami i zwrócić wersję wielkimi literami, zawsze należy przekazać wartość: Należy mimo wszystko wziąć kopię (nie można bezpośrednio zmienić stałej const) - lepiej więc uczynić ją tak przejrzystą, jak to możliwe dzwoniącego i wykonaj kopię wcześniej, aby dzwoniący mógł zoptymalizować w jak największym stopniu - jak szczegółowo opisano w tym dokumencie:
Jeśli jednak i tak nie musisz zmieniać parametru, weź go przez odniesienie do const:
Jeśli jednak celem tego parametru jest zapisanie czegoś w argumencie, przekaż go przez odwołanie inne niż const
źródło
__restrict__
(które mogą również działać na referencjach) niż robić nadmierne kopie. Szkoda, że standardowy C ++ nie przyjąłrestrict
słowa kluczowego C99 .Zależy od typu. Dodajesz niewielki narzut związany z tworzeniem odniesienia i dereferencją. W przypadku typów o rozmiarze równym lub mniejszym niż wskaźniki, które używają domyślnego ctor kopiowania, prawdopodobnie szybciej byłoby przekazać wartość.
źródło
Jak już wspomniano, zależy to od rodzaju. W przypadku wbudowanych typów danych najlepiej przekazać wartość. Nawet niektóre bardzo małe struktury, takie jak para liczb całkowitych, mogą działać lepiej, przekazując wartość.
Oto przykład, zakładając, że masz wartość całkowitą i chcesz przekazać ją innej procedurze. Jeśli ta wartość została zoptymalizowana do przechowywania w rejestrze, to jeśli chcesz przekazać ją jako odniesienie, najpierw musisz ją zapisać w pamięci, a następnie wskaźnik do tej pamięci umieszczony na stosie, aby wykonać wywołanie. Jeśli był przekazywany przez wartość, wszystko, co jest wymagane, to rejestr wypychany na stos. (Szczegóły są nieco bardziej skomplikowane niż w przypadku różnych systemów wywoływania i procesorów).
Jeśli wykonujesz programowanie szablonów, zwykle jesteś zmuszony zawsze przechodzić przez const ref, ponieważ nie znasz typów, które są przekazywane. Przekazanie kar za przekazanie czegoś złego pod względem wartości jest znacznie gorsze niż kary za przekazanie wbudowanego typu przez const ref.
źródło
Oto, co zwykle pracuję przy projektowaniu interfejsu funkcji innej niż szablon:
Przekaż wartość, jeśli funkcja nie chce modyfikować parametru, a kopiowanie wartości jest tani (int, double, float, char, bool itp.) Zwróć uwagę, że std :: string, std :: vector i reszta pojemników w standardowej bibliotece NIE są)
Przekaż wskaźnik const, jeśli kopiowanie wartości jest drogie, a funkcja nie chce modyfikować wskazanej wartości, a NULL jest wartością obsługiwaną przez funkcję.
Przekaż wskaźnik non-const, jeśli kopiowanie wartości jest drogie, a funkcja chce zmodyfikować wskazaną wartość, a NULL jest wartością obsługiwaną przez funkcję.
Przekaż referencję const, gdy kopiowanie wartości jest kosztowne, a funkcja nie chce modyfikować wartości, do której się odwołuje, a NULL nie byłaby prawidłową wartością, gdyby zamiast niej użyto wskaźnika.
Przekaż nie-stałe odwołanie, gdy kopiowanie wartości jest kosztowne, a funkcja chce zmodyfikować wskazaną wartość, a NULL nie byłaby prawidłową wartością, gdyby zamiast niej użyto wskaźnika.
źródło
std::optional
do zdjęcia i nie potrzebujesz już wskaźników.Wygląda na to, że masz odpowiedź. Przekazywanie wartości jest drogie, ale daje kopię do pracy, jeśli jej potrzebujesz.
źródło
Z reguły przekazywanie stałych przez referencje jest lepsze. Ale jeśli potrzebujesz zmodyfikować argument funkcji lokalnie, lepiej użyj przekazywania wartości. W przypadku niektórych podstawowych typów wydajność jest zasadniczo taka sama zarówno w przypadku przekazywania wartości, jak i odniesienia. Właściwie odniesienie wewnętrznie reprezentowane przez wskaźnik, dlatego możesz na przykład oczekiwać, że dla wskaźnika oba przekazywanie są takie same pod względem wydajności, a nawet przekazywanie wartości może być szybsze z powodu niepotrzebnego dereferencji.
źródło
Z reguły wartość dla typów nieklasowych i stałe odniesienie dla klas. Jeśli klasa jest naprawdę mała, prawdopodobnie lepiej jest przekazać wartość, ale różnica jest minimalna. To, czego naprawdę chcesz uniknąć, to przekazywanie jakiejś gigantycznej klasy pod względem wartości i powielanie jej wszystkich - to zrobi ogromną różnicę, jeśli przekażesz, powiedzmy, std :: vector z całkiem kilkoma elementami.
źródło
std::vector
faktycznie umieszcza swoje elementy na stercie, a sam obiekt wektorowy nigdy nie rośnie. Zaczekaj. Jeśli jednak operacja spowoduje wykonanie kopii wektora, w rzeczywistości przejdzie i powieli wszystkie elementy. To by było złe.sizeof(std::vector<int>)
jest stały, ale przekazanie go przez wartość nadal kopiuje zawartość przy braku sprytu kompilatora.Przekaż wartość dla małych typów.
Przekaż referencje const dla dużych typów (definicja big może się różnić w zależności od komputera) ALE, w C ++ 11, przekaż wartość, jeśli zamierzasz korzystać z danych, ponieważ możesz wykorzystać semantykę move. Na przykład:
Teraz kod wywołujący wykonałby:
I tylko jeden obiekt zostałby utworzony i przeniesiony bezpośrednio do członka
name_
w klasiePerson
. Jeśli przejdziesz przez const referen, będziesz musiał wykonać kopię do umieszczenia w nimname_
.źródło
Prosta różnica: - W funkcji mamy parametr wejściowy i wyjściowy, więc jeśli przekazujący parametr wejściowy i wyjściowy jest taki sam, użyj wywołania przez odniesienie, jeśli parametr wejściowy i wyjściowy są różne, lepiej użyć wywołania według wartości.
przykład
void amount(int account , int deposit , int total )
parametr wejściowy: konto, parametr wyjściowy depozytu: ogółem
wejście i wyjście to inne użycie połączenia przez vaule
void amount(int total , int deposit )
suma wejściowa suma suma depozytów
źródło