Klassimallid
Pärast selle praktikumi läbimist üliõpilane
- teab, mis on klassimall ja oskab klassimalli defineerida
- oskab defineerida klassimalli liikmefunktsioone väljaspool klassi
- oskab klassimalli abil klassi isendeid luua
- oskab luua mitme tüübiparameetriga klassimalli
Klassimalli defineerimine
Klassimall on kavand klassi genereerimiseks. Klassimallid erinevad funktsioonimallidest selle poolest, et kompilaator ei saa tuletada malliparameetrite tüüpe klassimalli jaoks. Klassimall muudab klassi definitsiooni üldisemaks. Klassimalli definitsioon algab päisega
template <typename tüübiparameeter>
Võtmesõna typename
asemel võib kasutada ka võtmesõna class
, aga tuleks eelistada typename
.
Klassi enda definitsioon on sama nagu varem, ainult tüübi asemel kasutatakse tüübiparameetrit. Tüübiparameetreid võib olla ka mitu. Vaatame näidet klassimallist. Olgu meil klass Paar
, mis hoiab kahte sama tüüpi elementi.
NB! Nii funktsioonimallid kui ka klassimallid koos liikmefunktsioonide definitsioonidega paigutatakse päisefaili. Kompilaator teeb klassist malliisendi iga kasutatava tüübi jaoks ja seetõttu on kompilaatoril vaja klassi liikmete kohta kogu infot.
template <typename T> class Paar{ public: Paar(); Paar(T esimene, T teine); void setEsimene(T esimene); void setTeine(T teine); T getEsimene(); T getTeine(); private: T m_esimene{}; T m_teine {}; };
Näites olevas klassimallis on mallitüübiks T
ja mall sisaldab vaid konstruktorite ja liikmefunktsioonide deklaratsioone. Funktsioonide definitsioonid võivad olla ka klassimalli sees, kuid nii nagu tavalise klassi puhul, kõik klassi sees defineeritud funktsioonid on automaatselt pistikfunktsioonid (inline functions).
Väljaspool klassimalli liikmefunktsioonide defineerimisel on eripära, et liikmefunktsioonid on ise mallid ja algavad päisega template <typename T>
. Funktsiooni tagastustüüp on enne funktsioonimalli skoobimäärangut Paar<T>::
:
template <typename T> void Paar<T>::setEsimene(T esimene) { m_esimene = esimene; }
Klassimall Paar
tervikuna on järgmine (nt failis deklaratsioonid.h
):
template <typename T> class Paar{ public: Paar(); Paar(T esimene, T teine); void setEsimene(T esimene); void setTeine(T teine); T getEsimene(); T getTeine(); private: T m_esimene{}; T m_teine {}; }; template <typename T> Paar<T>::Paar(T esimene, T teine) : m_esimene{esimene}, m_teine{teine}{} template <typename T> void Paar<T>::setEsimene(T esimene) { m_esimene = esimene; } template <typename T> void Paar<T>::setTeine(T teine) { m_teine = teine; } template <typename T> T Paar<T>::getEsimene(){ return m_esimene; } template <typename T> T Paar<T>::getTeine(){ return m_teine; }
Kui klassimall on defineeritud, siis saab klassist teha objekte. Objekti loomisel peab ette andma tüübi:
Paar<double> paar{};
Testime klassimallist objektide loomist funktsioonis main
:
#include <iostream> #include <string> #include "deklaratsioonid.h" using namespace std; int main() { Paar<int> täisarvupaar{2, 5}; täisarvupaar.setEsimene(3); täisarvupaar.setTeine(6); cout << "Paar: (" << täisarvupaar.getEsimene() << ", " << täisarvupaar.getTeine() << ")\n"; Paar<string> nimepaar{"Madli", "Madis"}; cout << "Paar: (" << nimepaar.getEsimene() << ", " << nimepaar.getTeine() << ")\n"; return 0; } | Paar: (3, 6) Paar: (Madli, Madis) |
Klassimall funktsiooni parameetrina
Klassimalli nime võib kasutada funktsiooni parameetrina. Näiteks, järgmine funktsioon tagastab paari elementide summa
int liida(Paar<int> paar){ return paar.getEsimene() + paar.getTeine(); }
ja funktsioon
string liida(Paar<string> paar){ return paar.getEsimene() + paar.getTeine(); }
tagastab ühendatud sõned.
Viimase kahe funktsiooni asemel saab luua funktsioonimalli (paneme faili deklaratsioonid.h
):
template <typename T> T liida(Paar<T> paar){ return paar.getEsimene() + paar.getTeine(); }
Katsetame seda funktsioonis main
:
Paar<int> täisarvupaar{3, 6}; Paar<string> nimepaar{"Madli", "Madis"}; cout << liida(täisarvupaar) << '\n'; cout << liida(nimepaar) << '\n'; | 9 MadliMadis |
Mitme tüübiparameetriga klassimall
Klassimallis võib tüübiparameetreid olla rohkem kui üks. Näide kahe tüübiparameetriga klassist:
template <typename T1, typename T2> class MinuKlass{ T1 m_t1{}; T2 m_t2{}; public: MinuKlass(T1 t1, T2 t2) : m_t1{t1}, m_t2{t2} {} void näita() { cout << m_t1 << ' ' << m_t2 << '\n'; } friend ostream& operator<<(ostream& os, const MinuKlass m) { os << "MinuKlass: " << m.m_t1 << ' ' << m.m_t2; return os; } };
Klassi on lisatud ka operaatori <<
üledefineerimine objekti ekraanile kuvamiseks. Testime klassimalli funktsioonis main
:
#include <iostream> #include <string> #include "deklaratsioonid.h" using namespace std; int main() { MinuKlass<string, int> isik{"Pille", 20}; MinuKlass<double, double> koordinaadid{1.2, 5.6}; isik.näita(); koordinaadid.näita(); cout << isik << '\n'; cout << koordinaadid << '\n'; return 0; } | Pille 20 1.2 5.6 MinuKlass: Pille 20 MinuKlass: 1.2 5.6 |
Järgmises näites on klassimalli tüübiparameetriks klass. Siin nii klassimallis kui ka kasutatavates klassides peavad kõik liikmefunktsioonid (ka sõbrad) olema defineeritud klassi sees. Funktsioonis main
tehakse klassimallist Loom
isendid, andes ette tüübiparameetri Kass
või Koer
. Klassi Loom
isenditele rakendatakse funktsioone, mille parameetriks on vastavalt Kass
ja Koer
isendid.
Fail deklaratsioonid.h
#ifndef N10_0_DEKLARATSIOONID_H #define N10_0_DEKLARATSIOONID_H #include <iostream> #include <string> class Kass{ public: Kass() = default; Kass(std::string nimi) : m_nimi{nimi}{} std::string m_nimi{}; void teeHäält(){ std::cout << "Mjäu\n"; } friend std::ostream& operator<<(std::ostream& os, const Kass k) { os << "Kassi nimi: " << k.m_nimi; return os; } }; class Koer{ public: Koer() = default; Koer(std::string nimi) : m_nimi{nimi}{} std::string m_nimi{}; void teeHäält(){ std::cout << "Auh\n"; } friend std::ostream& operator<<(std::ostream& os, const Koer k) { os << "Koera nimi: " << k.m_nimi; return os; } }; template <typename T> class Loom{ public: Loom() = default; void häälitse(T& loom){ loom.teeHäält(); } void tee(T& loom){ std::cout << loom << " Teen midagi\n"; } }; #endif //N10_0_DEKLARATSIOONID_H
#include <iostream> #include <string> #include "deklaratsioonid.h" using namespace std; int main() { Kass kass{"Miisu"}; cout << kass << '\n'; Loom<Kass> l1{}; l1.häälitse(kass); Koer koer{"Muki"}; cout << koer << '\n'; Loom<Koer> l2{}; l2.häälitse(koer); l2.tee(koer); return 0; } | Kassi nimi: Miisu Mjäu Koera nimi: Muki Auh Koera nimi: Muki Teen midagi |
Tüübiparameetrite vaikeväärtused ja lisaparameetrid (non-type parameters)
Nii nagu funktsioonimalli korral, on ka klassimalli korral võimalik tüübiparameetrile anda vaikeväärtus. Järgmises näites on tüübiparameetri T
vaikeväärtuseks int
ja lisaparameetriks int suurus
, mis on klassimallis sisalduva massiivi pikkus. Lisaparameeter võib olla kas int
, viit (pointer) või viide (reference). Teised tüübid, nt double
ei ole lubatud. Lisaparameetreid tuleb vaadelda kui konstante, nende väärtust ei saa muuta.
// int on fikseeritud parameeter, massiivi pikkus template <typename T = int, int suurus = 10> class Massiiv { T massiiv [suurus]{}; // massiiv public: Massiiv() { //konstruktor int i{}; for(; i < suurus; i++){ massiiv[i] = i; } } T& operator[](int i); }; // operaatori [] üledefineerimine ja piiride kontroll template <typename T, int suurus> T& Massiiv<T, suurus>::operator[](int i){ if(i < 0 || i > suurus - 1) { cout << "\nIndeksi väärtus "; cout << i << " on väljaspool piire.\n"; exit(1); //väljumine veaga } return massiiv[i]; // operaatori [] tagastus }
Funktsioonis main
katsetame erineva parameetritüübiga objekte:
int main() { // int massiiv pikkusega 10 Massiiv<> int_m{}; Massiiv<int, 10> int_ob{}; // double massiiv pikkusega 15 Massiiv<double, 15> double_ob{}; int i; cout << "Täisarvude massiiv: "; for(i = 0; i < 10; i++) cout << int_ob[i] << " "; cout << '\n'; cout << "Ujukomaarvude massiiv: "; for(i = 0; i < 15; i++) double_ob[i] = i*2.5; for(i = 0; i < 15; i++) cout << double_ob[i] << " "; cout << '\n'; int_m[12] = 100; // tekitab vea return 0; | Täisarvude massiiv: 0 1 2 3 4 5 6 7 8 9 Ujukomaarvude massiiv: 0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 32.5 35 Indeksi väärtus 12 on väljaspool piire. |
Näeme, et omistamine int_m[12] = 100
tekitab vea, sest 12 on väljaspool massiivi int_m
piire.
Klassimall määrab klassitüüpide perekonna. Klassimalli eksemplar on klassi definitsioon, mille kompilaator genereerib mallist, kasutades koodis määratud malliargumentide komplekti. Klassimalli tüübiparameetrile vastav argument võib olla põhitüüp, klassi tüüp, viidatüüp või viitetüüp.
Kitsendused tüübiparameetritele <type_traits>
Tüübiparameetritele saab seada ka tüübi osas kitsendusi. Selleks tuleb kaasata päis <type_traits>
. Kui lisame klassile Paar
kontrolli
template <typename T> class Paar{ static_assert(std::is_arithmetic_v<T>, "Parameetrid peavad olema arvud"); public: ...
siis katse luua isendit mittearvuliste parameetritega
Paar<string> paar{"1", "2"};
annab kompileerimisvea.
Täpsemalt saab <type_traits>
kohta uurida aadressil
https://en.cppreference.com/w/cpp/header/type_traits
ja
https://en.cppreference.com/w/cpp/types/is_arithmetic
Võtmesõnad typedef
ja using
Saab defineerida uue klassi tüübi nime, millel on sama tähendus nagu tüübiga klassimall, nt Paar<int>
. Selleks tuleb kasutada võtmesõna typedef
:
typedef Paar<int> Täisarvupaar;
Seda saab kasutada isendite loomisel
Täisarvupaar tp1{3, 5}, tp2{4, 8};
Võtmesõnaga typedef
peab kasutama konkreetset tüüpi, st ei saa defineerida typedef Paar<T> paar
. Uue standardi järgi on võimalik defineerida klassimallile tüübi alias:
template<typename T> using Duo = Paar<T>;
Nüüd Duo
on sünonüüm Paar
-le.
Duo<int> arvud{}; // arvud on Paar<int>
Klassimallide kohta saab täpsemalt uurida https://en.cppreference.com/w/cpp/language/class_template