NAJWAŻNIEJSZE PYTANIE PIERWSZE:
Czy są jakieś najlepsze praktyki dotyczące wysyłania parametrów w C ++, ponieważ naprawdę uważam, że nie jest to trywialne
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 :
void foo(my_class& obj)
{
// Modify obj here...
}
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 doconst
:
void foo(my_class const& obj)
{
// Observe obj here
}
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
lub char
, 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 :
void foo(my_class obj) // One copy or one move here, but not working on
// the original object...
{
// Working on obj...
// Possibly move from obj if the result has to be stored somewhere...
}
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ążenie foo()
i podać jedną wersję dla lvalues (akceptując odniesienie do const
lvalue) i jedną wersję dla rvalues (akceptując odniesienie rvalue):
// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = obj; // Copy!
// Working on copyOfObj...
}
// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = std::move(obj); // Move!
// Notice, that invoking std::move() is
// necessary here, because obj is an
// *lvalue*, even though its type is
// "rvalue reference to my_class".
// Working on copyOfObj...
}
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:
template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
// ^^^
// Beware, this is not always an rvalue reference! This will "magically"
// resolve into my_class& if an lvalue is passed, and my_class&& if an
// rvalue is passed
{
my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
// Working on copyOfObj...
}
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:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
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:
Jeśli przepiszę to jako [...], będą 2 ruchy i bez kopii.
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:
// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));
Ale to nie zadziała, jeśli spróbujesz to zrobić:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
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 do const
( 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).
Account(std::string number, float amount, CreditCard const& creditCard)
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }
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ób
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
W pewnym sensie łączy to oba przeciążenia, które pokazałem wcześniej, w jedną funkcję: C
zostanie wydedukowana, że jest CreditCard&
na wypadek, gdybyś przekazał lwartość, a ze względu na reguły zwijania odwołań spowoduje utworzenie wystąpienia tej funkcji:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Spowoduje to copy-construction of creditCard
, jak można chcieć. Z drugiej strony, po przekazaniu rvalue C
zostanie wydedukowana, że jest CreditCard
, a zamiast tego zostanie utworzona instancja tej funkcji:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
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).