10.1 Konwersje standardowe

Rozważmy niewinnie wyglądający fragment programu:

       double x = 1.5, y;
       int    k =  10;
           // ...
       y = x + k;
Jaka funkcja wykonująca dodawanie będzie wywołana, aby wykonać polecenie ' x+k' z ostatniej linii? Zmienna k jest typu int, zmienna x zaś typu double. Mają one zatem zupełnie inną strukturę w pamięci komputera. Aby je prawidłowo dodać, należy oczywiście tę strukturę uwzględnić i zdecydować, jaki z kolei ma być typ wyniku. Czy istnieje zatem jakaś funkcja realizująca takie dodawanie? A gdyby k była typu unsigned short? Dodawanie liczby zapisanej w postaci double do liczby zapisanej w postaci unsigned short jest oczywiście zupełnie czym innym niż dodawanie double'a do int'a. Musiałaby zatem istnieć ogromna liczba funkcji realizujących to samo zadanie, ale inaczej, w zależności od typów argumentów. To samo dotyczy wywołań funkcji. Dostępna po dołączeniu pliku nagłówkowego cmath (albo math.h) funkcja sin ma jeden parametr typu double (lub long). Czy oznacza to, że wywołanie sin(1) jest błędem, czy może istnieje inna wersja tej funkcji z parametrem int? Tymi właśnie problemami teraz się zajmiemy.

Załóżmy, że w programie występuje wyrażenie z operatorem dwuargumentowym (dodawanie, mnożenie, ...). Przed wykonaniem operacji dokonywane są na wartościach argumentów konwersje standardowe, których celem jest:

Cel pierwszy wynika z faktu, że funkcje realizujące działanie operatorów są zdefiniowane tylko dla argumentów takiego samego typu, przy czym nie są zdefiniowane dla wszystkich możliwych typów. Ważne jest też, jakiego typu jest wtedy wynik.

Typem wyniku arytmetycznej operacji dwuargumentowej jest wspólny typ argumentów po dokonaniu konwersji.

Wyjaśnijmy przebieg poszukiwania tego wspólnego dla wartości obu argumentów typu. Zauważmy, że konwersje są, jeśli to tylko możliwe, promocjami, to znaczy typ „węższy” jest awansowany do typu „szerszego” tak, żeby nie utracić dokładności. W tym sensie np. typ int jest „węższy” od double, bo każda wartość całkowita może być zapisania w formie double bez utraty dokładności, ale nie odwrotnie. Z drugiej strony, nie dla wszystkich pokrewnych typów można zdefiniować taką relację zawierania: zbiory wartości typu int i typu unsigned int są tak samo liczne (232 elementów), ale żaden nie zawiera się w drugim. Wrócimy do tego problemu w dalszym ciągu.

Zatem:

  1. Jeśli jeden z argumentów jest long double, to drugi jest też przekształcany do typu long double (oczywiście jeśli już nie był tego typu).
  2. W przeciwnym przypadku: jeśli jeden jest typu double, to drugi też jest przekształcany do typu double.
  3. W przeciwnym przypadku: jeśli jeden jest typu float, to drugi też jest przekształcany do typu float.
  4. W przeciwnym przypadku oba argumenty są typów całościowych i są poddawane promocji całościowej zgodnie z następującą procedurą:
  5. Następnie: Jeśli jeden z argumentów jest typu unsigned long, to drugi też jest przekształcany do typu unsigned long.
  6. W przeciwnym razie: jeśli jeden z argumentów jest typu long a drugi typu unsigned int i jeśli wartości typu unsigned int można w danej implementacji reprezentować w typie long (co zwykle nie zachodzi na „zwykłych” maszynach 32-bitowych), to wartość typu unsigned int jest przkształcana do typu long; w przeciwnym razie oba argumenty są przekształcane do typu unsigned long (co może spowodować nieszczęście...).
  7. W przeciwnym razie, jeśli jeden z argumentów jest typu long, to drugi jest też przekształcany do typu long.
  8. W przeciwnym razie, jeśli jeden z argumentów jest typu unsigned int, to drugi jest też przekształcany do typu unsigned int.
  9. W przeciwnym razie oba argumenty są typu int i żadna konwersja nie jest potrzebna.

Na przykład, zgodnie z tymi zasadami, wyrażenie w trzeciej linii fragmentu

       int i = 1, j = 2, k = 3, m;
       // ...
       m = (j > i) + (k > j);
jest prawidłowe: wartości typu bool, jakimi są wartości wyrażeń (j > i)(k > j), zostaną niejawnie przekształcone do wartości całkowitych (jedynek, bo obie relacje są w naszym przykładzie prawdziwe). Zatem operator dodawania „zobaczy” po obu stronach dwie jedynki i zmienna m uzyska wartość 2.

Często popełniany jest błąd przy dzieleniu: pamiętajmy, że dzielenie liczb całkowitych zawsze, zgodnie z powyższymi zasadami, daje wynik całkowity, a więc części ułamkowej nie ma. Początkującym często wydaje się, że jeśli przypisują wynik do zmiennej typu double, to w jakiś tajemniczy sposób część ułamkowa zostanie „odzyskana”: tak nie będzie, jej tam po prostu nie ma, nie została policzona! Tak więc, jeśli aktualną wartością zmiennej k typu int jest 7, to wartością wyrażenia ' k/2' jest dokładnie 3. Jeśli chodziło nam raczej o  3$ {\frac{{1}}{{2}}}$, to wystarczy dopisać kropkę przy dwójce: wartością ' k/2.' jest 3.5, bo '2.' (z kropką) jest interpretowane jako literał wartości typu double, a zatem i wartość k zostanie przekształcona do typu double, a co za tym idzie i wynik będzie również tego właśnie typu.

Do standardowych niejawnych konwersji, które mogą być wykonane bez naszej wiedzy (i, niestety, zgody...), należy też:

Brzmi to skomplikowanie, bo też, niestety, jest to skomplikowane. W dodatku jest też niebezpieczne: w C/C++ dozwolone są niejawne konwersje, na skutek których traci się informację (zwykle kompilatory wyświetlają wtedy jakieś ostrzeżenia).

Pamiętać przy tym należy, że niejawne konwersje wcale nie gwarantują, że otrzymana po przekształceniu wartość jest z naszego punktu widzenia sensowna, to znaczy zgodna w oczekiwanym przez nas sensie z wartością przed przekształceniem. Na przykład rozpatrzmy program:


P60: surp.cpp     Niejawne konwersje

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main()
      5.  {
      6.      int      k   = -2;
      7.      unsigned uns =  1;
      8.  
      9.      int      x = k + uns;
     10.      unsigned y = k + uns;
     11.  
     12.      cout << "x   = " << x      << endl;
     13.      cout << "y   = " << y      << endl;
     14.      cout << "y+1 = " << y + 1  << endl;
     15.  
     16.  
     17.      signed   char c = 255;
     18.      unsigned char d = 255;
     19.  
     20.      cout << "c+1 = " << c + 1  << endl;
     21.      cout << "d+1 = " << d + 1  << endl;
     22.      d = d + 1;
     23.      cout << "d   = " << (int)d << endl;
     24.  }

Program ten drukuje:

    x   = -1
    y   = 4294967295
    y+1 = 0
    c+1 = 0
    d+1 = 256
    d   = 0
Wartość zmiennej x to zgodnie z oczekiwaniem -1. Ale drukowanie wartości zmiennej y daje 4294967295 (nie jest to taka sobie przypadkowa liczba; jej wartość to 232 - 1). Po dodaniu do tej liczby jedynki otrzymujemy dokładnie zero (linia 14 programu drukuje 'y + 1 = 0'). Wydawałoby się, że zmienne cd mają te same wartości, więc i po dodaniu jedynki otrzymamy to samo. Ale linia 20 drukuje 'c + 1 = 0', natomiast linia 21 drukuje 'd + 1 = 256'. Jednak gdy wartość ' d + 1' przypiszemy znów do zmiennej d i wydrukujemy jej wartość jako int (linia 23), dostaniemy 0. Jak widać, szczególnie łatwo jest pogubić się przy konwersjach od typów ze znakiem do typów bez znaku i odwrotnie — związane jest to z faktem, o którym wspominaliśmy, że zbiory wartości takich typów nie pozostają do siebie w relacji zawierania: nie można powiedzieć, który jest „węższy”, a który „szerszy”.

Najczęściej jednak nie musimy aż tak dokładnie analizować tego typu wyrażeń, jeśli trzymamy się podstawowych typów i staramy się pisać kod w sposób czytelny i prosty. Trzeba tylko pamiętać, że typy całkowite węższe od int zawsze promowane są co najmniej do typu int. Dotyczy to w szczególności wartości znakowych (typu char). Użyte jako argument operatorów lub argument w wywołaniu funkcji są przekształcane do wartości całkowitej typu int równej kodowi ASCII danego znaku. Na przykład po przypisaniu

       int k = 3 + 'a';
wartość k wynosi 100, bo kod ASCII małej litery 'a' jest 97.

Można z tej własności skorzystać do napisania prostej funkcji odczytującej ciąg znaków aż do napotkania nie-cyfry i interpretującej ten napis jako dodatnią liczbę całowitą:


P61: konw.cpp     Konwersje znak liczba

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int konwert(char* nap) {
      5.      int w = 0, i = 0, c;
      6.      while (c = nap[i++], c >= '0' && c <= '9')
      7.          w = 10*w + c - '0';
      8.      return w;
      9.  }
     10.  
     11.  int main() {
     12.      char tab1[] = "123a";
     13.      char tab2[] = "456 1";
     14.      char tab3[] = " 56";
     15.  
     16.      cout << "tab1 -> " << konwert(tab1) << endl;
     17.      cout << "tab2 -> " << konwert(tab2) << endl;
     18.      cout << "tab3 -> " << konwert(tab3) << endl;
     19.  }

Wynikiem jest

    tab1 -> 123
    tab2 -> 456
    tab3 -> 0
Funkcja konwert pobiera jako argument tablicę znaków w postaci wskaźnika do pierwszego jej elementu. Następnie, w pętli while przetwarzane są kolejne znaki tego napisu. Wewnątrz nawiasów okrągłych, gdzie jest miejsce na wyrażenie logiczne sterujące pętlą, mamy tu wyrażenie przecinkowe. Wyrażenie będące lewym argumentem wczytuje kolejny znak napisu do zmiennej c, po czym zwiększa aktualną wartość indeksu. Wartością całego wyrażenia przecinkowego jest wartość prawego argumentu, a tu mamy sprawdzenie, czy wczytany znak jest cyfrą. Co jest bowiem np. wartością wyrażenia c >= '0'? Przed dokonaniem porównania obie strony tej nierówności są przkształcane do typu int. Zatem wartością zmiennej znakowej c będzie kod ASCII wczytanego znaku. Podobnie wartością '0' (z apostrofami!) będzie po konwersji do typu int kod ASCII znaku '0' — jest to liczba 48, ale wcale nie musimy o tym wiedzieć. Podkreślmy jeszcze raz, bo jest to źródłem wielu błędów: wartością liczbową zmiennej znakowej '0' jest kod ASCII znaku zero (czyli 48), a nie liczba zero. Literałem znaku o kodzie ASCII równym zero jest ' \0', łącznie z apostrofami i odwrotnym ukośnikiem.

Kody ASCII kolejnych cyfr, 0, ..., 9, są kolejnymi liczbami całkowitymi (zeru odpowiada 48, jedynce — 49 itd., ale na szczęście nie musimy tych kodów pamiętać). Tak więc warunek c >= '0' && c <= '9' sprawdza, czy kod ASCII znaku c jest jednocześnie większy lub równy od kodu znaku '0' i mniejszy lub równy od kodu znaku '9', czyli czy jest to znak odpowiadający cyfrze. Jeśli nie, pętla zostanie przerwana.

Powtórnie wykorzystujemy ten mechanizm w następnej linii: wyrażenie c-'0' ma wartość liczbową równą liczbie reprezentowanej przez znak c. Jeśli np.  c jest znakiem '4', to '4'-'0' jest tym samym co 52 - 48, czyli 4 (tym razem liczbowo cztery). Zatem, jeśli kolejny wczytany znak jest cyfrą, to dotychczasowa wartość zmiennej w jest mnożona przez dziesięć, co odpowiada przesunięciu cyfr o jedną pozycję w lewo, i dodawana jest na pozycji jedności wartość liczbowa odpowiadająca wczytanej cyfrze. Pętla kończy się, gdy kolejnym znakiem nie jest cyfra. Dlatego tab2 zostanie zinterpretowana jako 456; wczytywanie zostanie zakończone, gdy napotkany zostanie odstęp pomiędzy cyfrą 6 a 1. Dla tab3 otrzymamy 0, bo pętla nie wykona ani jednego obrotu: jako pierwszy znak zostanie bowiem wczytany odstęp, a więc nie-cyfra.

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