TDIU20 -
Objektorienterad
programmering i c++ - föreläsning 5
Pontus Haglund
Department of Computer and information science
Destruktor Kopiering
Kopieringstilldelening Flytt
Flyttilldelning
2 När genereras de inte?
Rule of three Rule of six Rule of zero Djup kopiering Korrekt flytt
Destruktor Kopiering
Kopieringstilldelening Flytt
Flyttilldelning
2 När genereras de inte?
Rule of three Rule of six Rule of zero Djup kopiering Korrekt flytt
Speciella medlemsfunktioner
• Du har redan stött på några!
• Funktioner som automatiskt genereras av konstruktorn
Vilka är de?
• Defaultkonstruktor
• Destruktor
• Kopieringskonstruktor
• Kopieringstilldelningsoperator
• Flyttkonstrukor
• Flyttilldelningsoperator
Defaultkonstruktor
• Finns om ingen annan
konstruktor explicit deklarerats
• Så vad gör default konstruktor?
• Kallar basklassernas defaultkonstruktorer
• Kallas datamedlemmarnas defaultkosntruktorer
• Samma beteende som en användardefinierad konstruktor med tom initieringslista och kropp
// Vad händer?
class Foo { public:
Foo() {
cout <<"Constructor called"<< endl;
} };
class Bar { private:
Foo f;
};
int main() {
Bar c{};
}
Destructor
• Anropas vid slutet av ett objekts livstid (lifetime)
• När är detta?
• När programmet avslutas (static)
• End of scope (slut av block) för sånt på stacken
• delete-expression för saker vi skapat med new-expression
• slutet av uttrycket för anonyma temporära data
• Och några fler fall...
Destruktor automatic duration
classBar {
public:
~Bar() {
cout <<"Destructor called"<< endl;
} };
intmain() {
{
Bar c{};
}
cout <<"end of main"<< endl;
}
Destruktor dynamic storage duration
classBar { public:
~Bar() { cout << "Destructor called"<< endl; } };
intmain() {
Bar* pb = newBar{};
cout <<"end of main"<< endl;
}
Destructor member
classFoo { public:
~Foo() { cout << "Foo Destructor called"<< endl; } private:
};
classBar { public:
~Bar() { cout << "Bar Destructor called"<< endl; } private:
Foo f{};
};
intmain() {
Bar* pb = newBar{};
delete pb;
cout <<"end of main"<< endl;
}
Destructor dynamic member
classFoo { public:
~Foo() { cout << "Foo Destructor called"<< endl; } };
classBar { public:
~Bar() {
cout <<"Bar Destructor called"<< endl;
} private:
Foo* f =new Foo{};
};
intmain() {
Bar* pb = newBar{};
delete pb;
cout <<"end of main"<< endl;
}
Destructor nameless temporaries
classBar { public:
~Bar() {
cout <<"Bar Destructor called"<< endl;
}
int operator+(int rhs) {
returni + rhs;
} // ...
private:
int i{};
};
intmain() {
// Vad är skillnaden?
Bar b{};
b + 4;
Bar{} + 4;
cout <<"end of main"<< endl;
}
Copy or copy initialization
Intialize an object from another object
- Cppreference
Value semantics
• Value semantics
• Kallas ibland copy semantics
• Tilldening kopierar värden, inte bara pekaren
• Reference semantics
• Tilldening kopierar pekaren
• Båda variablerna blir referenser till ett och samma värde
C++ har den första varianten som beteende och det är väll bra!?
Kopieringskonstruktor
Bar b{}; //Initiera objektet b
Bar bc{b};//Initiera objektet bc med b's datamedlemmar
• Med hjälp av ett annat objekt initializera detta objekt
• Vad är default beteende?
• Varje datamedlem i det nya objektet initieras med motsvarande datamedlem från det andra objektet
• Båda objekten är i valid-state efter kopiering
• Default finns om ingen flyttkonstruktor eller flyttildelningsoperator är deklarerad
Skriv din egen konstruktor
classBar { public:
Bar() : i{0} {}
Bar(Barconst& rhs) : Bar{rhs.get_i()}
{
cout <<"Bar copy constructor called"<< endl;
}
int get_i() { returni; } //...
private:
int i{};
};
intmain() {
Bar b{};
Bar bc{b};
cout <<"end of main" << endl;
}
Operator för kopieringstilldelning
Bar b{};
Bar bc = b;
• Samma default regler och beteende som kopieringskonstruktorn
Skriv din egen operator
classBar { public:
Bar() : i{0} {}
Bar&operator = (Barconst& rhs) {
i = rhs.get_i();
cout <<"Bar copy assignment operator called" << endl;
return *this;
}
int get_i() { returni; } //...
private:
int i{};
};
intmain() {
Bar b{};
Bar bc{b};
cout <<"end of main" << endl;
}
Move
• Initiera ett objekt med ett annat objekt
• Men lite annorlunda från copy
• xvalue?
Flyttkonstruktor
Bar bm{Bar{}};
Bar b{};
Bar bem{std::move(b)};
• Initierar ett objekt med hjälp av ett annat
• Flyttar datamedlemmarna från det en objektet till det andra
• Alltså bevaras inte original objektet
• Båda objekten är i valid state efter operationen
• Båda objekten är inte i ett specificerat state
• Default genereras om ingen kopieringskonstruktor, operator och destruktor finns
Skriv din egen konstruktor
classBar { public:
Bar() : i{0} {}
Bar(Barconst&& rhs) : Bar(std::exchange(rhs.i, 0)) {
cout <<"Bar move constructor called"<< endl;
}
int get_i()const{ returni; }
private:
int i{};
};
intmain() {
Bar b{};
Bar bc{move(b)};
cout <<"end of main"<< endl;
}
Flyttilldelningsoperator
Bar bm{Bar{}};
Bar b{};
Bar bem = std::move(b);
• Initierar ett objekt med hjälp av ett annat
• Flyttar datamedlemmarna från det en objektet till det andra
• Alltså bevaras inte original objektet
• Båda objekten är inte i valid state efter operationen
• Default genereras om ingen kopieringskonstruktor, operator och destruktor finns
Skriv din egen operator
classBar { public:
Bar() : i{0} {}
Bar(intn) : i{n} {}
Bar&operator= (Bar && rhs) {
cout <<"Before move"<< endl;
i = std::exchange(rhs.i, 0);
cout <<"After move"<< endl;
return*this;
}
int get_i()const{ returni; }
private:
int i{};
};
intmain() {
Bar b{};
Bar bc{move(b)};
cout <<"end of main"<< endl;
}
Destruktor Kopiering
Kopieringstilldelening Flytt
Flyttilldelning
2 När genereras de inte?
Rule of three Rule of six Rule of zero Djup kopiering Korrekt flytt
Rule of three
Om en klass har användardefinerat (non implicit) beteende för kopieringskonstruktor,
tilldelningsoperatorn eller destruktorn så behöver den nästan säkerligen användardefinerat beteende för alla tre.
- cppreference.com (översatt)
Varför?
• Används i många olika situationer
• Exempelvis vid parameteröverföring (by value)
• Implicita varianter klarar inte råpekare (annat)
• Leder ofta till inkorrekt beteende
Om du inte följer denna regel blir det förmodligen fel
Rule of five (six?)
Om en klass har användardefinerat beteende för kopiering, tilldelning och destruering så förhindrar det implicit definition av flyttkonstruktor och
fylttilldelningsoperator. Därför måste man definera sina egna om man vill ha flyttsematik.
Varför
• C++ använder flyttsemantik för att optimera programkod
• Det är mycket effektivare beteende att flytta ett objekt än att kopiera det
• Om defaultbeteende inte duger för de tre tidigare duger det inte för flytt heller
Blir förmodligen inte fel, men dåligt optimerat
Rule of zero
Om du kan, undvik att definera några speciella medlemsfunktioner.
Extra hänsynstagande
På grund av hur arv fungerar (se nästa
labb/föreläsning) förenklas ofta dessa regler till följande:
• Om du definerar eller tar bort en av dessa funktioner, definera eller ta bort alla 5.
Alla dessa regler kommer från cpp core guidlines som man kan titta i om man vill:
https://github.com/isocpp/CppCoreGuidelines
= default, = delete
• Nyckelord för att explicit berätta att speciella medlemsfunktioner inte finns eller har defaultbeteende
//Bar.h //...
classBar { public:
Bar() =delete; //No default constructor
~Bar() =default; //Default destruktor
Bar(Bar&) = delete; //No copy constructor
Bar(Bar&&); //User-defined move constructor
Bar&operator=(Bar&) = delete; //No copy assignment operator Bar&operator=(Bar&&); //User-defined move assignment //..
};
Djup kopiering
• Blir kopian rätt?
• Pass by value
#include <iostream>
using namespacestd;
classBar { public:
Bar(): ptri{nullptr} {}
Bar(int* ptr): ptri{ptr} {}
int* get_ptr() {return ptri; } private:
int* ptri;
};
int main() {
Bar b{new int{5}};
Bar bc{b};
++(*b.get_ptr());
cout << *b.get_ptr() << *bc.get_ptr() << endl;
cout <<"end of main"<< endl;
}
Djup kopiering
• Blir kopian rätt?
#include <iostream>
using namespacestd;
class Bar { public:
Bar(): ptri{nullptr} {}
Bar(int* ptr): ptri{ptr} {}
Bar(Barconst& rhs): Bar{new int{*rhs.get_ptr()}} {}
int* get_ptr() const{ returnptri; } private:
int* ptri;
};
int main() {
Bar b{new int{5}};
Bar bc{b};
++(*b.get_ptr());
cout << *b.get_ptr() << *bc.get_ptr() << endl;
cout <<"end of main"<< endl;
}
Korrekt flytt
Hur implementerar vi korrekt flytt?