User Tools

Site Tools


cpp:exception_inheritance

Κληρονομικότητα εξαιρέσεων

Ας υποθέσουμε ότι έχουμε τη σχέση κληρονομικότητας μεταξύ των κλάσεων BaseException και DerivedException, όπως παρακάτω:

BaseException.hpp
using namespace std;
 
class BaseException {
protected:
  int a;
  char s[64];
public:
  BaseException(int a) { this->a = a; }
  virtual char* message() {
    sprintf(s, "BaseException, a: %d\n", a);
    return s;
  }
};
DerivedException.hpp
#include "BaseException.hpp"
using namespace std;
 
class DerivedException: public BaseException {
  int b;
public:
  DerivedException(int a, int b): BaseException(a) { this->b = b; }
  char* message() {
    sprintf(s, "DerivedException, a: %d, b: %d\n", a, b);
    return s;
  }
};
ExceptionUse.cpp
#include <iostream>
#include "DerivedException.hpp"
using namespace std;
 
int main() {
  try {
    int option;
    cout << "Enter option (1-2): ";
    cin >> option;
    BaseException baseEx(-2);
    DerivedException derivedEx(4,5);
    switch(option) {
      case 1:
        throw baseEx;
        break;
      case 2:
        throw derivedEx;
        break;
    }
  } catch(BaseException ex) {
    cout << ex.message();
  } catch(DerivedException ex) {
    cout << ex.message();
  }
  return 0;
}

O παραπάνω κώδικας παράγει το παρακάτω warning κατά τη μεταγλώττιση:

ExceptionUse.cpp:22:5: warning: exception of type ‘DerivedException’ will be caught
   } catch(DerivedException &ex) {
     ^
ExceptionUse.cpp:20:5: warning:    by earlier handler for ‘BaseException’
   } catch(BaseException &ex) {
     ^

το οποίο εν συντομία λέει ότι ένα exception τύπου DerivedException θα “πιαστεί” από το 1ο catch block και το 2ο catch block δεν θα λειτουργήσει ποτέ. Το παραπάνω είναι λογικό διότι ένα αντικείμενο της κλάσης DerivedException είναι και BaseException με βάση τις αρχές της κληρονομικότητας.

Εκτελέστε όμως τον παραπάνω κώδικα (παρά το warning) δίνοντας ορίσμα τους αριθμούς 1 και 2. Το αποτέλεσμα είναι το εξής:

gthanos@gthanos-DESKTOP:~/Downloads/C++$ ./ExceptionUse 
Enter option (1-2): 1
BaseException, a: -2
gthanos@gthanos-DESKTOP:~/Downloads/C++$ ./ExceptionUse 
Enter option (1-2): 2
BaseException, a: 4

Παρατηρήστε ότι ενώ στην 2η περίπτωση παράγεται ένα DerivedException το αντικείμενο που τελικά λαμβάνουμε είναι τύπου BaseException. Εδώ θα πρέπει να τονίσουμε ότι η συνάρτηση message() είναι virtual πράγμα που διαισθητικά μας οδηγεί στο συμπέρασμα ότι θα κληθεί η κατάλληλη έκδοση της συνάρτησης με βάση τον τύπο του αντικειμένου για το οποίο καλείται, ανεξάρτητα από τον τύπο της (δες δυναμικό πολυμορφισμό).

Η απάντηση στο παραπάνω ερώτημα είναι ότι αν και παράγεται ένα αντικείμενο τύπου DerivedException αυτό γίνεται catch από το πρώτο catch block. Μέσα στο catch block το αρχικό αντικείμενο αντιγράφεται σε ένα άλλο αντικείμενο τύπου BaseException, διότι έχουμε κλήση με τιμή στο catch block. Πρακτικά αυτό σημαίνει ότι από το αρχικό αντικείμενο κρατάμε οτιδήποτε ανήκει στην κλάση BaseException και απορρίπτουμε το υπόλοιπο.

Ο τρόπος για να δουλέψει σωστά ο παραπάνω κώδικας είναι μέσα στο catch block να μην περάσουμε το αντικείμενο γιατί δημιουργείται αντίγραφο του, αλλά να περάσουμε μία αναφορά σε αυτό, όπως παρακάτω:

ExceptionUse.cpp
#include <iostream>
#include "DerivedException.hpp"
using namespace std;
 
int main() {
  try {
    int option;
    cout << "Enter option (1-2): ";
    cin >> option;
    BaseException bex(-2);
    DerivedException dex(4,5);
    switch(option) {
      case 1:
        throw bex;
        break;
      case 2:
        throw dex;
        break;
    }
  } catch(BaseException &ex) {
    cout << ex.message();
  } catch(DerivedException &ex) {
    cout << ex.message();
  }
  return 0;
}

Πλέον το αποτέλεσμα της εκτέλεσης είναι το αναμενόμενο

gthanos@gthanos-DESKTOP:~/Downloads/C++$ ./ExceptionUse 
Enter option (1-2): 1
BaseException, a: -2
gthanos@gthanos-DESKTOP:~/Downloads/C++$ ./ExceptionUse 
Enter option (1-2): 2
DerivedException, a: 4, b: 5

Συνιστάται, η διαχείριση μιας εξαίρεσης με χρήση αναφοράς για αντικείμενα σύνθετου τύπου (δηλαδή όχι τύπου char, int, long, double κλπ), διότι α) αποφεύγουμε την αντιγραφή του αντικειμένου μέσα στο catch block (πιο γρήγορη κλήση) και β) αποφεύγουμε την “αποκοπή” μέρους του αντικειμένου της εξαίρεσης λόγω του χειρισμού της από ένα catch block βασικότερου τύπου.

Είναι προφανές ότι η σειρά των catch blocks θα έπρεπε να είναι η αντίστροφη (πρώτα το catch block για την αναφορά τύπου DerivedException και στη συνέχεια το catch block για την αναφορά τύπου BasedException. Ο λόγος είναι ότι εάν παραμείνει η σειρά των catch blocks ως έχει το 2o catch block δεν εκτελείται ποτέ.

cpp/exception_inheritance.txt · Last modified: 2023/05/15 14:08 by gthanos