User Tools

Site Tools


cpp:function_try_blocks

Εξαιρέσεις που παράγονται στον κατασκευαστή

Ας υποθέσουμε ότι έχουμε την παρακάτω κλάση Name η οποία περιγράφει ένα όνομα

Name.hpp
#include <cstring>
 
class Name {
  char* name;
public:
  Name();
  Name(const char *name);
  ~Name();
  Name& operator=(const Name&);
};
 
Name::Name() {
  cout << "Name default constructor." << endl;
  name = nullptr;
}
 
Name::Name(const char *name) {
  cout << "Name constructor: " << name << endl;
  this->name = strdup(name);
}
 
Name::~Name() {
  cout << "Name destructor: " << name << endl;
  free(name);
  name = nullptr;
}
 
Name& Name::operator=(const Name& a) {
  cout << "--- Operator= ---\n";
  if(name != nullptr) 
    free(name);
  name = strdup(a.name);
  return *this;
}

και την κλάση Person η οποία περιγράφει έναν άνθρωπο. Η κλάση διαθέτει τα εξής δύο πεδία:

  Name* firstname;  // pointer to Name
  Name lastname;    // Name

Επιπλέον ο κατασκευαστή της κλάση Person παράγει ένα exception τύπου BadName. Η κλάσεις Person και BadName περιγράφονται παρακάτω:

Person.hpp
#include "Name.hpp"
#include "BadName.hpp"
 
class Person {
protected:
  Name* firstname;
  Name  lastname;
public:
  Person(const char *first, const char *last);
  ~Person();
};
 
Person::Person(const char *first, const char *last) {
  cout << "Person constructor" << endl;
  firstname = new Name(first);
  lastname = Name(last);
  BadName bad;
  throw bad;
}
 
Person::~Person() {
  cout << "Person destructor" << endl;
  delete firstname;
}
BadName.hpp
class BadName : public std::exception {
public:
  const char* what() {
    return "BadName";
  }
};

Εάν μεταγλωττίσουμε και εκτελέσουμε την παρακάτω συνάρτηση main ο κώδικας εκτυπώνει τα εξής:

CreatePerson.cpp
#include <iostream>
using namespace std;
 
#include "Person.hpp"
 
int main() {
  try {
    Person johnSnow("John", "Snow");
  } catch(BadName& a) {
    cout << "Exception occured: " << a.what() << endl;
  }
}
Name default constructor.
Person constructor
Name constructor: John
Name constructor: Snow
--- Operator= ---
Name destructor: Snow
Name destructor: Snow
Exception occured: BadName

Από τις εκτυπώσεις, παρατηρούμε ότι καταστρέφεται το αντικείμενο lastname, αλλά όχι και το αντικείμενο firstname. Ο λόγος είναι ότι δημιουργηθεί ένα exception στον κατασκευαστή, δεν καλείται ποτέ ο καταστροφέας της κλάσης. Καταστρέφονται αυτόματα όλα τα αντικείμενα που έχουν δεσμευτεί στο stack, και λαμβάνει χώρα η διαδικασία του stack unwinding.

Ως εκ τούτου, δεν καταστρέφονται τα αντικείμενα που είναι δεσμευμένα στο heap και γενικότερα τα resources που περιμένουμε να απελευθερωθούν στον καταστροφέα της κλάσης.

Ο λόγος που εκτυπώνεται 2 φορές το μήνυμα “Name destructor: Snow”, είναι διότι δημιουργούνται δύο αντικείμενα τύπου Name, ένα κατά την αρχικοποίηση του αντικειμένου Person (με τη βοήθεια του default constructor)και ένα στον κατασκευαστή της κλάσης στη γραμμή lastname = Name(last);.

Function Try Blocks

Προκειμένου να λυθεί το συγκεκριμένο πρόβλημα η C++ υποστηρίζει τα λεγόμενα function try blocks. Ουσιαστικά πρόκειται για μία ειδική σύνταξη try - catch που περιλαμβάνει το σύνολο του κατασκευαστή, ως εξής:

Person::Person(const char *first, const char *last) try {
  cout << "Person constructor" << endl;
  firstname = new Name(first);
  lastname = Name(last);
  BadName bad;
  throw bad;
}
catch(BadName& ex) {
  cout << "\n--> firstname deleted!" << endl;
  delete firstname;
}

Εάν ανανεώσετε τον κώδικα του κατασκευαστή τώρα το πρόγραμμα σας κατά την εκτέλεση εκτυπώνει:

Name default constructor.
Person constructor
Name constructor: John
Name constructor: Snow
--- Operator= ---
Name destructor: Snow
Name destructor: Snow

--> firstname deleted!
Name destructor: John
Exception occured: BadName

Παρατηρήστε ότι αν και εκτελείται ο κώδικας μέσα στο catch block, η εξαίρεση “μεταφέρεται” και στη συνάρτηση main. Ο λόγος είναι ότι η εξαίρεση, αναπαράγεται αυτόματα στο τέλος του catch block. Ο μόνος τρόπος να το αποφύγετε αυτό είναι να κάνετε throw μία νέα εξαίρεση, διαφορετικού τύπου.

Exception που συμβαίνουν στον κατασκευαστή της προγόνου κλάσης

Ας υποθέσουμε την κλάση Student που είναι απόγονος της κλάσης Person και δηλώνεται ως εξής:

Student.hpp
class Student: public Person {
  int aem;
public:
  Student(const char *first, const char *last, int id);
};
 
Student::Student(const char *first, const char *last, int id): Person(first, last), aem(id) {
  cout << "Student constructor" << endl;
} 

Εάν επιθυμούμε να “πιάσουμε” την εξαίρεση εντός του κατασκευαστή της κλάσης Student και όχι εντός της Person, θα πρέπει να κρατήσουμε τον κατασκευαστή της κλάσης Person στην αρχική του μορφή (πριν την τελευταία αλλαγή) και να ξαναγράψουμε τον κατασκευαστή της κλάσης Student ως εξής:

Student.hpp
class Student: public Person {
  int aem;
public:
  Student(const char *first, const char *last, int id);
};
 
Student::Student(const char *first, const char *last, int id) try : Person(first, last), aem(id) {
  cout << "Student constructor" << endl;
} 
catch(BadName& ex) {
  cout << "--> firstname deleted!" << endl;
  delete firstname;
}

Η παραπάνω σύνταξη επιτρέπει να πιάσουμε το exception που συμβαίνει στον κατασκευαστή της προγόνου κλάσης. Και εδώ το exception θα αναπαραχθεί σύμφωνα με τους κανόνες που ίσχυσαν και στο προηγούμενο παράδειγμα.

Παρατηρήστε ότι δεν εκτυπώνεται ποτέ το μήνυμα “Student constructor” του κατασκευαστή. Γιατί?

Η μέθοδος main δίνεται παρακάτω:

main.cpp
#include <iostream>
using namespace std;
 
#include "Person.hpp"
#include "Student.hpp"
 
int main() {
  try {
    Student johnSnow("John", "Snow", 1234);
  } catch(BadName& a) {
    cout << "Exception occured: " << a.what() << endl;
  }
}
cpp/function_try_blocks.txt · Last modified: 2022/05/23 06:05 by gthanos