16.6 Obsługa błędów strumieni

Prócz flagi stanu formatowania, każdy strumień posiada słowo stanu zawierające informacje o stanie i o ewentualnych błędach związanych z operacjami we/wy na tym strumieniu. Tak jak flaga stanu formatowania, słowo stanu strumienia ma postać całkowitoliczbowej zmiennej (typu iostate).

Programista ma możliwość badania aktualnego stanu strumienia za pomocą metod wywoływanych na rzecz obiektów-strumieni.

Można też badać stan strumienia bezpośrednio w warunkach logicznych instrukcji if, for, while itd. W takich przypadkach wartość strumienia zostanie przekonwertowana automatycznie dzięki przeciążeniu w klasie ios operatora '!' oraz operatora konwersji do typu void*.

W wyrażeniach typu

       if ( strm ) ...
wartość strm zostanie przekonwertowana do wartości wskaźnika pustego NULL (odpowiadającego false), jeśli stan strumienia jest „zły”, to znaczy, jeśli metoda fail wywołana na rzecz tego strumienia dałaby true (patrz dalej). W przeciwnym przypadku, czyli gdy stan strumienia jest „dobry”, zwrócona zostanie wartość niezerowa, odpowiadająca true. Podobnie w wyrażeniu
       if ( !strm ) ...
operator '!' zastosowany do strumienia zwróci wartość logiczną true wtedy i tylko wtedy, gdy metoda fail zwróciłaby true.

Słowo stanu zapewnia nam jednak więcej informacji. Tak jak flagę stanu formatowania można składać (lub rozkładać na czynniki pierwsze...) za pomocą nazwanych stałych, tak i słowo stanu strumienia można przez „ORowanie” składać z predefiniowanych stałych typu iostate.

ios::badbit —  Strumień jest zniszczony; nie wiadomo, czy ostatnia operacja na nim powiodła się. Następna na pewno nie powiedzie się.

ios::eofbit —  Wykonano próbę dostępu do danych poza końcem strumienia (pliku).

ios::failbit —  Następna operacja nie powiedzie się, ale strumień nie jest zniszczony, w szczególności nie zgubiono żadnych znaków.

ios::goodbit —  Strumień jest „zdrowy”. Wartością goodbit jest 0.

Stan strumienia można badać za pomocą funkcji składowych (metod) klasy ios

zwracających w postaci wartości logicznej ustawienie bitów ios::badbit, ios::eofbit, ios::failbitios::goodbit w słowie stanu strumienia.

Są też metody pozwalające manipulować słowem stanu strumienia „ręcznie” — nie tylko odczytywać go, ale i modyfikować.

iostate rdstate() —  Zwraca słowo stanu w postaci zmiennej typu iostate.

void clear(iostate stan = ios::goodbit) —  Zeruje słowo stanu, co odpowiada ustawieniu stanu na good. Następnie ustawia znacznik stan, który musi być jedną ze stałych ios::badbit, ios::failbit itd. Domyślną wartością jest ios::goodbit, a zatem wywołanie bez argumentu „naprawia” strumień — patrz przykład w programie plikrw.cpp.

void setstate(iostate stan) —  Dodaje (przez „ORowanie”) znacznik stan do słowa stanu. Argument musi być jedną ze stałych ios::badbit, ios::failbit itd. Równoważne wywołaniu ' clear( rdstate() | ios::stan )'.

Generalnie, stan strumienia należy w poważnych programach sprawdzać po każdej operacji. Jeśli stan strumienia jest „zły”, a więc funkcja fail, bad lub eof zwraca true, to każda następna operacja we/wy na tym strumieniu jest ignorowana, natomiast nie jest zgłaszany żaden błąd! Rozpatrzmy przykład:


P127: validan.cpp     Sprawdzanie poprawności wczytywanych danych

      1.  #include <fstream>
      2.  #include <iostream>
      3.  using namespace std;
      4.  
      5.  int main() {
      6.      const int DIM = 80;
      7.      char      name[DIM];
      8.      ifstream  inplik;
      9.      double    x;
     10.  
     11.      do {
     12.          cout << "Plik wejsciowy: ";
     13.          cin.getline(name, DIM);
     14.  
     15.          inplik.clear();
     16.          inplik.open(name);
     17.      } while (!inplik);
     18.  
     19.      cout << "Plik = " << name << endl;
     20.      inplik.close();
     21.  
     22.      do {
     23.          if (!cin) {
     24.              // wazna kolejnosc!
     25.              cin.clear();
     26.              cin.ignore(1024,'\n');
     27.          };
     28.          cout << "Podaj liczbe: ";
     29.          cin >> x;
     30.      } while (!cin);
     31.  
     32.      cout << "Liczba = " << x << endl;
     33.  }

W linii 13 wczytujemy nazwę pliku. Następnie, w linii 16, próbujemy go otworzyć do czytania. Jeśli plik o takiej nazwie nie istnieje, strumień inplik będzie w stanie bad, zatem warunek pętli do...while będzie spełniony; program przejdzie do następnego jej obrotu i jeszcze raz zapyta o nazwę. Stan strumienia w dalszym ciągu będzie bad, więc w linii 15 naprawiamy go przed podjęciem kolejnej próby. Ważne jest to, że pętlę opuścimy tylko wtedy, gdy plik został prawidłowo otwarty i strumień inplik jest w stanie good.

Bardziej subtelna jest pętla w liniach 22-30, której zadaniem jest wczytanie liczby. W linii 29 usiłujemy wczytać liczbę na zmienną x. Jeśli się to nie powiedzie, bo na przykład użytkownik wpisał zamiast liczby litery, to stan strumienia cin będzie bad. Zatoczymy zatem pętlę i podejmiemy kolejną próbę. Ale ponieważ stan strumienia jest zły, operacje wejścia na tym strumieniu będą ignorowane! Zatem musimy naprawić stan, co czynimy w linii 25. To jeszcze nie wszystko. Ponieważ bufor strumienia nie został „wyczytany”, gdybyśmy ograniczyli się do naprawy strumienia, to następna operacja będzie czytać nie wprowadzoną, być może tym razem prawidłowo, liczbę, ale „śmieci” pozostałe w buforze z poprzedniej, nieudanej próby. I tak w nieskończoność będziemy czytać te same śmieci! Zatem musimy je usunąć przez wywołanie ignore w linii 26. Teraz program działa prawidłowo (zakładamy, że istnieje w aktualnym katalogu plik val.dat):

    Plik wejsciowy: val.txt
    Plik wejsciowy: val
    Plik wejsciowy: val.dat
    Plik = val.dat
    Podaj liczbe: s12
    Podaj liczbe: @
    Podaj liczbe: 12
    Liczba = 12
Zauważmy też, że ważna jest kolejność naprawiania strumienia i wywoływania ignore z linii 25 i 26. Gdybyśmy wywołali ignore przed naprawieniem strumienia, to wywołanie to zostałoby zignorowane, bo przecież jest to operacja wejścia/wyjścia, a stan strumienia jest bad. A więc „śmieci” pozostałyby w strumieniu i znów wpadlibyśmy w nieskończoną pętlę, nie mogąc pozbyć się błędnych danych. Po odwróceniu kolejności linii 25 i 26 otrzymalibyśmy zatem:
    Plik wejsciowy: val.dat
    Plik = val.dat
    Podaj liczbe: s12
    Podaj liczbe: Podaj liczbe: Podaj liczbe: Podaj li
    czbe: Podaj liczbe: Podaj liczbe:
gdzie wykonanie musieliśmy przerwać wciskając Ctrl-C.


Niestety, prawidłowa i pełna obsługa wszystkich możliwych błędów operacji wejścia i wyjścia to zadanie bardzo trudne i pracochłonne. Kod z tym związany może stanowić znaczną część (czasem większość!) całego programu.

T.R. Werner, 25 lutego 2017; 22:31