Wyjątki

Czym są wyjątki

Wyjątki to inaczej błędy generowane podczas wykonywania programu - sytuacje wyjątkowe, anomalie.

W C++ istnieje specjalny mechanizm do obsługi takich sytuacji.

Podstawy obsługi wyjątków

Większość sytuacji wyjątkowych można rozwiązać z użyciem standardowych mechanizmów:

  • konstrukcje if..else:
     if(!myfile){
        cout << "Nie można otworzyć pliku!" << endl;
        return 1;
      }else{
        //wczytywanie
      }
  • konstrukcje z assert:
    #include <assert.h>
    ...
    int a, b, c
    cin >> a >> b;
    // Program zostanie zatrzymany gdy b == 0
    assert(b);
    c=a/b;

Te konstrukcje wymagają jednak, aby sytuacja wyjątkowa była rozwiązana lokalnie: w miejscu w którym się pojawiła. Rozpatrywanie większości sytuacji wyjątkowych w miejscu ich wystąpienia nie jest wygodne ani wręcz możliwe (nie dysponujemy odpowiednim kontekstem w jaki sposób powinna zostać obsłużony błąd).

int bar(int v) {
  if (v == 2) {
    ...
    return 0; //OK
  } else {
   //sugeruje blad funkcja spodziewala sie 
   //dwóch parametrów programu
    return -1;
  }
}
 
inf foo(int args_number) {
  int result = bar(args_number);
  if (result < 0) {
     //jeśli nastąpił błąd w funkcji bar
     //propaguje wynik do wywołania
     return result;
  } else {
    ...
  }
}
 
int main(int argc, char *argv[]) {
  int result = foo(argc);
  if (result < 0) {
     //dopiero z punktu wiedzenia funkcji main wiadomo,
     //że w tym wypadku należy poinformować użytkownika
     //o błędzie
     cerr << "nie poprawna ilość argumentów" << end;
     return -1;
  }
  ...
  return 0;
}

Alternatywna metoda w postaci zwracania kodów błędu z funkcji jest w praktyce niewygodna do stosowania i łatwo o przoczenie instrukcji warunkowej sprawdzającej przypadek błędnego wykonania.

Mając daną metodę klasy Matrix odpowiedzialną za dodawanie macierzy i zwracającą obiekt klasy macierz będący wynikiem dodawania, w jaki sposób można przekazać informację o tym, że wymiary podanych macierzy są nieprawidłowe do kodu wywołującego tę metodę, gdzie już wiadomo jak należy obsłużyć tą wyjątkową sytuację a z pominięciem wszystkich pośrednich wywołań funkcji (takich jak foo powyżej)? Odpowiedzią na ten problem są wyjątki.

Mechanizm wyjątków pozwala na przerwanie działania programu w momencie pojawienia się sytuacji wyjątkowej i wycofanie się do fragmentu kodu który tę sytuację wie jak obsłusłużyć (np. poprosić użytkownika o ponowne wprowadzenie danych, albo zamknięcie programu, albo ciche pominięcie problemu):

#include <stdexcept>
#include <iostream>
 
using namespace std;
 
// Klasa reprezentująca naszą sytuację wyjątkową
class WrongDimensionsException : public invalid_argument {
  public:
    WrongDimensionsException(const Matrix &m) : invalid_argument{"Wrong dimensions of matrix"}, m_{m}{}
  public:
    Matrix GetMatrix() const {
       return m_;
    }
  private:
    Matrix m_;  
};
 
class Matrix{
  public:
    ...
    Matrix add(const Matrix& m);
  private:
    int rows, cols;
    double** tab;
};
 
Matrix Matrix::add(const Matrix& m) {
  // jeśli wymiary nie są prawidłowe, to zgłoś wyjątek
  if( !validDimensions(*this,m) ) {
      throw WrongDimensionsException(m);
  }
 
  Matrix result;
  for(int r = 0; r < rows; r++){
    for(int c = 0; c < cols; c++){
      result.set(r,c,this.tab[r][c]+m.tab[r][c]);
 
  return result;
 
}
 
int static_main(){
  Matrix m1("[1 2 ; 3 4]");
  Matrix m2("[3 4 4; 5 6 4]");
 
  try {
    cout << m1.add(m2) << endl;
  } catch(const WrongDimensionsExcepion &w) {
    // jesli wyjątek zostanie zgłoszony program wejdze tutaj 
    // i wyświetli informacje
    cerr << w.what() << ": " << w.GetMatrix() << endl;
  }
}
 
int user_main() {  
  while(true) {
    try {
      std::string m1_str;
      std::string m2_str;
      cin >> m1_str;
      cin >> m2_str;
      Matrix m1(m1_str);
      Matrix m2(m2_str);
      cout << "Result is: " << m1.add(m2) << endl;
    } catch(const WrongDimensionsExcepion &w) {
      // nawet jesli wyjątek zostanie zgłoszony zostanie tutaj wyświetlona
      // informacja dla użytkownika
      cerr << "Wrong dimensions of Matrices. Please enter matrices once again..." << endl;
    }
  }
}
 
int main() {
  static_main();
  user_main();
}

Zwijanie stosu

Kiedy wyjątek zostaje wyrzucony program cofa się aż do miejsca gdzie znajduje się blok catch, który go obsługuje. Dla wszystkich obiektów które znajdowały się w obszarach z których program się wycofuje wywoływane są destruktory.

Problem pojawia się jednak gdy bezpośrednio alokujemy pamięć.

Zwijanie stosu

int foo() {
  string s("ala ma kota");
  bar();
  return 0;
}
 
int bar() {
  // pamięć nie 
  // zostanie zwolniona
  int * tab = new int[100];
  fido();
  return 0;
}
 
int fido() {
  Matrix m("[1 2 3; 3 4 5]");
  throw runtime_error(__func__ + " error in " + __FILE__ + " at " + __LINE__);
  return 0;
}
 
int main(){
  try{
 
    foo();
 
  } catch(const exception &e){
    cerr << e.what() << endl;
  }
 
  cout << "Dalej" << endl;
}

Zgłoszenie wyjątku

Do zgłoszenia wyjątku (wyrzucenia wyjątku) służy słowo kluczowe throw. Operator throw posiada jeden parametr (w szczególnych przypadkach nie posiada żadnego parametru), który może być dowolnego typu. Oznacza to, ze jako wyjątek możemy wyrzucić dowolny obiekt dowolnej klasy. Zazwyczaj jednak pisze się osobne klasy dla wyjątków o nazwach tłumaczących sens danego wyjątku.

Istnieje także specjalny plik nagłówkowy exception zawierający domyślną implementacje klasy do zgłaszania wyjątków.

using namespace std;
 
Matrix Matrix::div(const Matrix& m) {
  // jeśli wymiary nie są prawidłowe, to zgłoś wyjątek
 
  if( !validDimensions(*this,m) ) throw WrongDimensionException(m);
  if( zeroDeterminant(m) ) throw DivisionByZero(m);
 
  ...
 
}

Przechwycenie wyjątku

try/catch

Do przechwytywania wyjątków służy konstrukcja try/catch. Blok try powinien obejmować wszystkie operacje potencjalnie generujące wyjątki. Po zamknięciu bloku try następuje jeden lub kilka bloków catch które zostaną uruchomione w przypadku zaistnienia wyjątku który obsługują. Istnieje także specjalna konstrukcja bloku catch, która działa analogicznie jak else:

#include <iostream>
#include "Matrix.h"
#include "WrongDimensionException.h"
#include "DivisionByZero.h"
 
 
int main(){
  Matrix m1("[1 2 ; 3 4]");
  Matrix m2("[3 4 4; 5 6 4]");
 
  try{
    cout << m1.div(m2) << endl;
  }catch(WrongDimensionException w){
    w.printMessage();
  }catch(DivisionByZero d){
    d.printMessage();
  }catch(...){
    cout << "Został zgłoszony inny wyjątek!" << endl;
  }
}
 

Oznacza on mniej więcej tyle: jeśli pojawił się wyjątek, którego nie mają w parametrach wcześniejsze bloki catch ja się nim zajmę. Uwaga Kolejność bloków catch ma znaczenie! Konstrukcja catch(…) pasuje do wszystkich wyjątków, dlatego powinna być umieszczana zawsze jako ostatnia.

Jeśli dwa różne wyjątki mają wspólną klasę bazową można je przechwycić w pojedynczym bloku catch:

int main() {
  Matrix m1 ...
  Matrix m2 ...
  try {
     m1.div(m2);
  } catch (const invalid_argument &e) {
     cerr << e.what() << endl;
  } catch (...) {
     //ten blok jest wstanie przechwycić każdy inny wyjątek
     cerr << "Something went wrong" << endl;
  }
}

Wyjątki nieoczekiwane

W przypadku kiedy wewnątrz funkcji pojawi się wyjątek, który nie znajduje się w specyfikacji wyjątków danej funkcji, wywoływana jest automatycznie funkcja unexpected, która z kolei wywołuje funkcje ustawioną przez set_unexpected. Domyślnie funkcją tą jest terminate - czyli zakończenie programu.

Aby ustawić swoją własną funkcje, która zostanie wywołana w przypadku wystąpienia niespodziewanego wyjątku należy przekazać do set_unexpected wskaźnik do funkcji zwracającej void i nie pobierającej żadnych elementów.

// set_unexpected example
#include <iostream>
#include <exception>
using namespace std;
 
void myunexpected () {
  cerr << "unexpected called\n";
  throw 0;     // throws int (in exception-specification)
}
 
void myfunction () throw (int) {
  throw 'x';   // throws char (not in exception-specification)
}
 
int main (void) {
  set_unexpected (myunexpected);
  try {
    myfunction();
  }
  catch (int) { cerr << "caught int\n"; }
  catch (...) { cerr << "caught other exception \n"; }
  return 0;
}

Jeśli zdefiniowana przez programistę funkcja nie rzuca wyjątku określonego w specyfikacji funkcji (patrz Specyfikacji wyjątków, to program po jej wywołaniu i tak zostanie zakończony.

noexcept

Specyfikacja noexcept pozwala na jawne zadeklarowanie metody jako nie wyrzucającej wyjątków, wtedy żadna pochodna metoda również nie może wyrzucać wyjątków, wszystkie pozozstałe motedy są traktowane jako potencjlanie wyrzucające wyjątki:

#include <iostream>
#include <exception>
#include "WrongDimensionException.h"
#include "DivisionByZero.h"
 
using namespace std;
 
class Matrix{
  ...
  public:
    ...
    // Metoda potencjalnie wyrzuca wyjątki
    Matrix div(const Matrix&);
    // Metoda nie może wyrzucić wyjątku
    virtual ~Matrix() noexcept;
};

Konstruktory, destruktory i wyjątki

Wyjątki w konstruktorze

Dobrym przykładem wykorzystania wyjątków jest ich wyrzucanie w konstruktorach obiektów. Konstruktor nie posiada zwracanego typu, dlatego w przypadku niepowodzenia w utworzeniu obiektu, nie ma możliwości zwrócenia kodu błędu. W wyniku niepowodzenia konstruktora powstaje tak zwany obiekt zombie - istnieje, ale nie jest popranym obiektem.

Wyjątki w konstruktorach wyrzuca się w analogiczny sposób jak w zwykłych metodach. Kiedy zostanie wyrzucony wyjątek w ciele konstruktora, mechanizm zwijający stos uruchomi destruktory wszystkich obiektów składowych danego obiektu.

W praktyce zawsze należy sprwdzić dziedzinę parametrów przekazanych w konstruktorze i pozowalać na utworzenie jedynie w pełni poprawnego obiektu, namtomiast w przeciwnym wypadku należy wyrzucić wyjątek.

Problem pojawia się jednak kiedy w ciele konstruktora dynamicznie alokujemy pamięć. Nie zostanie ona w takim wypadku zwolniona. Chyba, że użyjemy smart pointery.

Wyjątki w destruktorze

Nigdy nie należy rzucać wyjątków w destruktorach! Jak zostało powiedziane w poprzedniej sekcji (Wyjątki w konstruktorach) w przypadku kiedy zostaje zgłoszony wyjątek w konstruktorze, dla wszystkich obiektów składowych danego obiektu wywoływane są destruktory. Zakładając, że któryś z tych destruktorów wyrzuciłby wyjątek, c++ runtime jest w sytuacji bez wyjścia: który z wyjątków anulować, a który wyrzucić dalej?

C++ gwarantuje co prawda, że w przypadku zaistnienia takiej sytuacji zostanie wywołana funkcja terminate() i program zostanie zamknięty, nie jest to jednak satysfakcjonujące rozwiązanie.

Od wersji C++11 destuktory domyślnie są zadeklarowane jako noexcept.

Dziedziczenie i wyjątki

(:!: do zrozumienia tej sekcji wymagana jest znajomość mechanizmu dziedziczenia w C++ → laboratorium Dziedziczenie i polimorfizm)

Ponieważ jako wyjątek można zgłosić dowolny obiekt, może zdążyć się, że wyrzuconych zostanie kilka obiektów z tej samej hierarchii dziedziczenia. Dodatkowo jeśli metoda ma wyspecyfikowany jako rzucany wyjątek A, to może wyrzucić wyjątek B, który dziedziczy po A i nie zostanie wywołana funkcja unexpected.

Mechanizm dziedziczenia w odniesieniu do wyjątków pozwala na polimorficzne przetwarzanie wyjątków, ale jednocześnie wymusza pamiętanie o tym, że kolejność bloków catch jest ważna podczas kombinacji dziedziczenie-wyjątki.

Rozważ poniższy kod:

#include <iostream>
using namespace std;
 
class CircleException{
  // Oznacza ze nie mozna wyrysowac kola
};
 
class BallException : public CircleException{
  // Oznacza ze nie mozna wyrysowac kuli
};
 
void drawBall() {
  throw BallException();
}
 
int main(){
  try{
    drawBall();
  }catch(CircleException a){
    cout << "Blad podczas rysowania kola" << endl;
  }catch(BallException b){
    cout << "Blad podczas rysowania kuli" << endl;
  }
 
}

Już podczas kompilacji otrzymujemy ostrzeżenie, że wyjątek BallException nigdy nie zostanie wychwycony. Dzieje się tak dlatego, że po napotkaniu pierwszego bloku catch, nastąpi automatyczne rzutowanie w górę i dopasowanie BallException do CircleException.

Warto się zapoznać

Ćwiczenia

  1. [1 plus] Przetestuj przykład z sekcji Dziedziczenie i wyjątki. Co zrobić, żeby wyjątek BallException był łapany poprawnie?
  2. [3 plusy] Napisz klasę PESEL, która w konstruktorze przyjmuje ciąg znaków będących numerem PESEL. Klasa powinna posiadać metodę validatePESEL(const char*), która sprawdza czy PESEL jest poprawny według algorytmu podanego na Wikipedii. Jeśli przekazany do konstruktora PESEL nie jest poprawny, program powinien wyrzucać wyjątek. Napisz funkcję main i przetestuj program.
  3. [3 plusy] Dla klasy Student dopisać metody walidujące argumenty przekazywane do klasy. W taki sposób by uniemożliwić storzenie niepoprawnego stanu obiektu.
    • Student musi mieć co najmniej imię i nazwisko, każdy człon imienia musi być napisany z wielkiej litery pozostałe litery małe. Imię nie może zawierać cyfr i znaków specjalnych (amperad, dolar, procent, itp…)
    • Wiek studenta musi mieścić się w zakresie 10 do 100 lat
    • kierunek studiów musi zawierać się w zbiorze oferowanym akutalnie przez uczelnie XYZ: informatyka, ekonomia, matematyka, fizyka, filozofia
    • w każdym wypadku, kiedy zostanie naruszony któryś z warunków należy zwrócić wyjątek dziedziczący po klasie invalid_argument, odpowiednio:
      • InvalidNameSurname, InvalidNameCharacters, InvalidAge, InvalidProgram
    • dopisać main z możliwością wczytywania danych o studencie i tworzącym studentów w pętli wstawiającym poprawnie utworzone obiekty do repozytorium.

Zadanie domowe

Napisz program, który będzie służył do opóźniania, lub przyspieszania wyświetlania napisów do filmów w dwóch różnych formatach: MicroDVD i SubRip. Elementem centralnym interfejsu biblioteki jest abstrakcyjna klasa: MovieSubtitles z dwoma wirtualnymi metodami (przestrzeń nazw moviesubs):

  • void ShiftAllSubtitlesBy(int offset_in_micro_seconds, int frame_per_second, std::istream *in, std::ostream *out)

    - metoda nie powinna posiadać domyślnej implementacji (abstrakcyjna, czysto wirtualna).

  • Destruktor (domyślna implementacja)
  1. [1 punkt] (lab8_micro_dvd_correct_cases_tests) Zaimplementuj klasę MicroDvdSubtitles implementującą metody z klasy bazowej MovieSubtitles i wspierającą format MicroDvd. Każda linia ma następujący format: {INT_ON}{INT_OFF}SUBTITLE gdzie nawiasy klamrowe wystepują w tej linii jak widać, natomiast INT_ON oznacza numer klatki filmu, w której pojawia się napis; INT_OFF numer klatki filmu w której znika napis; SUBTITLE oznacza napis do wyświetlenia. W tym formacie napis do wyświetlenia może zawierać dodatkowe informacje sterujące wyświetlaniem, np. | rozdzielający linie napisu, czy {y:b} wprowadzający pogrubienie.
  2. [1 punkt] (lab8_micro_dvd_error_cases_tests) Przesuwanie napisów powinno również obsługiwać przypadki, gdy podany plik (zawrtość strumienia wejściowego) zawiera niepoprawnie sformatowany tekst:
  • próba przesunięcia klatek wstecz która skutkowałaby ujemnymi wartościami INT_ON i/lub INT_OFF powinna być zasygnalizowana przez wyjątkiem NegativeFrameAfterShift
  • jeśli klatka ma zniknąć przed pojawieniem się (INT_ON >= INT_OFF) należy zasygnalizować to wyjątkiem SubtitleEndBeforeStart
  • jeżeli jest inny błąd formatowania lini np. brak {INT_OFF}, brak nawiasu klamrowego, itp. należy zwrócić wyjątek InvalidSubtitleLineFormat
  • jeżeli kolejne napisy pojawiają sie przed niż wcześniejszymi napisami powinine zostać zgłoszony wyjątek OutOfOrderFrames
  • wszystkie te wyjątki powinny dziedziczyć po klasie SubtitlesException, która przyjmuje dwa argumenty w konstruktorze: numer linii, w której wystąpił błąd, treść tej linii (cała linia, nie tylko etykieta). A z kolei klasa SubtitlesException powinna dziedziczyć po klasie std::invalid_argument z biblioteki stdexcept
  1. [1 punkt] (lab8_sub_rip_correct_cases_tests) Zaimplementuj klasę SubRipSubtitles implementującą metody z klasy bazowej MovieSubtitles i wspierającą format SubRip. Każda napis ma następujący format: INDEX NEW_LINE TIME_IN → TIME_OFF NEW_LINE SUBTITLE1 NEW_LINE SUBTITLE2 NEW_LINE NEW_LINE, gdzie INDEX - to numer klatki, NEW_LINE to znak nowej linii, TIME_IN i TIME_OFF to moment pojawienia się i zniknięcia klatki w formacie HH:MM:SS,mmm (godzina:minuta:sekundy,milisekundy). Napisy mogą być wielolinijkowe, nowa klatka pojawia się po pustej linii.
  2. [1 punkt] (lab8_sub_rip_error_cases_tests) poza wyjątkami wymienionymi w punkcie drugim należy zaimplementować też:
  • numery klatek są nie pokolei OutOfOrderFrames
  • jeśli klatka nie posiada secyfikacji kiedy miałaby się pojawić MissingTimeSpecification
  1. [1 punkt] (lab8_movie_subtitles_tests) sprawdzić czy wszystko działa ok.
pl/dydaktyka/jimp2/2017/labs/wyjatki.txt · ostatnio zmienione: 2019/06/27 15:50 (edycja zewnętrzna)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0