1 grudnia 2013

[C++11] Wyjątki i noexcept

Jeżeli wyjątek wystąpi w konstruktorze, nawet jeżeli obiekt jest tylko częściowo stworzony standard gwarantuje, że stworzone już dotąd obiekty zostaną poprawnie zniszczone. Tak samo w przypadku gdy wyjątek wystąpi w momencie inicjalizacji tablicy albo kontenera.

Jeżeli destruktor działa na operacji, która może zniszczyć obiekt, powinien on z-wrapować taką operację w blok try i obsłużyć wyjątek lokalnie. Destruktory nie powinny rzucać wyjątków! Jeżeli podczas zwijania stosu destruktor rzuci wyjątkiem którego sam nie złapał program zostanie zterminowany.
#include <iostream>
using namespace std;

struct MyClass {
    ~MyClass() /* noexcept */ {
        throw std::exception();
    }
};

int main() {
    MyClass a;
    return 0;
}
Wynik:
terminate called after throwing an instance of 'std::exception'
  what():  std::exception
The program has unexpectedly finished.
Kilka zasad dotyczących tworzenia, rzucania i łapania wyjątków:
  • Obiekty wyjątków, które są rzucane, muszą mieć dostępny destruktor, a także konstruktor kopiujący lub konstruktor move.
  • Najbardziej wyspecjalizowany catch musi pojawić się jako pierwszy.
  • Aby wyrzucić wyjątek wyżej, trzeba podać throw bez wyrażenia (throw;).
  • Aby złapać wyjątek każdego typu trzeba skorzystać z catch(...), który powinien zawsze pojawiać się jako ostatni.
#include <iostream>
#include <typeinfo>
#include <stdexcept>
using namespace std;

void old() {
    throw std::runtime_error("old()");
}

void young() {
    try {
        old();
    } catch(const std::bad_cast& e) {
        cout << e.what() << endl;
    } catch(...) {
        cout << "unknown, rethrow" << endl;
        throw;
    }
}

int main() {
    try {
        young();
    } catch(const std::runtime_error& e) {
        cout << "in main: " << e.what() << endl;
    }

    return 0;
}
Wynik:
unknown, rethrow
in main: old()

Lista inicjalizacyjna konstruktora

Konstruktor nie może złapać wyjątków rzuconych z listy inicjalizacyjnej. Aby się przed tym ustrzec, blok try musi znajdować się przed dwukropkiem. Zachowanie jest podobne w działaniu do zwykłych try-catch, z tą różnicą że złapany wyjątek zostanie ponownie wyrzucony z bloku.
#include <iostream>
#include <stdexcept>
using namespace std;

struct Member {
    Member() { }
    Member(Member& m) {
        throw std::runtime_error("Constructor");
    }
};

struct BadCalss {
    Member member;
    BadCalss(Member& m) try : member(m) {
        // body
    } catch(...) {
        cout << "some bug" << endl;
    }
};

int main() {
    Member m;
    BadCalss bc(m);
    return 0;
}
Wynik:
some bug
terminate called after throwing an instance of 'std::runtime_error'
  what():  Constructor
The program has unexpectedly finished.

noexcept

W nowym standardzie pojawił się specjalny specyfikator noexcept. Jeżeli wiemy, że funkcja nie będzie rzucać wyjątku, może z tej informacji skorzystać i programista i kompilator. Wspominał o tym również Scott Meyers w swoim wykładzie na Going Native 2013. Kompilator w takim przypadku wygeneruje znacznie mniej kodu, który będzie bardziej optymalny w działaniu.
#include <iostream>
#include <stdexcept>
using namespace std;

void fun() noexcept {
    throw std::runtime_error("fun()");
}

// gun() ma taki sam specyfikator rzucanych typów jak fun()
void gun() noexcept(noexcept(fun())) {
    cout << "gun()" << endl;
}

int main() {
    fun();
    return 0;
}
Wynik:
terminate called after throwing an instance of 'std::runtime_error'
  what():  fun()
The program has unexpectedly finished.
W rezultacie noexpect powinno być stosowane w dwóch przypadkach. Kiedy jesteśmy przekonani, że funkcja nie rzuci wyjątku albo/i gdy nie chcemy obsługiwać błędu. Jeżeli z funkcji (oznaczonej jako noexcept) zostanie jednak rzucony wyjątek, natychmiast zawołane zostanie std::terminate().

Inny przykład, w którym wyjątek przelatuje przez funkcję z noexcept, co powoduje zterminowanie programu.
#include <iostream>
#include <stdexcept>

using namespace std;


void fun_a() {
    throw std::runtime_error("bug");
}

void fun_b() noexcept {
    fun_a();
}

void fun_c() {
    try {
        fun_b();
    } catch(...) {
        cout << "Exception caught" << endl;
    }
}

int main() {
    fun_c();
}
Wynik:
terminate called after throwing an instance of 'std::runtime_error'
  what():  bug
The program has unexpectedly finished.
Niektóre kontenery jak np. std::vector, sprawdzają (podczas realokacji wektora), czy "move konstruktor" jest oznaczony jako noexcept. Jeżeli tak nie jest zostanie zwołany zwykły konstruktor kopiujący, co jest operacją dużo wolniejszą. Odpowiedni eksperyment stworzyłem w innym artykule.
Poza "move konstruktorem" noexcept standardowo powinno znaleźć się również w deklaracji destruktora.

Stare - throw()

W poprzedniej wersji standardu istniało słowo kluczowe throw(), które obecnie jest uznawane za przestarzałe. Można było za jego pomocą wyspecyfikować jakiego rodzaju wyjątki mogą być wyrzucone z funkcji, albo (odpowiednik noexpect) że funkcja nie rzuci żadnego wyjątku.
int fun(std::string& b) throw();

Brak komentarzy:

Prześlij komentarz