Czy wartość przekazywana jest rozsądną wartością domyślną w C ++ 11?

142

W tradycyjnym C ++ przekazywanie wartości do funkcji i metod jest powolne w przypadku dużych obiektów i generalnie jest źle widziane. Zamiast tego programiści C ++ mają tendencję do przekazywania referencji, co jest szybsze, ale wprowadza różnego rodzaju skomplikowane pytania dotyczące własności, a zwłaszcza zarządzania pamięcią (w przypadku, gdy obiekt jest alokowany na stosie)

Teraz w C ++ 11 mamy referencje Rvalue i konstruktory przenoszenia, co oznacza, że ​​można zaimplementować duży obiekt (taki jak std::vector), który jest tani do przekazywania wartości do i z funkcji.

Czy to oznacza, że ​​wartością domyślną powinno być przekazywanie wartości dla wystąpień typów, takich jak std::vectori std::string? A co z obiektami niestandardowymi? Jaka jest nowa najlepsza praktyka?

Derek Thurn
źródło
22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated). Nie rozumiem, jak skomplikowane lub problematyczne jest posiadanie? Może coś mi umknęło?
iammilind
1
@iammilind: przykład z własnego doświadczenia. Jeden wątek zawiera obiekt typu string. Jest przekazywany do funkcji, która generuje inny wątek, ale nieznana wywołującemu funkcja wzięła łańcuch jako const std::string&kopię, a nie kopię. Wtedy pierwszy wątek wyszedł ...
Zan Lynx,
12
@ZanLynx: To brzmi jak funkcja, która najwyraźniej nigdy nie została zaprojektowana do wywołania jako funkcja wątku.
Nicol Bolas,
5
Zgadzając się z iammilind, nie widzę żadnego problemu. Przekazywanie przez odwołanie do stałej powinno być domyślne dla „dużych” obiektów i według wartości dla mniejszych obiektów. Ustawiłbym limit między dużymi i małymi na około 16 bajtów (lub 4 wskaźniki w systemie 32-bitowym).
JN,
3
Herb Sutter wraca do podstaw! Prezentacja Essentials of Modern C ++ na CppCon zawierała sporo szczegółów na ten temat. Wideo tutaj .
Chris Drew

Odpowiedzi:

138

Jest to rozsądne ustawienie domyślne, jeśli chcesz wykonać kopię wewnątrz ciała. Oto, za czym opowiada się Dave Abrahams :

Wskazówka: nie kopiuj argumentów funkcji. Zamiast tego przekaż je według wartości i pozwól kompilatorowi wykonać kopiowanie.

W kodzie oznacza to, że nie rób tego:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

ale zrób to:

void foo(T t)
{
    // ...
}

który ma tę zaletę, że dzwoniący może używać w następujący foosposób:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

i tylko minimalna praca jest wykonywana. Potrzebujesz dwóch przeciążeń, aby zrobić to samo z odwołaniami, void foo(T const&);i void foo(T&&);.

Mając to na uwadze, napisałem teraz moich cenionych konstruktorów jako takich:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

W przeciwnym razie przekazanie przez odniesienie do constnadal jest rozsądne.

Luc Danton
źródło
29
+1, szczególnie za ostatni bit :) Nie należy zapominać, że Move Constructors można wywołać tylko wtedy, gdy obiekt, z którego ma się przesunąć, nie powinien być później niezmieniony: SomeProperty p; for (auto x: vec) { x.foo(p); }na przykład nie pasuje. Ponadto, ruch konstruktorów ma swój koszt (im większy obiekt, tym wyższy koszt), podczas gdy const&są zasadniczo bezpłatne.
Matthieu M.,
25
@MatthieuM. Ale ważne jest, aby wiedzieć, co „im większy obiekt, tym wyższy koszt” w rzeczywistości oznacza ruch: „większy” w rzeczywistości oznacza „im więcej ma zmiennych składowych”. Na przykład przeniesienie elementu std::vectorzawierającego milion elementów kosztuje tyle samo, co przeniesienie elementu zawierającego pięć elementów, ponieważ przesuwany jest tylko wskaźnik do tablicy na stercie, a nie każdy obiekt w wektorze. Więc to nie jest tak duży problem.
Lucas
+1 Odkąd zacząłem używać C ++ 11, używam również konstrukcji typu pass-by-value-then-move. To sprawia, że ​​czuję się trochę nieswojo, ponieważ mój kod jest teraz std::movewszędzie ...
stijn
1
Jest jedno ryzyko const&, które kilka razy mnie przewróciło. void foo(const T&); int main() { S s; foo(s); }. Może się to skompilować, nawet jeśli typy są różne, jeśli istnieje konstruktor T, który przyjmuje S jako argument. Może to być powolne, ponieważ może być konstruowany duży obiekt T. Możesz pomyśleć, że przekazujesz referencje bez kopiowania, ale możesz tak. Zobacz tę odpowiedź na pytanie, które zadałem więcej. Zasadniczo &zwykle wiąże się tylko z lvalues, ale jest wyjątek dla rvalue. Są alternatywy.
Aaron McDaid
1
@AaronMcDaid To stara wiadomość, w tym sensie, że zawsze trzeba było o tym wiedzieć, nawet przed C ++ 11. I nic w tym zakresie się nie zmieniło.
Luc Danton
71

W prawie wszystkich przypadkach Twoja semantyka powinna wyglądać następująco:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

Wszystkie inne podpisy powinny być używane oszczędnie i z dobrym uzasadnieniem. Kompilator będzie teraz prawie zawsze rozwiązywać je w najbardziej efektywny sposób. Możesz po prostu kontynuować pisanie kodu!

Ayjay
źródło
2
Chociaż wolę przekazywać wskaźnik, jeśli zamierzam zmodyfikować argument. Zgadzam się z przewodnikiem stylistycznym Google, że dzięki temu jest bardziej oczywiste, że argument zostanie zmodyfikowany bez konieczności podwójnego sprawdzania podpisu funkcji ( google-styleguide.googlecode.com/svn/trunk/ ... ).
Max Lybbert,
40
Powodem, dla którego nie lubię przekazywania wskaźników, jest to, że dodają one możliwy stan awarii do moich funkcji. Staram się pisać wszystkie moje funkcje tak, aby były poprawne, ponieważ znacznie zmniejsza to miejsce na ukrywanie błędów. foo(bar& x) { x.a = 3; }Jest o wiele bardziej niezawodne (i czytelne!) Niżfoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay,
22
@Max Lybbert: Z parametrem wskaźnika nie musisz sprawdzać podpisu funkcji, ale musisz sprawdzić dokumentację, aby wiedzieć, czy możesz przekazywać wskaźniki o wartości null, czy funkcja przejmie prawa własności itp. IMHO, parametr wskaźnika przekazuje znacznie mniej informacji niż odniesienie inne niż stała. Zgadzam się jednak, że byłoby miło mieć wizualną wskazówkę w miejscu wywołania, że ​​argument można zmodyfikować (jak refsłowo kluczowe w C #).
Luc Touraille,
Jeśli chodzi o przekazywanie wartości i poleganie na semantyce przenoszenia, uważam, że te trzy opcje lepiej wyjaśniają zamierzone użycie parametru. Są to wytyczne, których zawsze przestrzegam.
Trevor Hickey
1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Oba te założenia są niepoprawne. unique_ptri shared_ptrmoże zawierać wartości null / nullptr. Jeśli nie chcesz martwić się o wartości null, powinieneś używać odwołań, ponieważ nigdy nie mogą być puste. Nie będziesz też musiał pisać ->, co Cię denerwuje :)
Julian
10

Przekaż parametry według wartości, jeśli w treści funkcji potrzebujesz kopii obiektu lub musisz tylko przenieść obiekt. Pomiń, const&jeśli potrzebujesz tylko niemutującego dostępu do obiektu.

Przykład kopii obiektu:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

Przykład przeniesienia obiektu:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

Przykład dostępu bez mutacji:

void read_pattern(T const& t) {
    t.some_const_function();
}

Dla uzasadnienia zobacz te posty na blogu autorstwa Dave'a Abrahamsa i Xiang Fan .

Edward Brey
źródło
0

Podpis funkcji powinien odzwierciedlać jej przeznaczenie. Czytelność jest ważna, także dla optymalizatora.

Jest to najlepszy warunek wstępny dla optymalizatora do stworzenia najszybszego kodu - przynajmniej w teorii, a jeśli nie w rzeczywistości, to za kilka lat.

Zagadnienia dotyczące wydajności są bardzo często przeceniane w kontekście przekazywania parametrów. Przykładem jest perfekcyjna spedycja. Funkcje takie jak emplace_backsą przeważnie bardzo krótkie i tak czy inaczej wbudowane.

Patrick Fromberg
źródło