- Używaj auto, gdy tylko się da
- Korzystaj z inteligentnych wskaźników i surowych wskaźników (nie mających prawa własności)
- Stosuje nullptr. Nigdy 0 albo NULL
- Korzystaj z "range for"
- Korzystaj z wersji begin(), end() (free function) nie będącymi metodami klasy
- Korzystanie z lambd wpływa na elegancję i szybkość kodu
- Korzystaj z move semantic, zamiast kopiować obiekty
- Uniform Initialization. Korzystaj z operatora przypisania (=) dla prostych typów oraz auto - po staremu. Dla wszystkich innych przypadków lepiej korzystać z {}. Nowa składnia chroni przed kilkoma starymi problemami, jak niepożądana konwersja, niezainicjowane zmienne lub przypadkowe stworzenie deklaracji funkcji.
ekstaza, geniusz, przebłysk, olśnienie, półprawdy, półśrodki, przemilczenia, zaćmienia, głupstwa, kłamstewka, oszustwa, hultajstwo, wyrachowanie, nieprawda, nieobiektywność, niepodważalna prawda, nierówność, nieomylność, słuszność, perfekcja, krnąbrność ... niegodziwość
Pokazywanie postów oznaczonych etykietą move semantic. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą move semantic. Pokaż wszystkie posty
23 grudnia 2014
[C++11] Elementy nowoczesnego C++
Kilka porad (http://herbsutter.com/elements-of-modern-c-style/) od Herba Shuttera dotyczące najistotniejszych elementów nowego C++.
18 lutego 2014
[C++11] Copy control i move semantic
Kolejny zbiór nowości w standardzie, z którymi nie do końca czuje się jeszcze pewnie. Myślę, że do tematu jeszcze powrócę, jak tylko stanie się on dla mnie bardziej klarowny.
Należy pamiętać tylko o jednej rzeczy. Nigdy nie wolno wołać bezpośrednio std::swap(). Wymusza to bowiem korzystanie z wersji bibliotecznej swap(), tymczasem klasa może mieć zmienną, która posiada własną wersję tej funkcji. Najlepiej by wyboru dokonał kompilator, przez wciągnięcie przestrzeni nazw.
Najtrudniejsza rzecz, jaka przyszła mi do opanowania wraz z nowym standardem, czyli move semantic. Kilka linków:
Korzystając z std::move obiecujemy, że nie będziemy korzystać już z danego obiektu chyba, że zamierzamy do niego coś przypisać lub go zniszczyć. Po wykonaniu std::move nie mamy żadnej gwarancji co do wartości przesuniętego obiektu.
Głównym zadaniem std::forward jest przekazanie argumentu (rvalue, lvalue, z const-em lub nie) w niezmienionej formie, do innej funkcji. Jeśli oryginalny obiekt, który jest wstawiony do std::forward był rvalue, zrobi z niego rvalue, a jeżeli lvalue, to lvalue - dlatego właśnie jest warunkowy.
Pora na przykład, który pokazuje różne scenariusze, z którymi mamy do czynienia kopiując bądź przesuwając obiekty. Na początku "gadatliwa" klasa, która pokazuje, który z mechanizmów copy-control został użyty.
W drugim przypadku (returnValue2), mamy podobną sytuację, z tą różnicą, że zamiast "move operatora" działa "move konstruktor". Gdyby "copy elision" było włączone, kompilator prawdopodobnie wyciągnąłby wywołanie z tak prostej funkcji jak ret() i wywołał po prostu konstruktor w ciele naszego przykładu.
Nowe definicje i mechanizmy wstecznej kompatybilności sprawiły, że biblioteka standardowa pozwala teraz na przypisywanie do rvalue! Czasami takie użycie może być zaskakujące. Poniższy kod jest prawidłowy.
Tak jak const, kwalifikatory te można stosować dla funkcji nie statycznych, muszą też znajdować się w deklaracji i definicji, a jeżeli występują razem z const zapisuje się je na końcu. Jeżeli definiujemy dwie lub więcej metod, które mają takie same nazwy i listę parametrów, musimy wprowadzić reference qualifier dla nich wszystkich, albo dla żadnej.
Przykład. Utworzona został operator=, ale tylko dla lvalue. Dodatkowo stworzone zostały dwie metody info, wołające różny kod w zależności od tego na jakim obiekcie zostały zawołane.
Copy control
Konstruktor kopiujący
W przypadku konstruktora kopiującego parametr jest prawie zawsze referencją na const. Rzadko zdarza się sytuacja, abyśmy byli zmuszeni do modyfikacji obiektu, z którego kopiujemy.struct Foo {
Foo(const Foo&);
};
Zasada trzech/pięciu/zero (the rule of three/five/zero).
Istnieją trzy (pięć - w nowym standardzie) podstawowe operacje kontrolujące kopiowanie obiektu:- konstruktor kopiujący
- operator przypisania
- destruktor
- move konstruktor
- operator przeniesienia (move)
- http://www.stroustrup.com/C++11FAQ.html#default2
- https://en.cppreference.com/w/cpp/language/rule_of_three
- https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-zero
- Jeśli klasa wymaga destruktora, prawie na pewno wymaga konstruktora kopiującego i operatora przypisania
- Jeśli klasa wymaga konstruktora kopiującego, prawie na pewno wymaga operatora przypisania i vice versa
- Klasa która nie może być kopiowana powinna definiować konstruktor kopiujący i operator przypisania jako delete, zamiast tworzyć te dwie definicje prywatne
- Operator przypisania (ma to też zastosowanie dla operatora move) musi pracować poprawnie, gdy próbujemy przypisać do siebie ten same element. Ma to szczególne znaczenie jeżeli, obiekt posiada dane zapisane w pamięci dynamicznej, a w wyniku przypisania tworzymy w pamięci nowe dane i kasujemy stare. W takim przypadku dobrą praktyką jest skopiowanie wartości po prawej stronie do zmiennej lokalnej, zniszczenie wartości stojącej po lewej i w końcu przypisanie jej wartości tymczasowej. [C++Primer, s. 512; s. 536]
- Większość operatorów przypisania współpracuje z destruktorem i konstruktorem kopiującym
Storage& operator=(const Storage& r) {
auto tmp = new string(*r.data);
delete data;
data = tmp;
return *this;
}
W przeciwieństwie do mechanizmów copy-control swap nigdy nie jest wymagany, jednakże zdefiniowanie własnej metody, może być ważną optymalizacją dla klasy, która alokuje zasoby. Operatory przypisania, które korzystają z techniki "copy and swap", są automatycznie odporne na wyjątki i obsługują samoprzypisanie. Należy pamiętać tylko o jednej rzeczy. Nigdy nie wolno wołać bezpośrednio std::swap(). Wymusza to bowiem korzystanie z wersji bibliotecznej swap(), tymczasem klasa może mieć zmienną, która posiada własną wersję tej funkcji. Najlepiej by wyboru dokonał kompilator, przez wciągnięcie przestrzeni nazw.
inline void swap(Storage &l, Storage& r) {
using std::swap;
swap(l.data, r.data);
swap(l.counter, r.counter);
}
void swap(Counter &l, Counter &r) {
Clock *tmp = l.clock;
l.clock = r.clock;
r.clock = l.clock;
}
Jeżeli klasa posiada chociaż jeden z mechanizmów tj.: konstruktor kopiujący, operator przypisania lub destruktor kompilator nie stworzy nam konstruktora i operatora move!Move semantic
Najtrudniejsza rzecz, jaka przyszła mi do opanowania wraz z nowym standardem, czyli move semantic. Kilka linków:
- http://en.cppreference.com/w/cpp/language/move_constructor
- http://en.cppreference.com/w/cpp/algorithm/move
- http://en.cppreference.com/w/cpp/utility/forward
- http://stackoverflow.com/questions/4986673/c11-rvalues-and-move-semantics-confusion
g++ -std=c++11 -fno-elide-constructors main.cpp
std::move i std::forward
Ciekawie o nich opowiedział Scott Meyers w swoim wykładzie An Effective C++11/14 Sampler na GoingNative 2013, z czego sporo zaczerpnąłem. Żadne z mechanizmów niczego nie obiecuje, ich zadaniem jest jedynie rzutowanie (podczas kompilacji) do rvalue:- std::move (rzutowanie bezwarunkowe) - przekazuje obiekt jako rvalue
- std::forward (rzutowanie warunkowe) - przekazuje obiekt jako rvalue, tylko jeżeli oryginalny obiekt był rvalue inaczej będzie to lvalue
Korzystając z std::move obiecujemy, że nie będziemy korzystać już z danego obiektu chyba, że zamierzamy do niego coś przypisać lub go zniszczyć. Po wykonaniu std::move nie mamy żadnej gwarancji co do wartości przesuniętego obiektu.
Głównym zadaniem std::forward jest przekazanie argumentu (rvalue, lvalue, z const-em lub nie) w niezmienionej formie, do innej funkcji. Jeśli oryginalny obiekt, który jest wstawiony do std::forward był rvalue, zrobi z niego rvalue, a jeżeli lvalue, to lvalue - dlatego właśnie jest warunkowy.
void process(Storage& lvalue) {
cout << "By lvalue reference" << endl;
}
void process(Storage&& rvalue) {
cout << "By rvalue reference" << endl;
}
template <typename T>
void log(T&& param) {
process(std::forward<T>(param));
}
int main() {
Storage st;
log(st); // przekazanie jako lvalue
log<Storage>(std::move(st)); // przekazanie jako rvalue
}
Wynik:Default constr.: defaultData By lvalue reference By rvalue reference Destructor: defaultData
"Jeżeli oczekujesz szybkiego działania przekazuj przez wartość"
Zalecenia do poprzednich wersji standardu sugerowały by lvalue były opatrzone modyfikatorem const. Jest to już nieprawdą, bowiem powodowało by to kopiowanie obiektu. Jeżeli chcemy coś przesunąć, const nam w tym przeszkodzi.void process(const Storage st) {
Storage my = std::move(st); // Kopiowanie!
}
int main() {
Storage st;
process(st);
}
WynikDefault constr.: defaultData Copy constructor: defaultData Copy constructor: defaultData Destructor: defaultData Destructor: defaultData Destructor: defaultData
noexcept dla operacji move
Ponieważ z założenia operacje "move konstruktora" i "move operatora" kradną zasoby i nie alokują żadnych nowych, więc nie powinny też rzucać żadnych wyjątków. Powinniśmy poinformować o tym kompilator stosując noexcept. Co więcej, kontenery takie jak std::vector, sprawdzają, czy "move konstruktor" jest noexcept (podczas realokacji), jeżeli nie, zostanie zawołany zwykły konstruktor kopiujący.- http://stackoverflow.com/questions/21858187/why-does-vector-emplace-back-call-the-move-constructor
- http://stackoverflow.com/questions/8001823/how-to-enforce-move-semantics-when-a-vector-grows
- http://stackoverflow.com/questions/26224112/why-does-stdvector-use-the-move-constructor-although-declared-as-noexceptfals
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Verbose {
Verbose(std::string) { cout << "Constructor" << endl; }
Verbose(const Verbose&) { cout << "Copy" << endl; }
Verbose(Verbose &&) /*noexcept*/ {
cout << "Move" << endl;
}
Verbose& operator =(const Verbose&) { cout << "assign=" << endl; }
Verbose& operator =(Verbose&&) noexcept { cout << "move=" << endl; }
~Verbose() noexcept { cout << "Destructor" << endl; }
};
int main() {
std::vector<Verbose> v;
v.emplace_back("aaa");
v.emplace_back("bbb");
}
Wynik:Constructor Constructor Copy Destructor Destructor Destructor
Uboczne działania destruktora
Move semantic niesie jeszcze jedno zwodnicze działanie, o którym należy pamiętać. Nigdy nie mamy pewności kiedy zostanie zawołany destruktor obiektu, z którego przenosimy zasoby. Jeżeli zdefiniowaliśmy własny destruktor, który ma jakieś działania uboczne, należy pamiętać o odpaleniu tego samego kodu w konstruktorze i operatorze move.Przykłady
Pora na przykład, który pokazuje różne scenariusze, z którymi mamy do czynienia kopiując bądź przesuwając obiekty. Na początku "gadatliwa" klasa, która pokazuje, który z mechanizmów copy-control został użyty.
#include <iostream>
#include <string>
#include <vector>
#include <functional>
using namespace std;
struct Storage {
Storage() : data("defaultData") {
cout << "Default constr.: " << data << endl;
}
Storage(std::string str) : data(str) {
cout << "Constructor: " << data << endl;
}
Storage(const Storage& st) : data(st.data) {
cout << "Copy constructor: " << st.data << endl;
}
Storage(Storage&& st) noexcept : data(std::move(st.data)) {
st.data = "old " + data + ", data was moved from here";
cout << "Move constructor: " << data << endl;
}
Storage& operator=(const Storage& st) {
if (&st != this) {
cout << "operator= copy: " << data << " := " << st.data << endl;
data = "old " + data + ", now assigned " + st.data;
} else {
cout << "operator= copy: " << data << " (this)" << endl;
}
return *this;
}
Storage& operator=(Storage&& st) noexcept {
if (&st != this) {
cout << "operator= move: " << data << " := " << st.data << endl;
string old_copy = data;
data = std::move(st.data);
st.data = "old " + data + ", data was moved from here";
data = "old " + old_copy + ", now assigned " + data;
} else {
cout << "operator= move: " << data << " (this)" << endl;
}
return *this;
}
~Storage() noexcept {
cout << "Destructor: " << data << endl;
}
std::string data;
};
W przykładach simpleExample5 i simpleExample6, rzutujemy (std::move) wartość do rvalue reference - jest to referencja, więc stare dane zostają na miejscu (nic nie jest przesuwane, ani kopiowane).void simpleExample1() {
Storage st("val_1");
st = st;
}
void simpleExample2() {
Storage st1("val_1");
Storage st2("val_2");
st1 = st2;
}
void simpleExample3() {
Storage st1("val_1");
Storage st2("val_2");
st1 = std::move(st2);
}
void simpleExample4() {
Storage st1("val_1");
Storage st = std::move(st1);
}
void simpleExample5() {
Storage st1("val_1");
Storage&& st = std::move(st1);
}
void simpleExample6() {
Storage&& st1 = std::move(Storage("val_1"));
}
int main() {
cout << "simpleExample1():" << endl;
simpleExample1();
cout << "simpleExample2():" << endl;
simpleExample2();
cout << "simpleExample3():" << endl;
simpleExample3();
cout << "simpleExample4():" << endl;
simpleExample4();
cout << "simpleExample5():" << endl;
simpleExample5();
cout << "simpleExample6():" << endl;
simpleExample6();
}
Wynik dla simpleExample*:simpleExample1(): Constructor: val_1 operator= copy: val_1 (this) Destructor: val_1 simpleExample2(): Constructor: val_1 Constructor: val_2 operator= copy: val_1 := val_2 Destructor: val_2 Destructor: old val_1, now assigned val_2 simpleExample3(): Constructor: val_1 Constructor: val_2 operator= move: val_1 := val_2 Destructor: old val_2, data was moved from here Destructor: old val_1, now assigned val_2 simpleExample4(): Constructor: val_1 Move constructor: val_1 Destructor: val_1 Destructor: old val_1, data was moved from here simpleExample5(): Constructor: val_1 Destructor: val_1 simpleExample6(): Constructor: val_1 Destructor: val_1W przykładzie returnValue1, kompilator sam się domyśli, że stworzona lokalnie (w ret()) zmienna zaraz zostanie zniszczona, więc zamiast korzystać z operatora przypisania (jak to było dawnej) zoptymalizuje tą operację przesuwając stworzoną zmienną lokalną. Ponieważ wyłączona jest optymalizacja "copy elision", widać dwa przesunięcia. Pierwsze przesuwa lokalną zmienną do zmiennej na stosie, drugie ze zmiennej na stosie do docelowej zmiennej st.
W drugim przypadku (returnValue2), mamy podobną sytuację, z tą różnicą, że zamiast "move operatora" działa "move konstruktor". Gdyby "copy elision" było włączone, kompilator prawdopodobnie wyciągnąłby wywołanie z tak prostej funkcji jak ret() i wywołał po prostu konstruktor w ciele naszego przykładu.
Storage ret() {
Storage tmp("local_val");
return tmp;
}
void returnValue1() {
Storage st("val_1");
st = ret();
}
void returnValue2() {
Storage st = ret();
}
int main() {
cout << "returnValue1():" << endl;
returnValue1();
cout << "returnValue2():" << endl;
returnValue2();
}
Wyniki dla returnValue*:returnValue1(): Constructor: val_1 Constructor: local_val Move constructor: local_val Destructor: old local_val, data was moved from here operator= move: val_1 := local_val Destructor: old local_val, data was moved from here Destructor: old val_1, now assigned local_val returnValue2(): Constructor: local_val Move constructor: local_val Destructor: old local_val, data was moved from here Move constructor: local_val Destructor: old local_val, data was moved from here Destructor: local_valOstatni przykład pokazuje, że do funkcji można przekazać parametr korzystając z std::move, a więc niejako "prosząc" o skorzystanie z mechanizmu move semantic. Gdyby parametr był oznaczony modyfikatorem const, nie było by to możliwe.
Storage set(Storage st_set) {
return st_set;
}
void settingValue1() {
Storage st1("val_1");
Storage st = set(st1);
}
void settingValue2() {
Storage st = set(Storage("val_1"));
}
void settingValue3() {
Storage st1("val_1");
Storage st = set(std::move(st1));
}
int main() {
cout << "settingValue1():" << endl;
settingValue1();
cout << "settingValue2():" << endl;
settingValue2();
cout << "settingValue3():" << endl;
settingValue3();
}
Wyniki dla settingValue*.settingValue1() - "copy elision" OFF: Constructor: val_1 Copy constructor: val_1 Move constructor: val_1 Move constructor: val_1 Destructor: old val_1, data was moved from here Destructor: old val_1, data was moved from here Destructor: val_1 Destructor: val_1 settingValue1() - "copy elision" ON: Constructor: val_1 Copy constructor: val_1 Move constructor: val_1 Destructor: old val_1, data was moved from here Destructor: val_1 Destructor: val_1 settingValue2(): Constructor: val_1 Move constructor: val_1 Move constructor: val_1 Move constructor: val_1 Destructor: old val_1, data was moved from here Destructor: old val_1, data was moved from here Destructor: old val_1, data was moved from here Destructor: val_1 settingValue3(): Constructor: val_1 Move constructor: val_1 Move constructor: val_1 Move constructor: val_1 Destructor: old val_1, data was moved from here Destructor: old val_1, data was moved from here Destructor: val_1 Destructor: old val_1, data was moved from hereDo funkcji pass możemy przekazać tylko rvalue reference (przez rzutowanie std::move), wszelkie próby wsadzenia parametru lvalue ("jeśli zmienna ma nazwę to jest lvalue") zakończą się błędem. Taki parametr najczęściej pojawia się w metodach bibliotecznych i informuje, że możemy ukraść zasoby ze zmiennej. Towarzyszy jej bliźniacza metoda (przyjmująca const T&) będąca wersją służącą do kopiowania zasobów.
Storage pass(Storage&& st) {
return st;
}
void passingValue1() {
Storage st1("val_1");
Storage st = pass(std::move(st1));
}
int main() {
cout << "passingValue1():" << endl;
passingValue1();
}
Wynik dla passingValue*:passingValue1(): Constructor: val_1 Copy constructor: val_1 Move constructor: val_1 Destructor: old val_1, data was moved from here Destructor: val_1 Destructor: val_1Próba zdefiniowania własnego konstruktora kopiującego, operatora przypisania albo destruktora kończy się tym, że kompilator nie wygeneruje za nas operatora i konstruktora move.
struct StorageDestructor : public Storage {
~StorageDestructor() noexcept {
cout << "Own destructor: " << data << endl;
}
};
void deriveWithDestructor1() {
StorageDestructor st1;
StorageDestructor st2;
st1 = std::move(st2);
}
int main() {
cout << "deriveWithDestructor1():" << endl;
deriveWithDestructor1();
}
Wynik dla deriveWithDestructor1:deriveWithDestructor1(): Default constr.: defaultData Default constr.: defaultData operator= copy: defaultData := defaultData Own destructor: defaultData Destructor: defaultData Own destructor: old defaultData, now assigned defaultData Destructor: old defaultData, now assigned defaultDataKolejny przykład. Choć klasa przez zdefiniowanie własnego destruktora zapobiega stworzeniu (implicit) konstruktora i operatora move, nadal mamy możliwość wymusić ich powstanie przez skorzystanie z default.
struct StorageWithDefaultMove : public Storage {
// Destruktor zapobiega niejawnemu wygenerowaniu operatora move
~StorageWithDefaultMove() noexcept { }
// Wymuszamy wygenerowania operatora move!
StorageWithDefaultMove& operator=(StorageWithDefaultMove&&) = default;
};
void dervieWithOwnMoveAssign1() {
StorageWithDefaultMove st1;
StorageWithDefaultMove st2;
st1 = std::move(st2);
}
int main() {
cout << "dervieWithOwnMoveAssign1():" << endl;
dervieWithOwnMoveAssign1();
}
Wynik dla dervieWithOwnMoveAssign1:dervieWithOwnMoveAssign1(): Default constr.: defaultData Default constr.: defaultData operator= move: defaultData := defaultData Destructor: old defaultData, data was moved from here Destructor: old defaultData, now assigned defaultData
Reference qualifier
Nowe definicje i mechanizmy wstecznej kompatybilności sprawiły, że biblioteka standardowa pozwala teraz na przypisywanie do rvalue! Czasami takie użycie może być zaskakujące. Poniższy kod jest prawidłowy.
string s1; string s2; s1 + s1 = "Hello world!";Możemy się przed tym ustrzec w naszych klasach, zmuszając by operand lewostronny (obiekt, na który wskazuje this), był lvalue. Pozwala na to reference qualifier (& dla lvalue, oraz && dla rvlaue), który stosuje się tak jak const, gdy chcemy oznaczyć metody, które nie mogą modyfikować pól obiektu.
Tak jak const, kwalifikatory te można stosować dla funkcji nie statycznych, muszą też znajdować się w deklaracji i definicji, a jeżeli występują razem z const zapisuje się je na końcu. Jeżeli definiujemy dwie lub więcej metod, które mają takie same nazwy i listę parametrów, musimy wprowadzić reference qualifier dla nich wszystkich, albo dla żadnej.
Przykład. Utworzona został operator=, ale tylko dla lvalue. Dodatkowo stworzone zostały dwie metody info, wołające różny kod w zależności od tego na jakim obiekcie zostały zawołane.
#include <iostream>
using namespace std;
class Foo {
public:
Foo operator+(const Foo &) {
return *this;
}
Foo &operator=(const Foo &) & {
return *this;
}
void info() const & {
cout << "info: lvalue" << endl;
}
void info() && {
cout << "info: rvalue" << endl;
}
};
Foo retFoo() {
return Foo();
}
int main() {
Foo f1;
Foo f2;
Foo f3;
// (f1 + f2) = f3; // Error - zapis do rvalue, ale brak takiego operatora
// retFoo() = f3; // Error - zapis do rvalue, ale brak takiego operatora
f1.info();
retFoo().info();
}
Wynik:info: lvalue info: rvalue
14 lutego 2014
[C++11] rvalue reference
Przebrnięcie przez move semantic wymaga wcześniejszego zapoznania się z rvalue reference (definicja wprowadzona już w C++98). Temat sprawił mi nie lada kłopot i z pewnością do niego powrócę jak tylko będę miał lepsze zrozumienie. Na razie notatki w takiej postaci i mam nadzieje, że nie ma w nich zbyt wielu błędów. Zbiór linków:
Najpierw definicja lvalue/rvalue, które znalazłem pod pierwszym linkiem:
Logika podpowiada, że wszystko co ma postać T&& można by nazwać rvalue reference. Niestety nie jest to prawda, zależy to bowiem od kontekstu w jakim ta postać istnieje. Zgodnie z nowym standardem wszędzie tam, gdzie mamy do czynienia z dedukcją typu (szablony, auto, typedef oraz pewne konstrukcje decltype) i jest symbol &&, czasami możemy mieć rvalue reference, a czasami lvalue reference. Scott Meyers, proponuje swoją terminologię i nazywanie takich referencji universal reference, bowiem taka referencja może pokazywać na "wszystko": lvalue, rvalue, const, non-const, ...
Kiedy parametr funkcji jest typu T&&, a dodatkowo mamy do czynienia z dedukcją typu (bo funkcja jest szablonowa), to typ może ulec zmianie. Standard przewiduje trzy wyjątki (w książce dwa).
Przykłady:
W C++11 wraz z wprowadzeniem move semantic, liczba kategorii została rozbudowana:
Ich właściwości można podsumować w ten sposób:
- http://thbecker.net/articles/rvalue_references/section_01.html
- http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Scott-Meyers-Universal-References-in-Cpp11
- http://en.cppreference.com/w/cpp/language/reference
- http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
rvalue reference
Najpierw definicja lvalue/rvalue, które znalazłem pod pierwszym linkiem:
lvalue jest wyrażeniem, które odnosi się do jakiegoś miejsca w pamięci i możemy uzyskać adres tego miejsca korzystając z operatora &. rvalue jest wyrażeniem, które nie jest lvalue.Można też stosować definicję "if it has a name ..." - link.
Jeżeli zmienna posiada nazwę to możemy uzyskać adres tej zmiennej, dzięki operatorowi &, wtedy wiem, że jest to lvalue. Nie możemy np. uzyskać nazwy literału (liczba 42), więc wiemy, że jest to rvalue.lvalue reference mają trwały stan, tymczasem rvalue reference są albo literałami, albo tymczasowymi obiektami utworzonymi w czasie ewaluowania wyrażenia. Mają zatem ważne właściwości [C++Primer - s.533]:
- Odnoszą się tylko do obiektów, które zostaną zaraz zniszczone
- Nie ma innych użytkowników danego obiektu
#include <iostream>
using namespace std;
int main() {
int i = 42; // zmienna i jest lvalue
int& lv1 = i; // lvalue reference - referencja na i
// int& lv2 = i * 5; // i*5 to rvalue (stała) nie możemy zrobić referencji która pozwala na jej modyfikację
const int& lv3 = i * 5; // lvalue reference - możemy, ustawić referencję na stałą
// int&& rv1 = i; // rvalue reference - nie może pokazywać na lvalue
auto&& lv4 = i; // lvalue reference - następuje dedukcja typu na "int&"
int&& rv2 = 42; // rvalue reference - 42 to literał
int&& rv3 = i * 3; // rvalue reference - wynik mnożenia to ulotna wartość (rvalue)
// int&& rv4 = rv2; // rv2 nie jest ulotne i jest ważne do końca zakresu!
int&& rv5 = std::move(rv2); // rvalue reference - obietnica, że nie będziemy korzystać z rv2!
}
Reference collapsing i dedukcja typu
Logika podpowiada, że wszystko co ma postać T&& można by nazwać rvalue reference. Niestety nie jest to prawda, zależy to bowiem od kontekstu w jakim ta postać istnieje. Zgodnie z nowym standardem wszędzie tam, gdzie mamy do czynienia z dedukcją typu (szablony, auto, typedef oraz pewne konstrukcje decltype) i jest symbol &&, czasami możemy mieć rvalue reference, a czasami lvalue reference. Scott Meyers, proponuje swoją terminologię i nazywanie takich referencji universal reference, bowiem taka referencja może pokazywać na "wszystko": lvalue, rvalue, const, non-const, ...
Kiedy parametr funkcji jest typu T&&, a dodatkowo mamy do czynienia z dedukcją typu (bo funkcja jest szablonowa), to typ może ulec zmianie. Standard przewiduje trzy wyjątki (w książce dwa).
- Dla argumentu będącego lvalue => T będzie lvalue referencją (czyli T&)
- Dla argumentu będącego rvalue => T będzie po prostu T - o tej zasadzie wspomina Meyers w swoim wykładzie [strona 26], nie znalazłem tego w książce.
- Jeśli stworzymy referencję do referencji (choć nie bezpośrednio) wtedy ta referencja się "zapada". rvalue reference zapada się do rvalue reference, w pozostałych przypadkach zapadanie jest do lvalue reference.
- T& & => T&
- T& && => T&
- T&& & => T&
- T&& && => T&&
Przykłady:
#include <iostream>
// T&& to universal reference
template <typename T>
void f(T&& t, std::string name, std::string descr) {
std::cout << descr << std::endl;
if (std::is_const<typename std::remove_reference<decltype(t)>::type>::value)
std::cout << name << " is const" << std::endl;
if (std::is_lvalue_reference<decltype(t)>::value)
std::cout << name << " is lvalue reference" << std::endl;
if (std::is_rvalue_reference<decltype(t)>::value)
std::cout << name << " is rvalue reference" << std::endl;
std::cout << std::endl;
}
int main() {
f(42, "42", "f<int>(int &&) => f<int>(int&&)");
int v1 = 42; // lvalue
f(v1, "v1", "f<int&>(int& &&) => f<int&>(int&)");
const int v2 = 42; // const lvalue
f(v2, "v2", "f<const int&>(const int& &&) => f<const int&>(const int&)");
int tmp = 42; // lvalue
int& v3 = tmp; // lvalue reference
f(v3, "v3", "f<int&>(int& &&) => f<int&>(int&)");
const int& v4 = 42; // lvalue reference na const
f(v4, "v4", "f<const int&>(const in& &&) => f<const int&>(const int&)");
int&& v5 = 42; // rvalue reference
if (std::is_rvalue_reference<decltype(v5)>::value)
std::cout << "in main v5 if rvalue reference" << std::endl;
f(v5, "v5", "f<int&>(int& &&) => f<int&>(int&)");
const int&& v6 = 42; // rvalue reference na const
if (std::is_rvalue_reference<decltype(v6)>::value)
std::cout << "in main v6 if rvalue reference" << std::endl;
f(v6, "v6", "f<const int&>(const int& &&) => f<const int&>(const int&)");
int v7 = 67;
f(std::move(v7), "v7", "f<int>(int &&) => f<int>(int&&)");
}
Chociaż v5 i v6 to rvalue referencje, dzięki nazwie możemy uzyskać ich adres, więc w środku funkcji f(), będą to już lvalue referencje. Aby przekazać rvalue referencję do środka, trzeba skorzystać z std::move. Wynik:f<int>(int &&) => f<int>(int&&) 42 is rvalue reference f<int&>(int& &&) => f<int&>(int&) v1 is lvalue reference f<const int&>(const int& &&) => f<const int&>(const int&) v2 is const v2 is lvalue reference f<int&>(int& &&) => f<int&>(int&) v3 is lvalue reference f<const int&>(const in& &&) => f<const int&>(const int&) v4 is const v4 is lvalue reference in main v5 if rvalue reference f<int&>(int& &&) => f<int&>(int&) v5 is lvalue reference in main v6 if rvalue reference f<const int&>(const int& &&) => f<const int&>(const int&) v6 is const v6 is lvalue reference f<int>(int &&) => f<int>(int&&) v7 is rvalue referenceZ czterech form, dla których może następować dedukcja typu, trzy zachowują się tak samo: typ szablonowy, auto oraz typedef. Niestety decltype, posiada pewne wyjątki od tej reguły, ale w tej chwili, nie chce mi się w to zagłębiać. Może kiedyś.
Value categories
W C++11 wraz z wprowadzeniem move semantic, liczba kategorii została rozbudowana:
- https://en.cppreference.com/w/cpp/language/value_category
- https://cpp-polska.pl/post/podzial-wyrazen-ze-wzgledu-na-kategorie-wartosci-w-c
- https://www.youtube.com/watch?v=yIOH95oIKbo
@startuml "value_categories.png" (expression) --> (glvalue) (glvalue) --> (lvalue) (glvalue) --> (xvalue) (expression) --> (rvalue) (rvalue) --> (xvalue) (rvalue) --> (prvalue) @enduml
Ich właściwości można podsumować w ten sposób:
- glvalue ("generalized" lvalue)
- mają nazwę
- rvalue (nazwa historyczna, bo mogły się pojawić po prawej stronie operatora przypisania)
- można z nich przenosić
- lvalue (nazywa historyczna, bo mogły się pojawić po lewej stronie operatora przypisania)
- mają nazwę i można z nich przenosić
- xvalue ("eXpiring" value)
- mają nazwę i można z nich przenosić
- prvalue (pure rvalue)
- nie mają nazwy i można z nich przenosić
- wywołanie funkcji, której wynikiem jest rvalue reference (np. std::move(x) zwraca rvalue reference do x)
- a[n], wyrażenie, gdzie a jest tablicą rvalue
- a.m, wyrażenie, gdzie a jest rvalue i m jest składową non-static typu non-reference
- wyrażenie rzutowania do rvalue reference (np. static_cast<char&&>(x))
30 kwietnia 2013
Constructor() = default/delete;
Podstawy
W poprzedniej wersji C++, jeżeli klasa potrzebowała explicit konstruktorów np. domyślnego, który nic nie robił, trzeba było napisać coś w tym stylu:class MyClass
{
public:
MyClass() {}
};
Zalecane jest jednak by interfejs zawierał deklaracje publicznych metod bez ich implementacji. Rozwiązaniem jest przeniesienie definicji do pliku cpp, jednak to tylko niepotrzebnie zwiększa ilość kodu. MyClass::MyClass() {}
W nowym standardzie, kompilator wygeneruje za nas domyślny konstruktor bez potrzeby pisania implementacji, jeśli tylko użyjemy słowa default. Podobnie możemy korzystać z delete, gdy nie chcemy jakiegoś konstruktora dla naszej klasy, oraz nie chcemy by kompilator wygenerował konstruktor domyślny. Musimy więc "wykasować" konstruktor domyślny.#include <iostream>
class MyClass {
public:
MyClass() = default;
MyClass(const MyClass& r) = delete;
MyClass(int v) : value(v) {}
int value;
};
int main()
{
MyClass m1; // default constructor
MyClass m2(44);
// MyClass m3 = m2; // copy constructor - Error z powodu "delete"
std::cout << "m1 = " << m1.value << std::endl;
std::cout << "m2 = " << m2.value << std::endl;
// std::cout << "m3 = " << m3.value << std::endl;
return 0;
}
Wyniki:m1 = 134515449 m2 = 44Możemy też w ten sposób stworzyć operatora przypisania, który domyślnie zrobi płytką kapię zawartości obiektu.
#include <iostream>
#include <vector>
using namespace std;
class MyClass {
public:
MyClass() = default;
MyClass(const MyClass& r) = delete;
MyClass& operator=(const MyClass& r) = default;
void put(int value) {
vec.push_back(value);
}
void show() const {
for (const auto& v : vec)
cout << v << " ";
cout << endl;
}
private:
vector<int> vec;
};
int main()
{
MyClass m1;
m1.put(1);
m1.put(2);
MyClass m2;
m2 = m1;
m2.show();
return 0;
}
Wynik:1 2Operacje typu default traktowane są jako user-declared, więc np. klasa bez deklaracji destruktora nie jest taka sama jak klasa z destruktorem default. Przykład poniżej.
Można także usunąć funkcję, wtedy zabraniamy niejawnej konwersji argumentów w momencie jej zawołania.
#include <iostream>
bool convert(int val) {
return true;
}
bool convert(double vale) = delete;
int main()
{
convert(44);
convert(12.3); // Error
return 0;
}
Kiedy kompilator generuje za nas kod
Zasady automatycznego generowania konstruktorów domyślnych/kopiujących, operatorów przypisani/przesunięcia i destruktorów przez kompilator:- Jeżeli konstruktor domyślny, konstruktor kopiujący, operator przypisania albo destruktor w klasie bazowej jest skasowany (delete) lub niedostępny (np. przez private), wtedy odpowiadający mechanizm w klasie pochodnej zostanie zdefiniowany jako skasowany (delete).
- Jeżeli klasa bazowa ma niedostępny albo skasowany (delete) destruktor wtedy zsyntetyzowany konstruktor domyślny i konstruktor kopiujący w klasie pochodnej są zdefiniowane jako delete.
- Jeżeli klasa bazowa ma niedostępny albo skasowany (delete) destruktor wtedy move konstruktor zostanie zdefiniowany jako delete.
- Jeżeli operacje move, w klasie bazowej są skasowana albo niedostępna, to pomimo użycia default w klasie pochodnej, odpowiednie operacje nadal będą skasowane (ponieważ składowych klasy bazowej nadal nie można przenieść).
Wirtualny destruktor = default
Typowym sposobem tworzenia interfejsów w C++ jest wykorzystanie klas czysto wirtualnych (Base). Specyfika języka wymaga, aby w klasie takiej znalazł się również zdefiniowany wirtualny destruktor. Teraz zamiast tworzyć jego definicję można posłużyć się default.struct Base {
virtual ~Base() = default;
virtual void fun() = 0;
};
struct Child : public Base {
void fun() override { cout << "Child::fun" << endl; }
Data data;
};
Nasuwa się pytanie, na które wciąż nie mam wystarczająco dobrej odpowiedzi. Czy user-declared (default) destruktor sprawi, że nie będzie można skorzystać z move semantic? Stworzyłem kilka eksperymentów i spróbowałem sobie zracjonalizować, dlaczego uzyskałem takie, a nie inne wyniki. W pierwszej kolejności potrzebna jest gadatliwa klasa:#include <iostream>
using namespace std;
struct Data {
Data() = default;
Data(const Data&) {
cout << "Copy constructor" << endl;
}
Data(Data&&) noexcept {
cout << "Move constructor" << endl;
}
Data& operator=(const Data&) {
cout << "Copy assign" << endl;
return *this;
}
Data& operator=(Data&&) noexcept {
cout << "Move assign" << endl;
return *this;
}
~Data() = default;
};
Dziedziczenie "nie uszkadza" (nie kasuje) mechanizmów move semantic w klasie pochodnej. No prawie. Ponieważ klasa Base1 jest czysto wirtualna i nie posiada żadnych danych, więc nie ma też sensu, by narzucać jakieś ograniczenia klasie z niej dziedziczącej w kwestii kopiowania lub przesuwania.struct Base1 {
virtual ~Base1() = default;
virtual void fun() = 0;
};
struct Child1 : public Base1 {
void fun() override { cout << "Child1::fun" << endl; }
Data data;
};
int main() {
Child1 c1;
auto m1 = std::move(c1);
}
Bez problemu zadziała konstruktor move. Wynik:Move constructorCo się jednak stanie, gdy dane zostaną przeniesione do klasy Base (Base2)?
struct Base2 {
virtual ~Base2() = default;
virtual void fun() = 0;
Data data;
};
struct Child2 : public Base2 {
void fun() override { cout << "Child2::fun" << endl; }
};
int main() {
Child2 c2;
auto m2 = std::move(c2);
}
Wynik:Copy constructorCo ciekawe teraz move semantic już nie działa, za to działa kopiowanie. W pierwszej kolejności myślałem, że user-declared destruktor kasuje konstruktor move, gdy tylko w klasie znajdują się jakieś dane, ale to dalej nie jest do końca prawda.
Jeżeli user-declared destruktor kasowałby konstruktor move, to kod w którym konstruktor move jest explicit wykasowany (delete) powinien zachowywać się w identyczny sposób. W przykładzie poniżej Base3 się jednak nie skompiluje. Kompilator poinformuje o braku konstruktora move, ale także o braku konstruktora kopiującego. Musiałem więc stworzyć znacznie bardziej rozbudowaną wersją (Base4) z wykasowanym (delete) operatorem move i default-owym konstruktorem kopiującym.
//struct Base3 {
// Base3(Base3&&) = delete; // Error
// virtual ~Base3() = default;
//
// virtual void fun() = 0;
// Data data;
//};
//struct Child3 : public Base3 {
// void fun() override { cout << "Child3::fun" << endl; }
//};
struct Base4 {
Base4() = default;
Base4(Base4&&) = delete;
Base4(const Base4&) = default;
virtual ~Base4() = default;
virtual void fun() = 0;
Data data;
};
struct Child4 : public Base4 {
void fun() override { cout << "Child4::fun" << endl; }
};
int main() {
// Child3 c3;
// auto m3 = std::move(c3); // Error
Child4 c4;
auto m4 = std::move(c4);
}
Wynik:Copy constructorW takim razie skoro user-declared destruktor kasuje move konstruktor, to dlaczego nie kasuje także konstruktora kupującego, w końcu w Base4 wykasowany move konstruktor właśnie to uczynił? Najlepsza odpowiedź jaką uzyskałem, to taka, że default destruktor nie tyle kasuje move konstruktor, ale zapobiega jego deklaracji. Mało to intuicyjne jak dla mnie.
Jak widać reguły jakimi rządzi się ten mechanizm są bardzo skomplikowane. Jak więc powinna wyglądać prawidłowo taka klasa. Zalecaną metodą jest deklaracja wszystkich pięciu elementów: The rule of three/five/zero. Jeżeli potrzebujesz choćby jednego zadeklaruj wszystkie.
struct Base0 {
Base0() = default;
Base0(Base0&&) = default;
Base0(const Base0&) = default;
Base0& operator=(const Base0&) = default;
Base0& operator=(Base0&&) = default;
virtual ~Base0() = default;
virtual void fun() = 0;
Data data;
};
struct Child0 : public Base0 {
void fun() override { cout << "Child0::fun" << endl; }
};
int main() {
Child0 c0;
auto m0 = std::move(c0);
}
Wynik:Move constructor
Na koniec jeszcze jeden przypadek, którego już zupełnie nie rozumiem. Jest to zmodyfikowana wersja klasy Base4. Nie jest ona jednak już czysto wirtualna (funkcja fun ma definicję), można więc tworzyć obiekty tej klasy.
struct BaseX {
BaseX() = default;
BaseX(BaseX&&) = delete;
BaseX(const BaseX&) = default;
virtual ~BaseX() = default;
virtual void fun() { cout << "BaseX::fun" << endl; }
Data data;
};
struct ChildX : public BaseX {
void fun() override { cout << "ChildX::fun" << endl; }
};
int main() {
BaseX bx;
auto mb = std::move(bx); // Error
ChildX cx; // To samo co Child4
auto mc = std::move(cx);
}
ChildX niczym nie różnic się od Child4 i tak jak poprzednio std::move zwoła konstruktor kopiujący. Kompilator jednak zaprotestuje, gdy będziemy chcieli zawołać std::move na obiekcie BaseX. Co prawda nie ma konstruktora move, ale dlaczego w zamian nie zostanie zawołany konstruktor kopiujący?main.cpp: In function ‘int main()’:
main.cpp: error: use of deleted function ‘BaseX::BaseX(BaseX&&)’
auto mb = std::move(bx);
^
main.cpp: note: declared here
BaseX(BaseX&&) = delete;
^~~~~~
Subskrybuj:
Posty (Atom)
