blog.michalt.pl

Króciutko i teoretycznie o kontraktach w C++ 20

28.05.2019 21:37 - Tech

Tak, niestety, znowu tylko teoretycznie. Niedawno ściągnąłem i skompilowałem nowy snapshot GCC (z 19 maja) i progresu brak. W związku z tym postanowiłem rozłożyć nieco w czasie publikację artykułów na temat dwudziestego standardu języka C++. Z pewnością na deser zostawię najbardziej złożone nowości. Dziś postanowiłem przyjrzeć się kolejnej z nich, mam na myśli kontrakty. Na chwilę obecną nie da się skompilować przedstawionych tu przykładów na kompilatorach GCC i Clang.

Aktualizacja [21.07.2019]: kontrakty nie pojawią się w dwudziestym standardzie języka C++.


Najpierw wypadałoby napomknąć nieco, czym jest programowanie kontraktowe. Programowanie kontraktowe ma na celu sprawdzanie, czy funkcje lub metody przyjmują, przetwarzają lub zwracają dane o oczekiwanych wartościach. Ponadto można je znaleźć w innych językach programowania, m.in. w D, Cloujure i Eiffel.


Jak to wygląda w języku C++?


Zacznijmy od koncepcji składniowej:
[[rodzaj poziom identyfikator : wyrażenie]] – gdzie poziom i zmienna są opcjonalne.


Istnieją 3 rodzaje kontraktów:

  • expects – sprawdza wartość parametrów wejściowych przed wykonaniem funkcji,
  • assert – sprawdza wartość wewnątrz funkcji,
  • ensures – sprawdza wartość po wykonaniu funkcji.

Mamy również różne poziomy kontraktów:

  • default – jest wartością podstawianą domyślnie i zakłada, że koszt (czas wykonania) sprawdzania kontraktu powinien być niewielki,
  • audit – zakłada, że koszt (czas wykonania) kontraktu może być duży,
  • axiom – kontrakt nie jest wykonywany w trakcie uruchamiania programu.

W ramach przykładu postanowiłem napisać prostą funkcję, która przyjmuje małą literkę i zwraca dużą. Operujemy jedynie na literach z podstawowej puli znaków ASCII.

char upper_acii_char(char c) 
  [[expects  default : c>=97 && c<=122 ]]  
  [[ensures  audit : result>=65 && result<=90]] {
    c += 32;
    [[assert : c>= 0 && c < 128]];
    return c;
}


Jak możemy zauważyć, w naszym kodzie mamy do czynienia z trzema kontraktami. Pierwszy z nich weryfikuje, czy wprowadzony przez nas znak jest małą literą. Drugi sprawdza, czy zwracany przez funkcję znak jest wielką literą. Trzeci sprawdza, czy znak mieści się w puli podstawowej ASCII. Warto również zwrócić uwagę na poziomy. W drugim kontrakcie poziom default pojawił się dlatego, że tu sprawdzane są jedynie parametry wejściowe, przez co czas weryfikacji jest krótki. W drugim przypadku, aby zweryfikować kontrakt, musimy wykonać całą funkcję. Warto także zwrócić uwagę na identyfikator – w tym wypadku jest to taka jakby pomocnicza zmienna, która przechowuje wynik zwracany przez funkcję.


Oczywiście, tu nie kończą się możliwości programowania kontraktowego w C++ 20. Wariantów kontraktowania może być dużo więcej. Na przykład we wszystkich przypadkach możemy weryfikować zawartość kontenerów (nawet po wykonaniu funkcji).


Oto jeszcze jeden ciekawy przykład, który chciałbym przytoczyć:

class Square {
private:
  unsigned size;
public:
  Square(unsigned argSize) : size(argSize) {};
  
  void operator=(unsigned argSize) [[ expects : argSize != size]] { //błąd
    size = argSize;
  }
};

W tym przypadku próbujemy sprawdzić, czy rozmiar boku naszego kwadrata będzie inny od wcześniej ustalonego. Postanawiamy porównać parametr z atrybutem klasy. Okazuje się, że niestety otrzymamy błąd. Przyczyną takiego stanu rzeczy jest fakt, iż atrybut size jest prywatny. Prawdopodobnie kontrakt zadziałałby w przypadku atrybutu publicznego.


Warto wspomnieć, że kontraktów nie można przeciążać razem z metodami!


Co się stanie, gdy kontrakt nie zostanie spełniony? To prawdopodobnie będzie zależeć od trybu kompilacji, tj. off – kontrakty nie będą wykonywane, default – kontrakty o poziomie default zostaną wykonane, audit – wszystkie kontrakty zostaną wykonane. Niespełniony kontrakt może zostać przechwycony lub może zakończyć wykonywanie programu.


Tu moja wiedza na ten temat się kończy. Widzę w tym pewien potencjał dla testerów. A Wy?