2015-04-28 27 views
8

Mam zamiar rozpocząć ten nowy projekt w C++ i zastanawiam się nad niebolesnym sposobem obsługi błędów. Teraz nie będę rozpoczynać rzucania i łapania wyjątków, i prawdopodobnie nigdy nie będę generował wyjątków, ale myślałem - nawet dla regularnej obsługi błędów, po co rzucać własną/kopiować i wklejać klasę do opisywania błędów/status, kiedy mogłem po prostu użyć std::exception i jego klas potomnych (lub może std::optional<std::exception>)?Czy mogę/powinienem używać std :: exception's do regularnej obsługi błędów?

using Status = std::optional<std::exception>; 
Status somethingThatMayFail(int x); 

Czy ktoś/każdy projekt działa w ten sposób? Czy to niedorzeczny pomysł, czy tylko trochę skrzypiący?

+0

@vsoftco: Zobacz edycję. Mówię o obsłudze błędów wyrzucania wyjątków. – einpoklum

+0

Interesujące ... Ostatnio usłyszałem, że Google wydaje się używać takiego systemu, nie wiem, czy to prawda, czy nie. Co jeśli twoje funkcje muszą coś zwrócić? Użyjesz pary/krotki? – vsoftco

+0

@vsoftco: Nie zagłębiamy się w szczegóły ... chodzi tylko o to, czego użyć do zgłaszania błędów. Ale - albo funkcja będzie miała parametr wejścia/wyjścia (referencja lub wskaźnik), albo może zwróci pewną klasę przypadków (jestem pewien, że C++ ma już coś takiego, to lepsze niż związki C). – einpoklum

Odpowiedz

2

Myślę, że sama wydajność może okazać się problematyczna. Rozważmy następujący kod:

#include <iostream> 
#include <chrono> 
#include <ctime> 
#include <stdexcept> 

#include <boost/optional.hpp> 


int return_code_foo(int i) 
{ 
    if(i < 10) 
     return -1; 
    return 0; 
} 


std::logic_error return_exception_foo(int i) 
{ 
    if(i < 10) 
     return std::logic_error("error"); 
    return std::logic_error(""); 
} 


boost::optional<std::logic_error> return_optional_foo(int i) 
{ 
    if(i < 10) 
     return boost::optional<std::logic_error>(std::logic_error("error")); 
    return boost::optional<std::logic_error>(); 
} 


void exception_foo(int i) 
{ 
    if(i < 10) 
     throw std::logic_error("error"); 
} 


int main() 
{ 
    std::chrono::time_point<std::chrono::system_clock> start, end; 

    start = std::chrono::system_clock::now(); 
    for(size_t i = 11; i < 9999999; ++i) 
     return_code_foo(i); 
    end = std::chrono::system_clock::now(); 
    std::cout << "code elapsed time: " << (end - start).count() << "s\n"; 

    start = std::chrono::system_clock::now(); 
    for(size_t i = 11; i < 9999999; ++i) 
     return_exception_foo(i); 
    end = std::chrono::system_clock::now(); 
    std::cout << "exception elapsed time: " << (end - start).count() << "s\n"; 

    start = std::chrono::system_clock::now(); 
    for(size_t i = 11; i < 9999999; ++i) 
     return_optional_foo(i); 
    end = std::chrono::system_clock::now(); 
    std::cout << "optional elapsed time: " << (end - start).count() << "s\n"; 

    start = std::chrono::system_clock::now(); 
    for(size_t i = 11; i < 9999999; ++i) 
     exception_foo(i); 
    end = std::chrono::system_clock::now(); 
    std::cout << "exception elapsed time: " << (end - start).count() << "s\n"; 

    return 0; 
} 

na mojej CentOS, używając gcc 4.7, to planowane na:

[[email protected] tmp]$ ./a.out 
code elapsed time: 39893s 
exception elapsed time: 466762s 
optional elapsed time: 215282s 
exception elapsed time: 38436s 

w ustawieniach wanilii oraz:

[[email protected] tmp]$ ./a.out 
code elapsed time: 0s 
exception elapsed time: 238985s 
optional elapsed time: 33595s 
exception elapsed time: 24350 

na -O2 ustawień.

P.S. Osobiście użyłbym wyjątków/rozwijania stosów ze względu na przekonanie, że jest to podstawowa część C +, być może tak, jak wspomniano powyżej @vsoftco.

+0

Cóż, tak, punkt wzięty, ale mierzyłeś czas, kiedy błąd rzeczywiście występuje, a nie kiedy nie. Ponadto, jeśli mój kod obsługi błędów, za pomocą numerycznego kodu statusu, miałby gdzieś przeciągnąć ciąg lub zapisać go w strumieniu, to zrównoważyłoby to czasy, które bym pomyślał. – einpoklum

+0

@einpoklum Właściwie nie zgadzam się - jeśli zaznaczysz kod, "wyjątek" nigdy nie wystąpił w tym teście. To jest właśnie problem z tym podejściem - w normalnym przypadku wydaje się to niezwykle kosztowne. Odnosi się to również do twojego punktu widzenia na faktyczne obchodzenie się z błędami - zdarza się to znacznie rzadziej. (Jeśli myślisz o tym, yeshpomashehu :-)) –

3

Nie sądzę, że powinieneś konstruować wyjątki, chyba że faktycznie zamierzasz je wyrzucić. Polecam typ zwrotu bool lub enum. Intencja będzie znacznie wyraźniejsza dla kogoś czytającego twój kod, a oni będą szybsi. Jeśli jednak skonstruujesz wyjątek, ktoś inny pojawi się i pomyśli, że może wyrzucić wyjątek i spowodować awarię całego systemu.

Wyjątki C++ odgrywają ważną rolę w zarządzaniu zasobami, wyzwalaniu destruktorów i tym wszystkim (RAII). Używanie ich w jakikolwiek inny sposób może zaszkodzić wydajności i (co ważniejsze) pomylić świętego potępienia z każdym, kto próbuje zachować kod, później.

Możesz jednak robić, co chcesz, z klasą zgłaszania statusu, która NIE ma nic wspólnego ze std :: exception. Ludzie robią zbyt wiele, aby "szybszy" kod, kiedy nie muszą. Jeśli wyliczenie statusu nie jest wystarczająco dobre i konieczne jest zwrócenie większej ilości informacji, klasa raportująca status będzie działać. Jeśli ułatwia czytanie kodu, idź do niego.

Po prostu nie nazywam tego wyjątkiem, chyba że faktycznie go wyrzucisz.

+0

To. Nawet jeśli "opcjonalne " jest idealne dla przypadku użycia, używanie go poza kontekstem rzut/catch będzie mylące. W miarę możliwości należy unikać zamieszania. –

0

Aby odpowiedzieć na twoje pytanie, nie jest to absurdalny pomysł i można faktycznie używać std::exception do regularnej obsługi błędów; z pewnymi zastrzeżeniami.

Korzystanie std::exception jako wynik funkcji

Załóżmy, że funkcja może wyjść w kilku stanach błędu:

std::exception f(int i) 
{ 
    if (i > 10) 
    return std::out_of_range("Index is out of range"); 

    if (can_do_f()) 
    return unexpected_operation("Can't do f in the current state"); 

    return do_the_job(); 
} 

Jak można sobie z tym poradzić z std::exception lub opcjonalnego jednego? Gdy funkcja zwróci, zostanie utworzona kopia wyjątku, zachowując tylko część std::exception i odrzucając szczegóły rzeczywistego błędu; pozostawiając ci jedyną informację, że "tak, coś poszło nie tak ...". Zachowanie będzie takie samo, jak zwracanie wartości boolowskiej lub opcjonalnego oczekiwanego typu wyniku, jeśli taki istnieje.

Korzystanie std::exception_ptr zapisać specyfikę

Innym rozwiązaniem byłoby zastosowanie tego samego podejścia niż w std::promise, to zwróci std::exception_ptr. Tam będziesz mógł zwrócić albo nic albo wyjątek, zachowując rzeczywiste szczegóły błędu. Przywrócenie faktycznego typu błędu może nadal być trudne.

Wracając błąd lub wynik w tym samym obiekcie

Wreszcie inna opcja byłoby użyć Expected<T>proposal i its implementation. Tam będziesz mógł zwrócić wartość lub błąd w jednym obiekcie i obsłużyć błąd, który chcesz (testując błąd lub przy zwykłej obsłudze wyjątku), z pewnymi szczegółami dla przypadku funkcji zwracającej brak wartości (więcej na Stack Overflow lub na this blog).

Jak wybrać

Moja osobista opinia w tej sprawie jest to, że jeśli masz zamiar używać wyjątków, a następnie wykorzystywać je tak, zostały one zaprojektowane, ostatecznie z kilku dodatkowych narzędzi jak Expected<T> aby ułatwić . W przeciwnym razie, jeśli nie możesz korzystać ze standardowej obsługi wyjątków, przejdź do rozwiązania, które się sprawdziło, podobnie jak klasyczny system kodów błędów.