Jestem początkującym C ++, ale nie jestem początkującym programistą. Próbuję nauczyć się C ++ (c ++ 11) i jest dla mnie niejasne, najważniejsze: przekazywanie parametrów.
Rozważyłem te proste przykłady:
Klasa, która ma wszystkie składowe typy pierwotne:
CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)
Klasa, która ma jako składowe typy pierwotne + 1 typ złożony:
Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)
Klasa, która ma jako składowe typy pierwotne + 1 kolekcja pewnego typu złożonego:
Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)
Kiedy tworzę konto, robię to:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc);
Oczywiście w tym scenariuszu karta kredytowa zostanie skopiowana dwukrotnie. Jeśli przepiszę ten konstruktor jako
Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)
będzie jedna kopia. Jeśli przepiszę to jako
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))
Będą 2 ruchy i bez kopii.
Myślę, że czasami możesz chcieć skopiować jakiś parametr, czasami nie chcesz kopiować podczas tworzenia tego obiektu.
Pochodzę z C # i będąc przyzwyczajonym do odwołań, jest to dla mnie trochę dziwne i myślę, że dla każdego parametru powinny być 2 przeciążenia, ale wiem, że się mylę.
Czy są jakieś najlepsze praktyki dotyczące wysyłania parametrów w C ++, ponieważ naprawdę uważam, że nie jest to trywialne. Jak poradzisz sobie z moimi przykładami przedstawionymi powyżej?
std::string
to klasa, podobnie jakCreditCard
, a nie typ prymitywny."abc"
nie jest typustd::string
, nie typuchar */const char *
, ale typuconst char[N]
(z N = 4 w tym przypadku ze względu na trzy znaki i wartość null). To dobre, powszechne nieporozumienie, aby zejść z drogi.Odpowiedzi:
NAJWAŻNIEJSZE PYTANIE PIERWSZE:
Jeśli twoja funkcja wymaga zmodyfikowania oryginalnego przekazywanego obiektu, aby po powrocie wywołania modyfikacje tego obiektu były widoczne dla wywołującego, to powinieneś przekazać referencję lvalue :
Jeśli twoja funkcja nie musi modyfikować oryginalnego obiektu i nie musi tworzyć jego kopii (innymi słowy, musi tylko obserwować jego stan), to powinieneś przekazać referencję lwartości do
const
:Umożliwi to wywołanie funkcji zarówno z lvalues (lvalues to obiekty o stabilnej tożsamości), jak iz rvalues (rvalues to na przykład tymczasowe lub obiekty, z których zamierzasz się przenieść w wyniku wywołania
std::move()
).Można również argumentować, że w przypadku typów podstawowych lub typów, dla których kopiowanie jest szybkie , takich jak
int
,bool
lubchar
, nie ma potrzeby przekazywania przez referencję, jeśli funkcja musi po prostu obserwować wartość, a przekazywanie przez wartość powinno być preferowane . To prawda, jeśli semantyka referencji nie jest potrzebna, ale co by było, gdyby funkcja chciała gdzieś przechowywać wskaźnik do tego samego obiektu wejściowego, tak aby przyszłe odczyty przez ten wskaźnik zobaczyły modyfikacje wartości, które zostały wykonane w innej części kod? W takim przypadku przekazywanie przez odniesienie jest właściwym rozwiązaniem.Jeśli twoja funkcja nie musi modyfikować oryginalnego obiektu, ale musi przechowywać kopię tego obiektu ( prawdopodobnie w celu zwrócenia wyniku transformacji danych wejściowych bez zmiany danych wejściowych ), możesz rozważyć wzięcie według wartości :
Wywołanie powyższej funkcji zawsze będzie skutkowało jedną kopią przy przekazywaniu lvalues i jednym ruchem przy przekazywaniu rvalues. Jeśli twoja funkcja musi gdzieś przechowywać ten obiekt, możesz wykonać z niego dodatkowy ruch (na przykład w przypadku
foo()
jest to funkcja składowa, która musi przechowywać wartość w składniku danych ).W przypadku, gdy ruchy są drogie dla obiektów typu
my_class
, możesz rozważyć przeciążeniefoo()
i podać jedną wersję dla lvalues (akceptując odniesienie doconst
lvalue) i jedną wersję dla rvalues (akceptując odniesienie rvalue):Powyższe funkcje są tak podobne, że można by z nich zrobić jedną funkcję:
foo()
mogłaby stać się szablonem funkcji i można by użyć doskonałego przekazywania do określenia, czy ruch lub kopia przekazywanego obiektu zostanie wygenerowana wewnętrznie:Możesz dowiedzieć się więcej o tym projekcie, oglądając wykład Scotta Meyersa (pamiętaj tylko, że termin „ Universal References ”, którego używa, jest niestandardowy).
Jedną rzeczą, o której należy pamiętać, jest to, że
std::forward
zwykle kończy się to ruchem dla rwartości, więc nawet jeśli wygląda to stosunkowo niewinnie, wielokrotne przekazywanie tego samego obiektu może być źródłem problemów - na przykład dwukrotne przeniesienie się z tego samego obiektu! Uważaj więc, aby nie umieszczać tego w pętli i nie przekazywać wielokrotnie tego samego argumentu w wywołaniu funkcji:Zauważ również, że normalnie nie uciekasz się do rozwiązania opartego na szablonach, chyba że masz ku temu dobry powód, ponieważ utrudnia to odczytanie kodu. Zwykle powinieneś skupić się na przejrzystości i prostocie .
Powyższe są tylko prostymi wskazówkami, ale w większości przypadków będą wskazywać na dobre decyzje projektowe.
DOTYCZĄCE POZOSTAŁOŚCI PAŃSTWA:
To nie jest poprawne. Po pierwsze, odwołanie do wartości r nie może wiązać się z lwartością, więc kompiluje się tylko wtedy, gdy przekazujesz konstruktorowi wartość typu r
CreditCard
. Na przykład:Ale to nie zadziała, jeśli spróbujesz to zrobić:
Ponieważ
cc
jest to lwartość, a odwołania do rwartości nie mogą wiązać się z lwartościami. Co więcej, podczas wiązania odwołania do obiektu nie jest wykonywany żaden ruch : jest to tylko powiązanie referencji. Tak więc będzie tylko jeden ruch.Tak więc w oparciu o wytyczne przedstawione w pierwszej części tej odpowiedzi, jeśli interesuje Cię liczba ruchów generowanych podczas przyjmowania
CreditCard
wartości przez wartość, możesz zdefiniować dwa przeciążenia konstruktora, jeden pobierający odwołanie do lvalue doconst
(CreditCard const&
) i jeden przyjmujący odwołanie do wartości r (CreditCard&&
).Rozwiązanie przeciążenia wybierze pierwszą przy przekazywaniu lwartości (w tym przypadku zostanie wykonana jedna kopia), a drugą przy przekazaniu rwartości (w tym przypadku jeden ruch zostanie wykonany).
Twoje użycie
std::forward<>
jest zwykle widoczne, gdy chcesz osiągnąć doskonałe przekazywanie . W takim przypadku Twój konstruktor byłby faktycznie szablonem konstruktora i wyglądałby mniej więcej w następujący sposóbW pewnym sensie łączy to oba przeciążenia, które pokazałem wcześniej, w jedną funkcję:
C
zostanie wydedukowana, że jestCreditCard&
na wypadek, gdybyś przekazał lwartość, a ze względu na reguły zwijania odwołań spowoduje utworzenie wystąpienia tej funkcji:Spowoduje to copy-construction of
creditCard
, jak można chcieć. Z drugiej strony, po przekazaniu rvalueC
zostanie wydedukowana, że jestCreditCard
, a zamiast tego zostanie utworzona instancja tej funkcji:Spowoduje to powstanie konstrukcji ruchu
creditCard
, która jest tym, czego chcesz (ponieważ przekazywana wartość jest wartością r, a to oznacza, że jesteśmy upoważnieni do przejścia z niej).źródło
const
. Jeśli chcesz zmodyfikować oryginalny obiekt, przejdź przez ref do non-`const. Jeśli chcesz zrobić kopię, a ruchy są tanie, weź według wartości, a następnie przenieś.obj
zamiast tworzyć lokalną kopię, aby się przenieść, nie?std::forward
można wywołać tylko raz . Widziałem ludzi umieszczających to w pętlach itp., A ponieważ ta odpowiedź będzie widziana przez wielu początkujących, powinno IMHO być grubą etykietą „Ostrzeżenie!”, Która pomoże im uniknąć tej pułapki.std::is_constructible<>
cechę typu, chyba że są odpowiednio SFINAE- ograniczony - co dla niektórych może nie być trywialne.Najpierw poprawię kilka szczegółów. Kiedy powiesz:
To nieprawda. Wiązanie z odniesieniem do wartości r nie jest ruchem. Jest tylko jeden ruch.
Dodatkowo, ponieważ
CreditCard
nie jest parametrem szablonu,std::forward<CreditCard>(creditCard)
jest po prostu rozwlekłym sposobem powiedzeniastd::move(creditCard)
.Teraz...
Jeśli twoje typy mają „tanie” ruchy, możesz po prostu ułatwić sobie życie i brać wszystko według wartości i „
std::move
razem”.Takie podejście da ci dwa ruchy, podczas gdy może przynieść tylko jeden, ale jeśli są one tanie, mogą być do zaakceptowania.
Skoro już jesteśmy przy tej sprawie „tanich ruchów”, powinienem przypomnieć, że
std::string
jest ona często implementowana z tak zwaną optymalizacją małych ciągów, więc jej ruchy mogą nie być tak tanie jak kopiowanie niektórych wskaźników. Jak zwykle w przypadku problemów z optymalizacją, czy ma to znaczenie, czy nie, jest coś, o co należy zapytać swojego profilera, a nie mnie.Co zrobić, jeśli nie chcesz ponosić tych dodatkowych ruchów? Może okażą się zbyt drogie lub, co gorsza, może faktycznie nie da się ich przenieść i może to spowodować naliczenie dodatkowych kopii.
Jeśli występuje tylko jeden problematyczny parametr, możesz podać dwa przeciążenia za pomocą
T const&
iT&&
. Spowoduje to wiązanie odwołań przez cały czas, aż do rzeczywistej inicjalizacji elementu członkowskiego, podczas której nastąpi kopia lub przeniesienie.Jeśli jednak masz więcej niż jeden parametr, prowadzi to do gwałtownego wzrostu liczby przeciążeń.
Jest to problem, który można rozwiązać dzięki doskonałemu przekazywaniu. Oznacza to, że zamiast tego piszesz szablon i używasz go
std::forward
do przenoszenia kategorii wartości argumentów do ich ostatecznego miejsca przeznaczenia jako członków.źródło
Account("",0,{brace, initialisation})
już pisać .Przede wszystkim
std::string
jest dość mocną klasą, podobnie jakstd::vector
. Z pewnością nie jest prymitywny.Jeśli bierzesz jakieś duże ruchome typy według wartości do konstruktora, chciałbym
std::move
je do elementu członkowskiego:Dokładnie tak poleciłbym implementację konstruktora. Powoduje, że elementy
number
icreditCard
są konstruowane w ruchu, a nie kopiowane. Gdy użyjesz tego konstruktora, będzie jedna kopia (lub przeniesienie, jeśli jest to tymczasowe), gdy obiekt zostanie przekazany do konstruktora, a następnie jeden ruch podczas inicjalizacji elementu członkowskiego.Rozważmy teraz ten konstruktor:
Masz rację, będzie to wymagało jednej kopii
creditCard
, ponieważ jest ona najpierw przekazywana do konstruktora przez referencję. Ale teraz nie możesz przekazaćconst
obiektów do konstruktora (ponieważ odwołanie jest inne niżconst
) i nie możesz przekazać obiektów tymczasowych. Na przykład nie możesz tego zrobić:Rozważmy teraz:
Tutaj pokazałeś niezrozumienie odniesień rwartości i
std::forward
. Powinieneś używać tylkostd::forward
wtedy, gdy obiekt, który przekazujesz, jest zadeklarowany jakoT&&
jakiś typ dedukowanyT
. TutajCreditCard
nie jest wydedukowane (zakładam), więcstd::forward
jest używany błędnie. Wyszukaj uniwersalne referencje .źródło
Używam dość prostej reguły dla ogólnego przypadku: użyj copy dla POD (int, bool, double, ...) i const & dla wszystkiego innego ...
A jeśli chcesz skopiować, czy nie, odpowiedź nie jest podpisem metody, ale bardziej tym, co robisz z parametrami.
precyzja dla wskaźnika: prawie nigdy ich nie używałem. Jedyną zaletą w stosunku do & jest to, że mogą być zerowe lub ponownie przypisane.
źródło
(*) wskaźniki mogą odnosić się do dynamicznie alokowanej pamięci, dlatego jeśli to możliwe, należy preferować odwołania od wskaźników, nawet jeśli ostatecznie odwołania są zwykle implementowane jako wskaźniki.
(**) „normalnie” oznacza konstruktor kopiujący (jeśli przekazujesz obiekt tego samego typu parametru) lub normalny konstruktor (jeśli przekazujesz zgodny typ dla klasy). Kiedy przekazujesz obiekt
myMethod(std::string)
, na przykład, konstruktor kopiujący zostanie użyty, jeślistd::string
zostanie do niego przekazany, dlatego musisz się upewnić, że istnieje.źródło