Föreläsning 8

Föreläsning 8
Undantag, konstruktorer, mallar
TDDD86: DALP
Utskriftsversion av föreläsning i Datastrukturer, algoritmer och programmeringsparadigm
24 september 2015
Tommy Färnqvist, IDA, Linköpings universitet
8.1
Innehåll
Innehåll
1
Undantag
2
Konstruktion, tilldelning och flytt
2.1 Varför konstruktorer? . . . . .
2.2 Defaultkonstruktor . . . . . .
2.3 Typomvandlande konstruktor .
2.4 Kopieringskonstruktor . . . .
2.5 Kopieringstilldelningsoperator
2.6 Flyttsemantik . . . . . . . . .
3
1
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Mallar
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
3
4
4
5
7
10
8.2
Undantag
Problem: size vs capacity
• Vad händer om klienten kommer åt ett element på plats större än size? list.get(7)
– Nu tillåter listan detta och returnerar 0
∗ Är det bra eller dåligt? Vad (om något) bör vi göra åt det?
8.3
Felutskrifter och sentinelvärden
• Felutskrift
– Skriv ut ett felmeddelande och avbryt programmet
∗ Lite väl drastiskt, programmet får ingen chans att återhämta sig från problemet
• Sentinelvärde
– Returnera ett speciellt värde som indikerar att något gått fel, t.ex. -1
∗ Problemet är att alla heltal kan förekomma som värden i listan!
8.4
1
Previllkor
• previllkor: Något din kod antar är sant när den anropas.
– Ofta dokumenterat som en kommentar vid funktionens huvud
/*
* Returns the element at the given index.
* Precondition: 0 <= index < size
*/
int ArrayList::get(int index) {
return m_elements[index];
}
– Att ange ett previllkor “löser” egentligen inte problemet, men det dokumenterar vårt val och
varnar klienten.
– Men om klienten inte lyssnat och använder ett dåligt index ändå?
– Kan vi se till att klienten måste följa previllkoret?
8.5
Kasta undantag
• throw expression;
– Genererar ett undantag som kraschar programmet om det inte har kod för att hantera (catch)
undantaget
– I Java kan man bara kasta objekt som är Exceptions; i C++ kan alla typer av värden kastas
(int, string, etc.)
throw 0;
throw new vector<double>;
throw 3.14159;
// Kasta en int
// Kasta en vector<double> *
// Kasta en double
– Det finns en klass std::exception att använda
try {
// Gör något
}
catch(int myInt) {
// Om koden kastar en int fortsätter exekveringen här
}
catch(const exception& e) {
// Om koden kastar ett standardundantag fortsätter exekveringen här
cout << "exception: " << e.what() << ’\n’;
}
catch(...) { // Specialsyntax för att fånga alla typer av undantag
cout << "An unexpected error has ocurred!\n";
}
8.6
Standardundantag
8.7
2
Privat hjälpfunktion
// I ArrayList.h
private:
void checkIndex(int i, int min, int max) const;
// I ArrayList.cpp
void ArrayList::checkIndex(int i, int min, int max) const {
if (i < min || i > max) {
throw std::out_of_range("Index " + std::to_string(i)
+ " out of range; (must be between "
+ std::to_string(min) + " and "
+ std::to_string(max) + ")");
}
}
8.8
2
Konstruktion, tilldelning och flytt
2.1
Varför konstruktorer?
Initiering vs tilldelning
• Initiering är fundamentalt i C++ och skiljt från tilldelning
– Initiering transformerar ett objekts initiala skräpdata till giltigt data.
∗ Definieras av typens konstruktor
– Tilldelning ersätter existerande giltigt data med annat giltigt data
∗ Definieras av typens tilldelningsoperator
// Initiering: Defaultkonstruktor
Widget x;
// Initiering: Kopieringskonstruktor
Widget y(x);
// Initiering: Kopieringskonstruktor (alternativ form)
Widget z = x;
// Tilldelning: Kopieringstilldelning
z = x;
8.9
Notera
• Det är inte alltid nödvändigt att definiera alla sorters konstruktorer och tilldelningsoperatorer. Om du
inte gör det kommer kompilatorn att skapa en defaultversion åt dig.
8.10
2.2
Defaultkonstruktor
Defaultkonstruktor
ArrayList() = default;
• En defaultkonstruktor är en konstruktor som anropas utan argument
ArrayList list;
// variabeldeklaration utan initierare
• = default innebär att konstruktorn genereras av kompilatorn
– medlemmar av enkel typ initieras endast om de har en initierare i sin deklaration
– medlemmar av klasstyp initieras av någon konstruktor beroende på hur vi kodat, defaulkonstruktorn om inte annat
• De icke-statiska datamedlemmarna initieras av respektive NSDMI (“non-static data member initializer”)
int m_size = 0;
int m_capacity = 10;
int* m_elements = new int[m_capacity];
8.11
3
Medlemsinitierarlista
Förekommer inte i ArrayList men är en grundläggande konstruktion
• En egen defaultkonstruktor hade kunnat skrivas
ArrayList()
: m_size{ 0 }, m_capacity { 10 }, m_elements { new int[m_capacity] }
{}
• Om deklarationen av en datamedlem har en initerare utförs den inte om det finns en medlemsinitierare
• Medlemsinitierarlistan låter oss initiera (inte tilldela) datamedlemmar när vi initierar vår typ
// Tilldelning
struct Widget {
const int value;
Widget();
};
Widget::Widget() {
value = 42; //ERROR
}
// Initiering
struct Widget {
const int value;
Widget();
};
Widget::Widget()
: value{42}
{}
8.12
2.3
Typomvandlande konstruktor
Typomvandlande konstruktor
ArrayList::ArrayList(const vector<int>& v) {
for (auto vi : v) {
add(vi);
}
}
• En konstruktor som kan anropas med ett argument av annan typ är en typomvandlande konstruktor
ArrayList list(vector<int> {23, 24, 25, 26});
– syntaxen ovan kallas direktinitiering
• Följande deklarationssyntax kallas kopieringsinitiering
ArrayList a1 = a2; // samma typ - kopieringskonstruktorn gör initiering
ArrayList a3 = vector<int>{1, 2, 3}
// implicit typomvandling
ArrayList a4 = ArrayList(vector<int>{1, 2, 3}) // explicit typomvandling
– flyttkonstruktorn gör initieringen i de två senare fallen
– optimering kan förekomma. . .
• Alla explicita typomvandlingar sker med denna konstruktor, exempelvis
auto a5 = static_cast<ArrayList>(vector<int> {23, 24, 25, 26});
8.13
2.4
Kopieringskonstruktor
Kopieringsinitiering
I C++ kan kopieringsinitiering ske på tre olika sätt:
1. En variabel skapas som en kopia av ett existerande värde
MyClass one;
MyClass two = one;
Koden ovan är ekvivalent med
MyClass one;
MyClass two(one);
2. Värdeöverföring av ett objekt till en funktion
void myFunction(MyClass arg) {
...
}
MyClass mc;
myFunction(mc);
4
3. Ett objekt returneras som värde från en funktion
MyClass myFunction() {
MyClass mc
return mc;
}
8.14
Kopieringskonstruktorn
Kopieringsinitiering i C++ sker med hjälp av kopieringskonstruktorn:
• Syntaktiskt skrivs kopieringskonstruktorn som en konstruktor som tar en parameter — en instans av
klassen överförd som referens till const
class MyClass {
public:
MyClass();
~MyClass();
MyClass(const MyClass& other); // Copy constructor
/* ... */
};
• Den kompilatorgenererade kopieringskonstruktorn gör medlemsvis kopiering. . .
8.15
Grund kopieringsbugg
• Kopieringsinitiering av vår ArrayLista orsakar problem
• En ändring i en lista påverkar den andra (dåligt!)
list2.add(88);
list1.remove(0);
– När objekten förstörs frigörs minnet två gånger (dåligt!)
8.16
Djup kopiering
• För att komma till rätta med kopieringsbuggen behöver vi skriva en kopieringskonstruktor som gör
en djup kopiering av ArrayListan
• Rule of Three: Om en klass har någon av följande tre medlemsfunktioner:
– Destruktor
– Kopieringskonstruktor
– Kopieringstilldelningsoperator
så bör den antagligen ha alla tre funktionerna.
8.17
Förbjud kopiering
• En enkel åtgärd är att helt enkelt förbjuda kopiering
// ArrayList.h
ArrayList(const ArrayList& list) = delete;
– Nu leder försök att kopiera till kompileringsfel
– Löser problemet, men är kanske lite väl restriktivt
8.18
5
Kod för djup kopieringsinitiering
// ArrayList.cpp
ArrayList::ArrayList(const ArrayList& other) {
m_capacity = other.m_capacity;
m_size = other.m_size;
m_elements = new int[m_capacity]; // deep copy
for (int i = 0; i < m_size; i++) {
m_elements[i] = other.m_elements[i];
}
}
8.19
2.5
Kopieringstilldelningsoperator
Kopieringstilldelning
Tilldelning i C++ är mycket enklare än initiering och äger endast rum om ett existerande objekt explicit
tilldelas ett nytt värde:
MyClass one, two;
MyClass two = one;
• Jämför med följande kod där two initieras som en kopia av one
MyClass one;
MyClass two = one;
8.20
Kopieringstilldelningsoperatorn
Kopieringstilldelning i C++ sker med hjälp av kopieringstilldelningsoperatorn:
• Syntaktiskt är kopieringstilldelningsoperatorn mycket mer komplex än kopieringskonstruktorn eftersom det är en överlagrad operator
class MyClass {
public:
MyClass();
~MyClass();
MyClass(const MyClass& other); // Copy constructor
MyClass& operator= (const MyClass& other); // Assignment operator
/* ... */
};
• Den kompilatorgenererade kopieringstilldelningsoperatorn gör medlemsvis tilldelning. . .
8.21
Kod för djup kopieringstilldelning
Koden för en korrekt kopieringstilldelningsoperator är mycket mer involverad än för kopieringskonstruktorn.
• Till viss del beror detta på att C++ ger oss maximal flexibilitet. Till exempel är följande en giltig
implementation:
void MyClass::operator= (const MyClass& other) {
cout << "I’m sorry, Dave. I’m afraid I can’t copy that object." << endl;
}
8.22
Kod för djup kopieringstilldelning: version 1
/* Många stora felaktigheter här. Använd inte som referens! */
void ArrayList::operator= (const ArrayList& other) {
m_capacity = other.m_capacity;
m_size = other.m_size;
m_elements = new int[m_capacity]; // deep copy
for (int i = 0; i < m_size; i++) {
m_elements[i] = other.m_elements[i];
}
}
• Koden baserad på kopieringskonstruktorn
– Men, när tilldelningsoperatorn anropas har ArrayListan redan sitt eget fält av element, vilket
betyder att vi har en minnesläcka. . .
8.23
6
Kod för djup kopieringstilldelning: version 2
/* Flera stora felaktigheter här. Använd inte som referens! */
void ArrayList::operator= (const ArrayList& other) {
delete[] m_elements;
m_capacity = other.m_capacity;
m_size = other.m_size;
m_elements = new int[m_capacity]; // deep copy
for (int i = 0; i < m_size; i++) {
m_elements[i] = other.m_elements[i];
}
}
• All kod efter delete[] är exakt likadan som den i kopieringskonstruktorn
– Ingen slump — i de flesta fall kommer stort överlapp att finnas
– Eftersom vi inte får anropa vår egen kopieringskonstruktor direkt (eller någon annan konstruktor för den delen) bryter vi ut kopieringskoden i en hjälpfunktion
8.24
Kod för djup kopieringstilldelning: version 3
void ArrayList::copyOther(const ArrayList& other) {
m_capacity = other.m_capacity;
m_size = other.m_size;
m_elements = new int[m_capacity]; // deep copy
for (int i = 0; i < m_size; i++) {
m_elements[i] = other.m_elements[i];
}
}
ArrayList::ArrayList(const ArrayList& other) {
copyOther(other);
}
/* Inte helt perfekt än. Använd inte som referens! */
void ArrayList::operator= (const ArrayList& other) {
delete[] m_elements;
copyOther(other);
}
• Vi har ett par saker kvar att ta hänsyn till
– Betrakta följande kodsnutt:
ArrayList one;
one = one;
8.25
Kod för djup kopieringstilldelning: version 4
/* Inte helt perfekt än. Använd inte som referens! */
void ArrayList::operator= (const ArrayList& other) {
if (this != &other) {
delete[] m_elements;
copyOther(other);
}
}
• En sista bugg att ta hand om
– Betrakta följande kodsnutt:
ArrayList one, two, three;
three = two = one;
8.26
Kod för djup kopieringstilldelning: slutlig version
ArrayList& ArrayList::operator= (const ArrayList& other) {
if (this != &other) {
delete[] m_elements;
copyOther(other);
}
return *this;
}
8.27
7
2.6
Flyttsemantik
Utan C++11
vector<string> ReadAllWords(const string& filename) {
ifstream input(filename.c_str());
vector<string> result;
result.insert(result.begin(),
istream_iterator<string>(input),
istream_iterator<string>());
return result;
}
• Hur effektiv är den här koden?
8.28
Utan C++11
8.29
Utan C++11
8.30
Utan C++11
8.31
Med C++11
vector<string> ReadAllWords(const string& filename) {
ifstream input(filename.c_str());
vector<string> result;
result.insert(result.begin(),
istream_iterator<string>(input),
istream_iterator<string>());
return result;
}
• Ingen ändring i koden. . .
8.32
8
Med C++11
8.33
Med C++11
8.34
Med C++11
8.35
Med C++11
8.36
Flyttsemantik
• Kopieringssemantik (C++03): Kan duplicera ett objekt
– Kopieringsskonstruktor och kopieringstilldelningsoperator
• Flyttsemantik (C++11): Kan flytta ett objekt från en plats till en annan
– Flyttkonstruktor och flytttilldelningsoperator
• Flyttsemantik ger asymptotiskt bättre prestanda i många fall
8.37
Rvalue-referens
• Syntax Type &&
• Referens till en temporär variabel
• Representerar en variabel vars innehåll kan flyttas från en plats till en annan
8.38
9
Med C++11
/* Flyttkonstruktor */
ArrayList::ArrayList(ArrayList&& other) {
m_elements = other.m_elements;
m_size = other.m_size;
m_capacity = other.m_capacity;
other.m_size = 0;
other.m_capacity = 10;
other.m_elements = new int[other.m_capacity];
}
/* Flytttilldelningsoperator */
ArrayList& ArrayList::operator= (ArrayList&& other) {
if (this != &other) {
delete[] m_elements;
m_elements = other.m_elements;
m_size = other.m_size;
m_capacity = other.m_capacity;
other.m_size = 0;
other.m_capacity = 10;
other.m_elements = new int[other.m_capacity];
}
return *this;
}
8.39
Flyttsemantik 6= kopieringssemantik
• Kan fortfarande returnera objekt genom att kopiera
• C++11 provar att flytta först, använder kopiering om det inte lyckas
• Objekt kan vara flyttbara utan att vara kopierbara
8.40
3
Mallar
Vad är en mall (template)?
• En mallfunktion är en ritning för att generera funktioner
• Det ekvivalent med att låta kompilatorn automatiskt skriva ut varje instans av funktionen för alla
typer du behöver
• Instansiering av mallen sker när en mallfunktion används för en specifik typ av variabel
8.41
Vad är en mall (template)?
• För att deklarera en mallfunktion, lägg bara till följande rad före funktionens definition:
template <typename T>
• T är mallparametern, vilken kommer att ersättas med en specfik typ när du använder mallfunktionen.
Den måste inte heta T.
template<typename T>
T min(T a, T b) {
return (a < b) ? a : b;
}
8.42
Validering av mallar
• Kompilatorn “verifierar” instansieringen av en mallfunktion.
• Mallfunktioner fungerar bara om alla operationer som används på variabeln av den mallifierade typen
stöds av typen som instansieras.
8.43
Använda mallar
• Hittills har vi bara använt mallar för att slippa upprepa samma funktion.
• Mallar kan användas till mycket, mycket mer.
10
8.44
Klassmallar
• Markera varje/klass funktion som mallifierade i .h- och .cpp-filerna
• Ersätt föregående typen (t.ex. int) med T i koden
8.45
.h och .cpp för mallklasser
• På grund av en egenhet i C++ mallsystem måste separationen mellan header och implementation
reduceras.
– Skriv antingen alla kroppar i .h-filen,
– eller, inkludera .cpp-filen i slutet av .h-filen för att sammanfoga dem.
8.46
11