Kihagyás

Egy kis extra: hibakezelés, final, öröklődés vs. tárolók, többszörös öröklődés

Kivételkezelés folytatás...

Nem osztály típusú hibák

A hibakezelés elején említésre került, hogy általában osztály típusúak a hibák, azonban ez C++ esetében nem feltétlenül kell így legyen (Java esetén csak így lehetett, ráadásul ott az sem volt mindegy, melyik osztályból származik a kivétel objektumok osztálya). C++-ban bármit el lehet dobni, bármilyen értéket; legyen az const char*, std::string, int, stb. Természetesen ezeket is elkaphatjuk referencia szerint, azonban primitív típusok esetén ez nem szükséges. Ami azonban fontos, hogyha a dobott kivétel típusa nem származik az std::exception osztályból, akkor természetesen nem is tudjuk elkapni az ennek megfelelő ágban ezeket a kivételeket.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
int main() {
  try {
    throw 42;
  }
  catch(const std::exception& e) { // Nem fut le, hiaba a legelso!
    std::cerr << "A hiba: " << e.what() << std::endl;
  }
  catch(int i) {
    std::cerr << "A hiba: " << i << std::endl;
  }

  try {
    throw "Ez egy const char* tipusu literal";
  }
  catch(const char* hiba) {
    std::cerr << hiba << std::endl;
  }
}

Kimenet

A hiba: 42
Ez egy const char* tipusu literal

Minden hiba kezelése

Mivel egyes hibák nem tartoznak az std::exception alá, így általánosan nem kezelhetők. Azonban ha szeretnénk megoldani, hogy biztosan elkapjunk minden hibát, minden lehetséges hibatípusra kell catch ágat írnunk. Azonban ez nem biztos, hogy minden esetben lehetséges (vagy azért mert rengeteg féle-fajta hiba van, amiket azonosan akarunk kezelni, vagy pedig azért mert nem is tudjuk pontosan, hogy milyen hibákra kell felkészíteni a programunkat). Azonban ezt is megoldhatjuk, ha elkapáskor a típusok helyett egyszerűen csak ... (három pontot) írunk. Ezzel bármit el tudunk kapni a catch ágban.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
int main() {
  try {
    throw 42;
  }
  catch(const std::exception& e) {    // Nem fog lefutni
    std::cerr << e.what() << std::endl;
  }
  catch(...) {
    std::cerr << "Ismeretlen hiba 1" << std::endl;
  }

  try {
    throw "Hibauzenet";
  }
  catch(const std::exception& e) {    // Nem fog lefutni
    std::cerr << e.what() << std::endl;
  }
  catch(...) {
    std::cerr << "Ismeretlen hiba 2" << std::endl;
  }
}

Kimenet

Ismeretlen hiba 1
Ismeretlen hiba 2

Final osztályok, metódusok

A Java nyelvben lehetőség van arra, hogy a final kulcsszóval biztosítsuk, hogy adott esetben egy metódus ne legyen felülírható egy leszármazott osztályban, vagy ugyanezzel jelezhettük azt is, ha nem akarjuk azt, hogy egy osztályból lehessen másik osztályt származtatni.

A C++ 11 szabvány a C++ nyelvben is lehetőséget teremt erre, ugyanakkor a final C++ esetében nem lesz kulcsszó. Ez azt jelenti, hogy a megfelelő környezetben (a metódus vagy osztály neve után) azt a szerepet tölti be, mint Java esetében, minden egyéb esetben csak egy egyszerű azonosítónak számít.

Öröklődés vs. tárolók

Elképzelhető, hogy polimorfikus típus elemeit szeretnénk valamilyen tárolóban tárolni. Mondjuk különböző típusú Hangszer objektumokat szeretnénk eltárolni, amelyek között lehet Zongora, Fuvola, Dob, stb. Minden hanszer meg tud szólalni valamilyen hangon, de egy tetszőleges Hangszer objektumra nyilván nehéz megmondani, hogyan is fog megszólalni. Ebben az esetben tehát érdemes a Hangszer szolj() metódusát virtuálisan definiálni, így amikor megszólaltatunk egy-egy hangszert, az mind a saját hangját tudja visszaadni.

Egy zenekarban sok-sok hangszer lehet, különböző típusúak, ezeket valahogy hozzá kell rendelnünk a zenekarhoz. Nyilván a legpraktikusabb az, ha ezt valami tároló segítségével oldjuk meg.

A következő példában csak egyetlen speciális hangszert emelünk ki, és a zenekart sem hozzuk létre, csak a main metódusban kipróbáljuk, mi történik akkor, ha érték szerint próbáljuk eltárolni egy vektorban a meghatározott hangszerünket:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <vector>

using namespace std;

class Hangszer {
public:
  virtual void szol() const {
    cout << "???" << endl;
  }
};

class Fuvola : public Hangszer {
public:
  void szol() const override {
    cout << "Trilla-trilla" << endl;
  }
};

int main() {
    vector<Hangszer> container;
    Fuvola fuvola;
    container.push_back(fuvola);
    container[0].szol();
}

Kimenet

???

A probléma látszik. A containerbe érték szerint bemásolva a Fuvola objektumot hasonló helyzetet eredményez, mint az érték szerinti paraméterátadás, ahol az objektum elvesztette a speciális tulajdonságait, és a polimorfikus hívás így nem tudott érvényesülni.

Ehelyett nyilván megoldás lehet, ha valahogy az objektumra mutató referenciát tudnánk eltárolni, de ezt nem tudjuk megtenni.

std::reference_wrapper

A fenti problémára megoldás lehet az std::reference_wrapper használata, amely gyakorlatilag egy változóba csomagolja nekünk a Fuvola objektumra vonatkozó referenciát. Módosítsuk a main függvényt az alábbiak szerint:

1
2
3
4
5
6
int main() {
  vector<reference_wrapper<Hangszer>> container;
  Fuvola fuvola;
  container.push_back(fuvola);
  container[0].get().szol();
}

Ekkor megszólaltatva a tároló elemét (ha több is lenne, akkor elemeit), már a kívánt eredményt kapjuk:

Kimenet

Trilla-trilla

Fontos! A tárolóba a példában lokálisan létrehozott objektum került. Azaz az erre vonatkozó referencia is addig létezik, ameddig maga a lokális objektum él. Az ilyen módon létrehozott tároló nem lenne alkalmas arra, hogy globálisan kezelje a hangszereket. Ahhoz, hogy ezt megtehessük, más eszközöket kell majd alkalmazzunk.

Többszörös öröklődés

C++-ban lehetőségünk van arra, hogy egy osztály több másik osztályból származzon. Az egyetlen "probléma" ilyenkor a névütközések kezelése, azaz az, ha az osztály több helyről is örököl ugyanolyan szignatúrájú elemet. Amennyiben a duplán örökölt tagok nem virtuálisak, akkor egyszerűen a szkópolt nevükkel tudunk rájuk a gyerek osztályban hivatkozni, legyen akár adat tagról, vagy metódusról szó.

Virtuális metódusok esetében a virtuális táblája öröklődik az ősöknek. Amennyiben nem akarjuk direkt a gyerek típusra hívni az adott metódust, csak az ősön keresztül, akkor egyértelmű, hogy melyik táblát kell használni a hívás feloldásához. Ha azonban a többszörösen származtatott osztály objektumán keresztül szeretnénk a duplán örökölt metódust hívni, azt csak akkor tehetjük meg, ha a származtatott osztályban felülírtuk a metódust.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include  <iostream>
class A {
public:
  int value = 5;

  void nonVirtualCall() {
    std::cout  << "A nem virtualis metodusa - value: " << value << std::endl;
  }

  virtual void virtualCall() const {
    std::cout  << "A virtualis metodusa - value: " << value << std::endl;
  }
};

class B {
public:
  int value;

  void nonVirtualCall() {
    std::cout  << "B nem virtualis metodusa - value: " << value << std::endl;
  }

  virtual void virtualCall() const {
    std::cout  << "B virtualis metodusa - value: " << value << std::endl;
  }
};

class C : public A, public B {
public:
  void foo() {
    //szkopolt hivatkozasok nem virtualis tagokra
    B::value=1;
    A::nonVirtualCall();
    B::nonVirtualCall();
  }

  void virtualCall() const override {
    std::cout  << "C virtualis metodusa - A::value: " 
               << A::value << "; B::value " << B::value << std::endl;
    //orokolt, ugyanolyan nevu adattagokra szkopolt nevvel hivatkozhatunk
  }
};

void polymorphCallA(const A& a) {
  a.virtualCall(); 
}

void polymorphCallB(const B& b) {
  b.virtualCall();
}

int main() {
  C c;
  c.foo();
  c.virtualCall(); //forditasi hiba, ha a C osztalyban nincs felulirva a virtualCall
  polymorphCallA(c);
  polymorphCallB(c);
}

Kimenet

A nem virtualis metodusa - value: 5
B nem virtualis metodusa - value: 1
C virtualis metodusa - A::value: 5; B::value 1
C virtualis metodusa - A::value: 5; B::value 1
C virtualis metodusa - A::value: 5; B::value 1


Utolsó frissítés: 2024-07-24
Létrehozva: 2024-07-24