Czasami możesz napotkać przypadek, w którym chcesz przechwycić wyjątek, ale nie chcesz (lub nie masz możliwości) w pełni go obsłużyć w momencie złapania to. Jest to częste zjawisko, gdy chcesz zarejestrować błąd, ale przekazać go wywołującemu, aby go faktycznie obsłużył.
Gdy funkcja może używać kodu powrotu, jest to proste. Rozważmy następujący przykład:
Database* createDatabase(std::string filename)
{
Database* d {};
try
{
d = new Database{};
d->open(filename); // załóżmy, że zgłasza to wyjątek int w przypadku niepowodzenia
return d;
}
catch (int exception)
{
// Tworzenie bazy danych nie powiodło się
delete d;
// Zapisz błąd w jakimś globalnym pliku dziennika
g_log.logError("Creation of Database failed");
}
return nullptr;
}W powyższym fragmencie kodu funkcja ma za zadanie utworzyć obiekt Database, otworzyć bazę danych i zwrócić obiekt Database. W przypadku, gdy coś pójdzie nie tak (np. zostanie przekazana zła nazwa pliku), procedura obsługi wyjątku rejestruje błąd, a następnie w rozsądny sposób zwraca wskaźnik zerowy.
Rozważmy teraz następującą funkcję:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // zgłasza wyjątek int w przypadku niepowodzenia
}
catch (int exception)
{
// Zapisz błąd w jakimś globalnym pliku dziennika
g_log.logError("getIntValueFromDatabase failed");
// Jednak tak naprawdę nie uporaliśmy się z tym błędem
// Co więc tutaj zrobimy?
}
}W przypadku, gdy ta funkcja się powiedzie, zwraca wartość całkowitą — dowolna wartość całkowita może być prawidłową wartością.
Ale co w przypadku, gdy coś pójdzie nie tak z funkcją getIntValue()? W takim przypadku getIntValue() zgłosi wyjątek będący liczbą całkowitą, który zostanie przechwycony przez blok catch w getIntValueFromDatabase(), który zarejestruje błąd. Ale jak w takim razie powiedzieć osobie wywołującej getIntValueFromDatabase(), że coś poszło nie tak? W przeciwieństwie do najwyższego przykładu, nie możemy tu użyć dobrego kodu powrotu (ponieważ dowolna zwracana wartość całkowita może być poprawna).
Zgłaszanie nowego wyjątku
Jednym z oczywistych rozwiązań jest zgłoszenie nowego wyjątku.
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // zgłasza wyjątek int w przypadku niepowodzenia
}
catch (int exception)
{
// Zapisz błąd w jakimś globalnym pliku dziennika
g_log.logError("getIntValueFromDatabase failed");
// Wrzuć wyjątek char „q” w górę stosu, który ma zostać obsłużony przez obiekt wywołujący
throw 'q';
}
}W powyższym przykładzie program przechwytuje wyjątek int z metody getIntValue(), rejestruje błąd, a następnie zgłasza nowy wyjątek z wartością znaku „q”. Chociaż zgłaszanie wyjątku z bloku catch może wydawać się dziwne, jest to dozwolone. Pamiętaj, że do przechwycenia kwalifikują się tylko wyjątki zgłoszone w bloku try. Oznacza to, że wyjątek zgłoszony w bloku catch nie zostanie przechwycony przez blok catch, w którym się znajduje. Zamiast tego zostanie propagowany w górę stosu do osoby wywołującej.
Wyjątek zgłoszony z bloku catch może być wyjątkiem dowolnego typu — nie musi być tego samego typu, co właśnie przechwycony wyjątek.
Ponowne zgłoszenie wyjątku (w niewłaściwy sposób)
Inną opcją jest ponowne zgłoszenie ten sam wyjątek. Można to zrobić w następujący sposób:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // zgłasza wyjątek int w przypadku niepowodzenia
}
catch (int exception)
{
// Zapisz błąd w jakimś globalnym pliku dziennika
g_log.logError("getIntValueFromDatabase failed");
throw exception;
}
}Chociaż to działa, metoda ta ma kilka wad. Po pierwsze, nie powoduje to wygenerowania dokładnie tego samego wyjątku, który został przechwycony — raczej generuje kopię zmiennego wyjątku zainicjalizowaną przez kopiowanie. Chociaż kompilator może pominąć kopię, może tego nie robić, więc może to być mniej wydajne.
Ale co istotne, rozważ, co stanie się w następującym przypadku:
int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
assert(d);
try
{
return d->getIntValue(table, key); // zgłasza wyjątek pochodny w przypadku niepowodzenia
}
catch (Base& exception)
{
// Zapisz błąd w jakimś globalnym pliku dziennika
g_log.logError("getIntValueFromDatabase failed");
throw exception; // Niebezpieczeństwo: powoduje to wyrzucenie obiektu podstawowego, a nie obiektu pochodnego
}
}W tym przypadku getIntValue() zgłasza obiekt Derived, ale blok catch przechwytuje referencję Base. Nie ma w tym nic złego, ponieważ wiemy, że możemy mieć referencję bazową do obiektu pochodnego. Jednakże, gdy zgłaszamy wyjątek, zgłoszony wyjątek jest inicjowany przez kopiowanie z wyjątku zmiennej. Wyjątek zmiennej ma typ Base, więc wyjątek zainicjowany przez kopiowanie ma również typ Base (nie pochodny!). Innymi słowy, nasz obiekt pochodny został pocięty!
Możesz to zobaczyć w następującym programie:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
void print() override { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived{};
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
throw b; // obiekt pochodny pozostałości tutaj
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
}
return 0;
}Wypisuje:
Caught Base b, which is actually a Derived Caught Base b, which is actually a Base
Fakt, że druga linia wskazuje, że Base jest w rzeczywistości bazą, a nie pochodnym, dowodzi, że obiekt pochodny został pocięty.
Ponowne zgłoszenie wyjątku (we właściwy sposób)
Na szczęście C++ umożliwia ponowne zgłoszenie dokładnie tego samego wyjątku, co właśnie przechwycony. Aby to zrobić, po prostu użyj słowa kluczowego rzut z bloku catch (bez powiązanej zmiennej), na przykład:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() { std::cout << "Base"; }
};
class Derived: public Base
{
public:
Derived() {}
void print() override { std::cout << "Derived"; }
};
int main()
{
try
{
try
{
throw Derived{};
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
throw; // uwaga: teraz ponownie wrzucamy obiekt tutaj
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
}
return 0;
}Wypisuje:
Caught Base b, which is actually a Derived Caught Base b, which is actually a Derived
To słowo kluczowe rzut, które nie wydaje się rzucać niczego konkretnego, w rzeczywistości ponownie zgłasza dokładnie ten sam wyjątek, który właśnie został przechwycony. Nie są tworzone żadne kopie, co oznacza, że nie musimy się martwić, że kopie obniżą wydajność lub zostaną pocięte.
Jeśli wymagane jest ponowne zgłoszenie wyjątku, należy preferować tę metodę w porównaniu z alternatywami.
Reguła
Podczas ponownego zgłaszania tego samego wyjątku użyj samego słowa kluczowego rzut

