Nu je een basis van klassen hebt opgedaan in C++ gaan we erving introduceren. Gegeven de volgende modellen:
Gegeven de volgende acceptatie criteria:
Een voorbeeldimplementatie in Java zou dit kunnen zijn:
public abstract class Voedsel { }
public class Vlees extends Voedsel { }
public class Groenten extends Voedsel { }
public abstract class Dier {
public abstract boolean kanEten(Voedsel voedsel);
public abstract int beweeg();
}
public abstract class Viervoeter extends Dier {
protected int aantalPoten = 4;
}
public class Hond extends Viervoeter {
@Override
public boolean kanEten(Voedsel voedsel) {
return true;
}
@Override
public int beweeg() {
return 10 * aantalPoten;
}
}
public class Kat extends Viervoeter {
private int aantalKeerBewogen;
@Override
public boolean kanEten(Voedsel voedsel) {
return voedsel instanceof Vlees;
}
@Override
public int beweeg() {
aantalKeerBewogen++;
int velocity = aantalKeerBewogen >= 2 ? 5 : 15;
return velocity * aantalPoten;
}
}
public class Vlinder extends Dier {
private int vleugelGrootte = 4;
@Override
public boolean kanEten(Voedsel voedsel) {
return voedsel instanceof Groenten;
}
@Override
public int beweeg() {
return 2 * vleugelGrootte;
}
}
De noties van abstract
, de @Override
annotatie en access modifiers bestaan natuurlijk ook in C++ - zie hoofdstuk 15. Bovenstaande Java code omgezet naar C++:
class Voedsel {
public:
virtual int voedingswaarde() = 0;
};
class Vlees : public Voedsel {
int voedingswaarde() override { return 10; }
};
class Groenten : public Voedsel {
int voedingswaarde() override { return 15; }
};
class Dier {
public:
virtual bool kanEten(const Voedsel& voedsel) = 0;
virtual int beweeg() = 0;
};
class Viervoeter : public Dier {
protected:
int aantalPoten;
public:
Viervoeter() : aantalPoten(4) {}
};
class Hond : public Viervoeter {
public:
int beweeg() override {
return 10 * aantalPoten;
}
bool kanEten(const Voedsel &voedsel) override {
return true;
}
};
class Kat : Viervoeter {
private:
int aantalKeerBewogen;
public:
bool kanEten(const Voedsel &voedsel) override {
return typeid(voedsel) == typeid(Vlees);
}
int beweeg() override {
aantalKeerBewogen++;
int velocity = aantalKeerBewogen >= 2 ? 5 : 15;
return velocity * aantalPoten;
}
};
class Vlinder : Dier {
private:
int vleugelGrootte;
public:
Vlinder() : vleugelGrootte(4) {}
private:
bool kanEten(const Voedsel &voedsel) override {
return typeid(voedsel) == typeid(Groenten);
}
int beweeg() override {
return 2 * vleugelGrootte;
}
};
typeid()
leeft in de <typeinfo>
header. Een alternatief is dynamische pointers casten (zie onder). Voor de rest zijn de grootste verschillen - buiten de syntax:
override
na een methode wordt ook door de compiler gebruikt om te controleren of wat je override wel een virtuele methode is. In Java is @Override
enkel ter documentatie.abstract
op een klasse bestaat niet. Daarvoor moet je een “pure virtuele methode” (= 0
) aanmaken.virtual
. In Java zijn alle methodes virtual.Als je de access modifiers in de klasse definitie vergeet wordt private
aangehouden. Voor een struct is dit standaard public
.
Vergeet niet voor cmake
onder windows de -G
flag toe te voegen; zie installatie instructies cmake.
Wat is de output van het volgende programma?
int main() {
Groenten g; // vergeet niet dat dit in Java null zou zijn.
Vlees v;
Kat k;
std::cout << "kan een kat groenten eten? " << k.kanEten(g);
std::cout << "kan een kat vlees eten? " << k.kanEten(v);
}
En wat is de output van Dier d;
? Juist: error: variable type ‘Dier’ is an abstract class.
final
als suffix als een subklasse deze virtuele methode niet meer mag overschrijven. Kan op klasse of methode niveau.::
operator als je toch een virtual base methode wil aanspreken die reeds overschreven is: kat.Dier::iets()
waarbij iets zowel op Kat als op Dier gedefiniëerd is.friend
als prefix om een klasse instantie toegang te geven tot private fields van de andere. Kan op klasse of methode niveau. Zie InternalsVisibleTo C# AssemblyInfo.using Base::member
als prefix om bepaalde members toch public access te geven als alles als private gedeclareerd is.De method shadowing regels volgen ongeveer dezelfde als die van Java: een non-virtual functie met dezelfde naam en argumenten kan een functie hiden van een superklasse.
Kijk eens goed naar het volgende voorbeeld:
class A {
public:
virtual int a() { return 3; }
};
class B : public A {
public:
int a() { return 5; }
};
int main() {
A b = B();
std::cout << b.a() << std::endl;
}
Wat is de output? 3, en niet 5, ook al is het type van b een instantie van klasse B. Huh? Calls naar virtuele functies kunnen at run-time resolved worden, maar dat “hoeft” niet (zie p.604). De enige uitzondering hier is het gebruik van pointers: met A* b = new B();
geeft b->a()
wél 5 terug.
typeid(b).name()
blijft “1A” teruggeven omdat de variabele als type A gedeclareerd is.
C++ voorziet een hele resem aan cast methodes die in het licht van klassen en subklassen nodig kunnen zijn:
dynamic_cast<T>(t)
: downcaster in gebruik. Geeft nullptr
terug indien niet gelukt. Dit is Java’s aangenomen instanceof
manier.static_cast<T>(t)
: impliciete conversie ongedaan maken (zie elders). Als je bijvoorbeeld weet dat een void*
eigenlijk een Punt*
is.Dit kan fouten at compiletime geven.reinterpret_cast<T>(t)
: pointer conversies in lijn van C. Dit kan fouten at runtime geven.const_cast<T>(t)
: verwijdert of voegt const
speficier toe.De C-style cast (Punt*) pt
wordt aanzien als bad practice in de C++ wereld.
C++ biedt zoals verwacht zelfs op operator niveau flexibiliteit: je kan je eigen operatoren implementeren in klassen (p.552). Op die manier kan je bijvoorbeeld twee 2D punten met elkaar optellen: punt1 + punt2
. In Java zal je een methode moeten maken: punt1.plus(punt2)
dat een nieuw punt teruggeeft.
Alle mogelijke operatoren kunnen overloaded worden, behalve ::
, .*
, .
en ?:
. Dit brengt ook potentiële problemen met zich mee! Stel je voor dat ->
overloaded is en je klasse zich heel anders gedraagt dan een standaard pointer reference. With great power comes great responsibility…
Een voorbeeld:
using namespace std;
class Punt {
private:
int x, y;
public:
Punt(int theX, int theY) : x(theX), y(theY) {}
Punt operator +(const Punt& other) {
return Punt(x + other.x, y + other.y);
}
friend ostream& operator<<(ostream& os, Punt punt);
};
ostream& operator<<(ostream& os, Punt punt) {
os << "(" << punt.x << "," << punt.y << ")";
return os;
}
int main() {
Punt a(1, 2);
Punt b(3, 4);
cout << a + b << endl; // print (4,6)
}
Operators kan je ook rechtstreeks aanroepen met punt.operator+(other)
. Ze zijn niet verplicht om member te zijn van de klasse zelf maar ik zie geen reden om dingen die samen te horen niet samen te zetten. Een duidelijke uitzondering zijn IO operators! (p.557)
Andere veelgebruikte operatoren:
<<
als toString()
in Java naar stdout.==
als equals()
in Java. Vergeet niet !=
ook te implementeren dan!=
als in-assignment om { }
te gebruiken.[]
als lijst-accessor.Een conversie tussen twee types gebeurt impliciet als de compiler een match kan vinden. Je kan de compiler een handje helpen door er zelf in je klasse bij te definiëren: operator int()
(zonder return type). Vanaf dan compileert Punt p; p + 5;
in combinatie met de plus operator!
Impliciete conversies zijn niet altijd wenselijk, daarvoor dient de prefix explicit
(ook toepasbaar op constructoren). Expliciete conversies doe je zelf met static_cast<int>(p)
- gegeven dat de operator geïmplementeerd is natuurlijk.
Herinner je de STL vector
klasse. Deze collectie kan integers opslaan, of Punt instances, door tussen <>
een type mee te geven: vector<Punt> punten;
. Er is een template voor gedefiniëerd. Stel dat ik de punt klasse wens uit te breiden met de mogelijkheid niet alleen integers maar ook doubles als coordinaten te gebruiken:
template<typename T> class Punt {
private:
T x, y;
public:
Punt(T theX, T theY) : x(theX), y(theY) {}
};
Punt<double> pt(1.2, 3.4);
Punt<int> pt2(3, 5);
Templates kunnen ook op functie niveau gedefiniëerd worden, als losstaande functie en als deel van een klasse genaamd member templates. (p.672)
De C++ compiler maakt voor elk template argument in je code (hier double
en int
) een aparte versie van de Punt
klasse. In Java wordt dat weggecompileerd en dienen generics enkel als syntaxtisch hulpmiddel. Dit heeft wel als negatief gevolg dat de binary erg groot kan worden als die vol zit met duplicate versies van Punt
!
Er zijn mogelijkheden tot compiler- en objectoptimalisatie met het keyword extern
. Aanschouw het volgende schema met bijhorende code:
// header.h
#ifndef _header_h
#define _header_h
template<typename T> T punt(T t) {
return t;
}
#endif
// source1.cpp
#include "header.h"
void punt1() {
auto pt = punt<int>(5); // template<int> compiled
}
// source2.cpp
#include "header.h"
void punt2() {
auto pt = punt<int>(4); // template<int> compiled - opnieuw!!
}
Als we een source file compileren én proberen te linken vinden we geen main()
functie:
Wouters-MacBook-Air:cmake-build-debug wgroenev$ g++ -std=c++11 source1.cpp Undefined symbols for architecture x86_64: "_main", referenced from: implicit entry/start for main executable ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
Vergeet de -c
optie dus niet. Door de symbol table van de machine code van source1.o te inspecteren krijgen we inzicht in de zonet gecompileerde bytes. Op Unix kan dit met nm
:
Wouters-MacBook-Air:cmake-build-debug wgroenev$ nm source1.o | c++filt 0000000000000070 short EH_frame0 0000000000000020 S int punt(int) 00000000000000b0 S int punt (int) (.eh) 0000000000000000 T punt1() 0000000000000088 S punt1() (.eh)
De c++filt
pipe is nodig omdat C++ vervelenderwijs voor gecompileerde objecten naamgeving helemaal overhoop gooit. Zonder zou je nooit punt1()
kunnen zien, maar verandert die lijn in iets als 000000000000004c T __Z4puntIiET_S0_
. Niet erg begrijpbaar dus. Lees ook: c++filt Linux manpage.
Je ziet op adres 00000000000000b0
de nieuwe functie die als <int>
gecompileerd is. Dit zit dubbel en ook in source2.cpp! Dit lossen we op door in één van de twee cpp bestanden extern template int punt(int x);
toe te voegen zodat de compiler dit niet opnieuw behandelt:
Wouters-MacBook-Air:cmake-build-debug wgroenev$ nm source1.o | c++filt 0000000000000040 short EH_frame0 U int punt(int) 0000000000000000 T punt1() 0000000000000058 S punt1() (.eh)
De U
duidt aan dat dit een onbekende functie is die naderhand (hopelijk) gelinkt zal worden en binnen een ander object leeft. Lees meer over interessante object files en symbolen. In de praktijk geldt dit ook voor STL klassen als vector<int>
: externals worden meestal in een gedeelde header file geplaatst.
Omdat een Punt<double>
dus een andere klasse is dan een Punt<int>
zijn ze niet compatibel met elkaar: het zijn twee unieke klassen. Dit is het grootste verschil tussen Templates in C++ en Generics in Java. De notatie <T extends BaseClass>
is hierdoor niet nodig (maar kan wel met enable_if
).
De constructor - of eender welke methode met T
buiten de klasse template definiëren betekent dat we de template notatie zullen moeten herhalen want de compiler weet dan niet meer wat die T
precies is:
// in punt.h
template<typename T> class Punt {
Punt(T theX, T theY);
}
// in punt.cpp
template<typename T> Punt<T>::Punt(T theX, T theY) : x(theX), y(theY) {
}
typename
staat voor “dit is type T” en kan eender welk type zijn. Een constante expressie zoals 5
of een string "hallo"
aanvaarden gaat zo ook: dat zijn immers ook types.
Constante expressies met unsigned
in de template definitie kunnen pointers, value references of integrale types zijn. In ons voorbeeld is het niet aangewezen om dit toe te passen: Punt<3, 4> pt;
slaat enkel op iets als dit punt nooit kan muteren.
Kan op twee manieren:
typedef Punt<int> iPunt;
: iPunt p;
template<typename T> using pt = Punt<T>;
: pt<int> p;
Waarbij optie twee meestal gebruikt wordt om verschillende template types te linken: nu heeft dit niet bijzonder veel nut. typedef
kan niet refereren naar een template type.
Als ik een template type wil van een klasse aangeven, maar dit in 80% van de gevallen een int
gaat zijn kan ik deze defaulten:
template<typename T = int> class Punt;
Punt<> pt; // <> nog steeds verplicht.
Zonder <>
krijg je “error: use of class template ‘Punt’ requires template arguments”.
Voor functies probeert de compiler automatisch het type te deduceren gebaseerd op het meegegeven argument (p.678). Dat wil zeggen dat we het type niet moeten meegeven en ook niet hoeven te defaulten:
template<typename T> T puntFn(T t) {
return t;
}
int pt = puntFn<int>(5); // geldig
int pt = puntFn(5); // ook geldig!
auto pt = puntFn(5); // ook geldig!
Wat nu als je verschillende argumenten nodig hebt die allemaal verschillende types hebben, waarvan je het type niet op voorhand weet? De altijd-aanwezige flexibiliteit in C++ lost dit probleem even voor je op met variadic templates:
template <typename... Ts> void som(Ts... args) {}
Dankzij compiler deductie hoeven we niet alle templates aan te vullen als we het aanroepen: som(1, 2.0, true);
zou hetzelfde zijn als som<int, double, bool>(1, 2.0, true);
. Om dit voorbeld te laten werken hebben we echter recursie nodig: een functie voor een basisgeval, en een functie voor de rest. Daarom heet dit “packing” en is ...
het unpacken van de template arguments.
template<typename T> T som(T t) { return t; }
template<typename T, typename... TRest> T som(T first, TRest... args) {
return first + som(args...);
}
auto result = som(1, 2.0, 3);
Zie docs.
Een geïntegreerd voorbeeld met:
= const
const SomeObj&
std::move
Kan je terugvinden in de repository van deze cursus, onder examples/cpprefs
: https://github.com/KULeuven-Diepenbeek/cpp-course/tree/master/examples/cpprefs.
bool
is OK), gebaseerd op de matchende diploma’s.stdout
).Punt
klasse uit met de volgende vereisten:bool voeder(const Voedsel &voedsel)
methode op dierentuin. De functie geeft TRUE
terug indien het voedsel voldoende is voor alle dieren en FALSE
indien het onvoldoende is. Voedsel heeft een voedingswaarde
. Elk dier eet even veel in voedingswaarde als zijn gewicht. Verzin voedsel subklassen om alle edge cases te kunnen testen!voeder()
implementatie? Voorzie een methode bool isAllergischAan(const Voedsel &voedsel)
in je Dier klasse.Tips: denk aan het thema: subklassen, operators, templates.
->
overload je best niet, en waarom? Geef een voorbeeld.Punt
uitbreiden tot X dimensies?