Czy w C ++ lepiej jest przekazywać wartość lub przechodzić przez stałe odwołanie?

Odpowiedzi:

203

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, doubleitp), na iteratory i obiektów funkcyjnych (lambda, zajęcia wynikające z std::*_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:

  1. Możemy przekazać obiekt przez odniesienie, a następnie skopiować go wewnętrznie.
  2. Możemy przekazać obiekt według wartości.

„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.

Konrad Rudolph
źródło
hmmm ... Nie jestem pewien, czy warto przejść obok ref. double-s
sergtk,
3
Jak zwykle boost pomaga tutaj. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm zawiera elementy szablonu, które automatycznie ustalają , kiedy typ jest typem wbudowanym (przydatne dla szablonów, w których czasami nie można tak łatwo poznać).
CesarB,
13
Ta odpowiedź pomija ważny punkt. Aby uniknąć krojenia, musisz przejść przez referencję (const lub w inny sposób). Zobacz stackoverflow.com/questions/274626/...
ChrisN
6
@Chris: racja. Pominąłem całą część polimorfizmu, ponieważ jest to zupełnie inna semantyka. Uważam, że OP (semantycznie) miał na myśli przekazywanie argumentów „wartością”. Gdy wymagana jest inna semantyka, pytanie nawet się nie stawia.
Konrad Rudolph
98

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:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

kompilator może to zoptymalizować

g.i = 15;
f->i = 2;

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).

  • Jeśli funkcja zamierza zmienić argument jako efekt uboczny, weź go przez odniesienie inne niż stałe.
  • Jeśli funkcja nie modyfikuje swojego argumentu, a argument jest typu pierwotnego, weź go według wartości.
  • W przeciwnym razie weź to jako stałe odniesienie, z wyjątkiem następujących przypadków
    • Jeśli funkcja i tak musiałaby utworzyć kopię odwołania do stałej, weź ją według wartości.

„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:

my::string uppercase(my::string s) { /* change s and return it */ }

Jeśli jednak i tak nie musisz zmieniać parametru, weź go przez odniesienie do const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Jeśli jednak celem tego parametru jest zapisanie czegoś w argumencie, przekaż go przez odwołanie inne niż const

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}
Johannes Schaub - litb
źródło
Uważam, że twoje zasady są dobre, ale nie jestem pewien co do pierwszej części, w której mówisz o tym, że nie przekazanie go jako sędziego przyspieszy. tak pewnie, ale pominięcie czegoś jako ref, po prostu optymalizacja nie ma żadnego sensu. jeśli chcesz zmienić przekazywany obiekt stosu, zrób to przez ref. jeśli nie, podaj wartość. jeśli nie chcesz tego zmieniać, podaj jako const-ref. optymalizacja związana z przekazywaniem wartości nie powinna mieć znaczenia, ponieważ zyskujesz inne rzeczy, przechodząc jako ref. nie rozumiem „chcesz prędkości?” jeśli chcesz wykonać te operacje, i tak przejdziesz przez wartość ..
chikuba
Johannes: Uwielbiałem ten artykuł, kiedy go czytałem, ale byłem rozczarowany, gdy go wypróbowałem. Ten kod nie powiódł się zarówno w GCC, jak i MSVC. Czy coś przeoczyłem, czy nie działa w praktyce?
user541686,
Nie sądzę, że zgadzam się z tym, że jeśli mimo wszystko chcesz zrobić kopię, przekażesz ją według wartości (zamiast const ref), a następnie przeniesiesz. Spójrz na to w ten sposób, co jest bardziej wydajne, kopia i ruch (możesz nawet mieć 2 kopie, jeśli przekażesz dalej), czy tylko kopię? Tak, istnieją specjalne przypadki po obu stronach, ale jeśli mimo to danych nie można przenieść (np. POD z mnóstwem liczb całkowitych), nie ma potrzeby wykonywania dodatkowych kopii.
Ion Todirel
2
Mehrdad, nie jestem pewien, czego się spodziewałeś, ale kod działa zgodnie z oczekiwaniami
Ion Todirel
Rozważałbym konieczność kopiowania, aby przekonać kompilator, że typy nie pokrywają się z brakiem języka. Wolę używać GCC __restrict__(które mogą również działać na referencjach) niż robić nadmierne kopie. Szkoda, że ​​standardowy C ++ nie przyjął restrictsłowa kluczowego C99 .
Ruslan
12

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ść.

Lou Franco
źródło
W przypadku typów nienatywnych możesz (w zależności od tego, jak dobrze kompilator optymalizuje kod) uzyskać wzrost wydajności przy użyciu stałych referencji zamiast tylko referencji.
Dz.U.
9

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.

Torlack
źródło
Uwaga na temat terminologii: struktura zawierająca milion ints jest nadal „typem POD”. Być może masz na myśli „dla typów wbudowanych najlepiej przekazać wartość”.
Steve Jessop
6

Oto, co zwykle pracuję przy projektowaniu interfejsu funkcji innej niż szablon:

  1. 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ą)

  2. 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ę.

  3. 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ę.

  4. 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.

  5. 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.

Martin G.
źródło
Dodaj std::optionaldo zdjęcia i nie potrzebujesz już wskaźników.
Violet Giraffe
5

Wygląda na to, że masz odpowiedź. Przekazywanie wartości jest drogie, ale daje kopię do pracy, jeśli jej potrzebujesz.

GeekyMonkey
źródło
Nie jestem pewien, dlaczego zostało to odrzucone? Ma to dla mnie sens. Jeśli potrzebujesz bieżącej wartości, przekaż wartość. Jeśli nie, przekaż referencję.
Totty
4
Jest całkowicie zależny od typu. Wykonanie typu POD (zwykłe stare dane) przez odniesienie może w rzeczywistości obniżyć wydajność, powodując większy dostęp do pamięci.
Torlack,
1
Oczywiście przekazanie int przez referencję niczego nie uratuje! Myślę, że pytanie implikuje rzeczy większe niż wskaźnik.
GeekyMonkey,
4
To nie jest takie oczywiste, widziałem dużo kodu przez ludzi, którzy tak naprawdę nie rozumieją, jak komputery pracują, przekazując proste rzeczy przez const ref, ponieważ powiedziano im, że jest to najlepsza rzecz do zrobienia.
Torlack,
4

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.

sergtk
źródło
Jeśli musisz zmodyfikować kopię parametru odbiorcy, możesz wykonać kopię w wywoływanym kodzie zamiast przekazywać wartości. IMO generalnie nie powinieneś wybierać API w oparciu o takie szczegóły implementacji: źródło kodu wywołującego jest tak samo w obu przypadkach, ale jego kod obiektowy nie jest.
Steve Jessop,
Jeśli przejdziesz przez wartość, tworzona jest kopia. I IMO nie ma znaczenia, w jaki sposób tworzysz kopię: poprzez przekazywanie argumentów przez wartość lub lokalnie - to dotyczy C ++. Ale z punktu widzenia projektowania zgadzam się z tobą. Ale opisuję tutaj tylko funkcje C ++ i nie dotykam designu.
sergtk
1

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.

Piotr
źródło
Rozumiem, że std::vectorfaktycznie 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.
Steven Lu,
1
Tak właśnie myślałem. sizeof(std::vector<int>)jest stały, ale przekazanie go przez wartość nadal kopiuje zawartość przy braku sprytu kompilatora.
Peter
1

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:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Teraz kod wywołujący wykonałby:

Person p(std::string("Albert"));

I tylko jeden obiekt zostałby utworzony i przeniesiony bezpośrednio do członka name_w klasie Person. Jeśli przejdziesz przez const referen, będziesz musiał wykonać kopię do umieszczenia w nim name_.

Germán Diago
źródło
-5

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

  1. void amount(int total , int deposit )

suma wejściowa suma suma depozytów

Dhirendra Sengar
źródło