Wyjątki a if'y podczas obsługi błędów

0

Co powoduje, że lepszym rozwiązaniem jest stosowanie bloków try i catch zamiast if'ów i zwracania konkretnego kodu błędu?

Exceptiony:

#include <iostream>
#include <exception>

int main(int argc, char *argv[])
{
	try
	{
		if (argc != 2)
			throw std::out_of_range("Too few arguments.");
		std::cout << argv[1] << '\n';
	}
	catch (const std::out_of_range& e)
	{
		std::cerr << "Exception: " << e.what() << '\n';
		return -1;
	}
	return 0;
}

If'y:

#include <iostream>

int main(int argc, char *argv[])
{
	if (argc != 2) 
	{
		std:cerr << "Too few arguments." << '\n';
		return -1;
	}
	std::cout << argv[1] << '\n';
	return 0;
}

Z tego co do tej pory czytałem to:

  1. Wyjątki nie mogą być ignorowane. Po rzuceniu wyjątku nawet jeżeli nie został złapany, a następnie obsłużony, proces i tak zostanie zabity.
  2. RAII działa, czyli istnieje gwarancja, że wszystkie zasoby zaalokowane na stosie zostaną automatycznie zwolnione po wystąpieniu wyjątku.

Z tego co mi się wydaję to ten drugi punkt może powodować przewagę systemu wyjątków nad if'ami z C. Czy jest coś jeszcze co powinienem wziąć pod uwagę?

8

Wyjątki stosuje się kiedy:

  • Masz sytuacje wyjątkową :) Sprawdzanie czy ktoś podał 2 argumenty to nie jest sytuacja wyjątkowa!
  • Chcesz wyrzucić sterowanie bardzo mocno w górę stosu - jesteś gdzieś bardzo głęboko ale dostałeś błąd który sprawia że cały aktualny kontekst przestaje mieć sens i chcesz wrócić gdzieś na sam początek i pokazać userowi błąd

Używanie wyjątków do control-flow to bezsens i większość języków jednak od tego odchodzi, przechodząc na stosowanie Optional czy Either.

Jakieś OutOfMemory to dobry kandydat na rzucenie wyjątku, ale argc!=2 nie.

3

Wydaje mi się, że nie rozumiesz do czego są wyjątki i jak one działają. Twój przykład jest bardzo prymitywny i jedyne co udowadnia to wyższość ifów - mniej kodu i szybsze działanie.
Twoje wnioski są nie precyzyjne - odnoszą się do ogółu, czy do twojego przykładu?

0

Tak myślałem, że w moim przykładzie if'y byłyby lepszym rozwiązaniem, ale if'ami zrobiłbym to po prostu intuicyjnie, a niekoniecznie byłby to wynik wiedzy i rozumienia.

Rozumiem na przykład, że "błąd" typu Network service unavailable jest wyjątkiem. OutOfMemory również postawiłbym, że jest to wyjątek, ale czy na przykład File not found to wyjątek? Ciężko mi te wyjątki jednoznacznie zdefiniować.

7

Żeby pokazać przykład:

Załóżmy że piszesz sobie jakiś parser/kompilator.
User ładuje plik i następnie odpalasz tworzenie AST na podstawie tego pliku. I jesteś gdzieś w połowie, schodzisz super głęboko w tym AST budując drzewo jakiegoś skomplikowanego wyrażenia logicznego i nagle dostajesz jakiś std::bad_alloc bo pamięć się skończyła. I co teraz robisz?
Nie bardzo ma sens pakowanie do tego AST wszędzie jakiegoś Either, bo tam "tak normalnie" nic sie nie może popsuć. Co więcej, taki Either nic by ci nie dał, bo nie da sie tego "uratować" w żaden sposób. Mógłbyś go przepychać w górę przez N poziomów co najwyżej, a to bez sensu. W takiej sytuacji dużo lepiej rzucić wyjątek i wrócić ze sterowaniem do miejsca w którym user wybrał plik do parsowania.

Z drugiej strony, jeśli user podał ścieżkę do pliku który nie istnieje, to to jest bląd, ale "taki normalny", raczej częsty i do przewidzenia. W takiej sytuacji dużo sensowniej sprawdzić czy plik istnieje, a nie pchać tą ścieżkę gdzieś głębiej i czekać aż sie coś niżej wywali.

Zastanów się:

  • Czy można tą sytuacje "uratować"?
  • Jak daleko od miejsca w którym jesteś chcesz "obsłużyć" ten błąd?
0

To już wiele wyjaśnia.
Przy File not found sytuacja jest relatywnie częsta i można odpytać użytkownika o plik jeszcze raz więc rzucanie wyjątku jest tu bez sensu raczej bo... to nie wyjątek. Ale jak program wywala się przy std::bad_alloc i kończy się pamięć to właściwie tej sytuacji "uratować" się zbytnio nie da, i jest to "wyjątkowa" sytuacja więc warto rzucić wyjątek wychodząc z procesu. Myślę, że już powoli ogarniam, ale jeśli nadal coś przekręcam to proszę o skorygowanie. :)

2

wychodząc z procesu

Niekoniecznie musisz ubić proces do końca. To już zależy od rodzaju błędu. Ale generalnie chcesz wrócić ze sterowaniem gdzieś bardzo daleko, bo tam gdzie wystąpił błąd nie da się niczego "naprawić".

3

Nawet jeśli to jest sytuacja wyjątkowa, trzeba się zastanowić czy wyjątek jest rzeczywiście potrzebny. To musi być sytuacja wyjątkowa której nie można zignorować. Przesada w rzucaniu wyjątkami utrudnia potem używanie takich klas.

3

Czyli hipotetycznie trzeba każdy push_back vectora chować w bloku try/catch? - leto wczoraj, 23:31

A potrafisz naprawić, zareagować? Sądzę, ze nie.
Nie pojawiało się jeszcze w tym wątku, ale druga zasada: łapać wyjątki tam, gdzie da się jakoś racjonalnie zareagować.
Nie naprawisz pojedynczego push'a, a ubijesz raczej algorytm (metodę) wyższego rzędu, ergo wywołanie tejże metody w try..catch
(na marginesie wniosek: w studenckich programach nie pisać wszystkiego w main)

5

Można powiedzieć, że ifami i kodami błędów najlepiej robić wszystko to, co jest przewidziane w algorytmie programu. Brak uprawnień do pliku, brak samego pliku itp. jest do przewidzenia. To, że system w trakcie działania programu zgłasza problemy, można uznać za sytuację wyjątkową. Np. mam uchwyt do folderu i pobrałem listę plików. W momencie dokonywania operacji na tych plikach system może stwierdzić, że uchwyt jest nieważny. Można obsłużyć to wyjątkiem, żeby nie sprawdzać kodu błędu dla każdego pliku, bo wiadomo, że to nie plik jest problemem. Poza tym czasem dziwnie byłoby sprawdzać cały kontekst przed każdym plikiem.

EDIT: Podaję przykład wedle sugestii @Miang
Przykład jest banalny, ale chodzi tylko o pokazanie, że nie wszystko można "przewidzieć" algorytmem. Czasem nawet system może popsuć nam zabawę z nieznanych nam przyczyn.

public static void processFile(String path) throws IOException {
		File file = new File(path);

		//Tu możemy zareagować
		if (!file.exists()) {
			return;
		}

		FileInputStream fileInputStream = new FileInputStream(file);

		//Tu też możemy zareagować
		if (fileInputStream.available() == 0) {
			return;
		}

		byte[] buffer = new byte[fileInputStream.available()];
		/*
		* Teraz albo - co gorsza - podczas działania metody read system może zabrać nam uchwyt do pliku.
		* TU NIE MOŻEMY ZAREAGOWAĆ - TU WYJĄTEK SIĘ PRZYDA
		 */
		int bytesRead = fileInputStream.read(buffer);
		System.out.println("Odczytane bajty " + bytesRead);
		fileInputStream.close();
		//robimy z danymi coś albo coś innego...
	}

1 użytkowników online, w tym zalogowanych: 0, gości: 1