Koduję małą bibliotekę i mam problemy z zaprojektowaniem obsługi wyjątków. Muszę powiedzieć, że jestem (nadal) zdezorientowany tą funkcją języka C ++ i starałem się przeczytać jak najwięcej na ten temat, aby zrozumieć, co powinienem zrobić, aby poprawnie pracować z klasami wyjątków.
Zdecydowałem się zastosować takie system_error
podejście, które czerpie inspirację z wdrożenia future_error
klasy STL .
Mam wyliczenie zawierające kody błędów:
enum class my_errc : int
{
error_x = 100,
error_z = 101,
error_y = 102
};
oraz pojedyncza klasa wyjątków (wspierana przez error_category
rodzaj struktur i wszystko inne, czego potrzebuje system_error
model):
// error category implementation
class my_error_category_impl : public std::error_category
{
const char* name () const noexcept override
{
return "my_lib";
}
std::string message (int ec) const override
{
std::string msg;
switch (my_errc(ec))
{
case my_errc::error_x:
msg = "Failed 1.";
break;
case my_errc::error_z:
msg = "Failed 2.";
break;
case my_errc::error_y:
msg = "Failed 3.";
break;
default:
msg = "unknown.";
}
return msg;
}
std::error_condition default_error_condition (int ec) const noexcept override
{
return std::error_condition(ec, *this);
}
};
// unique instance of the error category
struct my_category
{
static const std::error_category& instance () noexcept
{
static my_error_category_impl category;
return category;
}
};
// overload for error code creation
inline std::error_code make_error_code (my_errc ec) noexcept
{
return std::error_code(static_cast<int>(ec), my_category::instance());
}
// overload for error condition creation
inline std::error_condition make_error_condition (my_errc ec) noexcept
{
return std::error_condition(static_cast<int>(ec), my_category::instance());
}
/**
* Exception type thrown by the lib.
*/
class my_error : public virtual std::runtime_error
{
public:
explicit my_error (my_errc ec) noexcept :
std::runtime_error("my_namespace ")
, internal_code(make_error_code(ec))
{ }
const char* what () const noexcept override
{
return internal_code.message().c_str();
}
std::error_code code () const noexcept
{
return internal_code;
}
private:
std::error_code internal_code;
};
// specialization for error code enumerations
// must be done in the std namespace
namespace std
{
template <>
struct is_error_code_enum<my_errc> : public true_type { };
}
Mam tylko niewielką liczbę sytuacji, w których zgłaszam wyjątki zilustrowane przez wyliczenie kodu błędu.
Powyższe nie pasowało do jednego z moich recenzentów. Był zdania, że powinienem był stworzyć hierarchię klas wyjątków z klasą pochodną, std::runtime_error
ponieważ osadzenie kodu błędu w tym warunku łączy różne rzeczy - wyjątki i kody błędów - i byłoby bardziej żmudne zajmowanie się kwestią obsługi; hierarchia wyjątków pozwoliłaby również na łatwą personalizację komunikatu o błędzie.
Jednym z moich argumentów było to, że chciałem uprościć sprawę, że moja biblioteka nie musiała zgłaszać wielu rodzajów wyjątków i że dostosowanie jest również łatwe w tym przypadku, ponieważ jest obsługiwane automatycznie - error_code
ma error_category
powiązaną z nią translację kod do właściwego komunikatu o błędzie.
Muszę powiedzieć, że nie broniłem dobrze mojego wyboru, co świadczy o tym, że nadal mam pewne nieporozumienia dotyczące wyjątków w C ++.
Chciałbym wiedzieć, czy mój projekt ma sens. Jakie byłyby zalety tej drugiej metody w porównaniu z tą, którą wybrałem, ponieważ muszę przyznać, że również tego nie widzę? Co mogę zrobić, aby poprawić?
źródło
Odpowiedzi:
Myślę, że twój kolega miał rację: projektujesz przypadki wyjątków w oparciu o to, jak proste jest wdrożenie w hierarchii, a nie w oparciu o potrzeby obsługi wyjątków kodu klienta.
Z jednym typem wyjątku i wyliczeniem warunku błędu (twoje rozwiązanie), jeśli kod klienta musi obsługiwać przypadki pojedynczego błędu (na przykład
my_errc::error_x
), musi napisać kod w następujący sposób:Przy wielu typach wyjątków (mających wspólną podstawę dla całej hierarchii) możesz napisać:
gdzie klasy wyjątków wyglądają tak:
Podczas pisania biblioteki należy skupić się na łatwości użytkowania, a nie (koniecznie) łatwości wewnętrznej implementacji.
Powinieneś ograniczać łatwość użycia (jak będzie wyglądał kod klienta) tylko wtedy, gdy wysiłek zrobienia go bezpośrednio w bibliotece jest zaporowy.
źródło
Zgadzam się z twoimi recenzentami i @utnapistim. Możesz zastosować
system_error
podejście, gdy wdrażasz rzeczy między platformami, gdy niektóre błędy wymagają specjalnej obsługi. Ale nawet w tym przypadku nie jest to dobre rozwiązanie, ale mniej złe rozwiązanie.Jeszcze jedna rzecz. Podczas tworzenia hierarchii wyjątków nie rób jej zbyt głębokiej. Utwórz tylko te klasy wyjątków, które mogą być przetwarzane przez klientów. W większości przypadków używam tylko
std::runtime_error
istd::logic_error
. Rzucam,std::runtime_error
gdy coś idzie nie tak i nie mogę nic zrobić (użytkownik wysuwa urządzenie z komputera, zapomniał, że aplikacja nadal działa), astd::logic_error
gdy logika programu jest zepsuta (użytkownik próbuje usunąć rekord z bazy danych, który nie istnieje, ale przed usunięciem operacji on może to sprawdzić, więc dostanie błąd logiczny).A jako twórca bibliotek, pomyśl o potrzebach użytkowników. Spróbuj użyć go sam i zastanów się, czy to dla ciebie komfort. Następnie możesz wyjaśnić swoje stanowisko recenzentom za pomocą przykładów kodu.
źródło