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); // assume this throws an int exception on failure
return d;
}
catch (int exception)
{
// Database creation failed
delete d;
// Write an error to some global logfile
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); // throws int exception on failure
}
catch (int exception)
{
// Write an error to some global logfile
g_log.logError("getIntValueFromDatabase failed");
// However, we haven't actually handled this error
// So what do we do here?
}
}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); // throws int exception on failure
}
catch (int exception)
{
// Write an error to some global logfile
g_log.logError("getIntValueFromDatabase failed");
// Throw char exception 'q' up the stack to be handled by caller
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); // throws int exception on failure
}
catch (int exception)
{
// Write an error to some global logfile
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); // throws Derived exception on failure
}
catch (Base& exception)
{
// Write an error to some global logfile
g_log.logError("getIntValueFromDatabase failed");
throw exception; // Danger: this throws a Base object, not a Derived object
}
}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; // the Derived object gets sliced here
}
}
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; // note: We're now rethrowing the object here
}
}
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

