C++: mity i fakty

| Technika

W latach 90. rozpowszechniła się w kręgach zainteresowanych tematyką opinia, że język C++ nie nadaje się do tego, żeby pisać w nim programy dla mikrokontrolerów jednoukładowych. W przypadku małych systemów 8- i 16-bitowych, dominujących w tamtym okresie, jest to zapewne prawda. Ale dziś, w dobie tanich mikroprocesorów 32-bitowych o wielkiej (w porównaniu do tych z poprzednich dekad) mocy przetwarzania i znacznych zasobach, można sobie zadać pytanie, na ile ta niezbyt pochlebna dla C++ opinia ma nadal pokrycie w faktach, a na ile jest utrzymującym się siłą inercji mitem.

C++: mity i fakty

Kwestia pamięci ROM

Linkery przeznaczone do systemów jednoukładowych pozwalają na umieszczanie stałych w pamięci ROM. W języku C wszystkie dane, co do których jest pewność, że są stałe, można w związku z tym zainicjować podczas kompilacji, co pozwala na umieszczenie ich w ROM-ie.

W C++ można zrobić to samo, ale niezbyt to pasuje do obiektowego stylu programowania. W dobrze napisanym programie w C++ większość danych jest zawarta w obiektach, obiekty należą do klas, a klasy mają konstruktory. Obiekt może być określony jako const, a obiekt określony jako const static i mający konstruktor musi zostać umieszczony w pamięci RAM, gdyż w przeciwnym wypadku konstruktor nie będzie go mógł zainicjować.

A zatem, gdzie w języku C obiekt const static znajdzie się w ROM-ie, który jest tani i jest go dużo, tam w C++ jego najbardziej naturalny odpowiednik musi rezydować w drogim i niezbyt obszernym RAM-ie. Inicjowanie wykonuje kod znajdujący się w pamięci ROM i wywołujący po kolei wszystkie konstruktory z parametrami odpowiednimi dla poszczególnych deklaracji. Ten kod zajmuje więcej miejsca niż mogłyby go zająć dane statycznie inicjujące dane obiekty (jak to jest w C).

Jeśli chcemy dane umieścić w pamięci ROM, trzeba poświęcić nieco uwagi konstrukcji klas tak, żeby odpowiadające im obiekty mogły się tam znaleźć. Przede wszystkim, obiekt musi być statycznie zainicjowany, tak jak struktura w języku C. Mimo że łatwo jest to osiągnąć przez zadeklarowanie go jako struktury (bez metod), da się jednak i tu użyć nieco bardziej obiektowych metod programowania.

Statyczne inicjowanie obiektów jest dozwolone pod następującymi warunkami:

  • klasa nie może być klasą pochodną,
  • w klasie nie może być konstruktora,
  • w klasie nie może być metod wirtualnych,
  • klasa nie może zawierać elementów oznaczonych jako private lub protected,
  • wszelkie klasy w niej zawarte muszą spełniać te same kryteria,
  • wszystkie metody muszą zostać zadeklarowane jako const.

Mimo że to rozwiązuje bezpośredni problem umieszczenia obiektu w pamięci ROM i rozszerza zwykłą strukturę C o metody, dalekie jest jednak od ideału programowania obiektowego, który stanowi klasa łatwa do prawidłowego, a trudna do nieprawidłowego użycia. Nieuważny programista może, na przykład, zadeklarować niezainicjowaną i nieconst instancję tej klasy albo uzyskać dostęp do jej struktur wewnętrznych z pominięciem zdefiniowanych metod, a jej projektant nie jest w stanie temu zapobiec.

Rozwiązanie tego problemu wymaga czegoś więcej. Tym czymś jest zagnieżdżanie klas: w C++ można deklarować jedne klasy wewnątrz drugich. Można zatem taką klasę, zdefiniowaną jak powyżej (gdzie, jak powiedziano, zachodzi ryzyko nieprawidłowego użycia) umieścić w prywatnej części innej klasy. A instancje const static tej klasy można uczynić elementami private static klasy-opakowania. Ta zewnętrzna klasa nie podlega restrykcjom nałożonym na to, co chcemy ulokować w ROM-ie, możemy więc sobie w niej zdefiniować interfejs dostępu do naszych danych statycznych.

Nie ma tu miejsca na przykładowe listingi - są nieco zbyt obszerne - poprzestaniemy więc na ogólnym wskazaniu, że w danym wypadku potrzebna jest bardzo przemyślana konstrukcja klas. A z drugiej strony, jest prawdą, że nieprzemyślane użycie tego czy tamtego w C++ może prowadzić do niespodziewanego i niepożądanego rozrostu kodu wynikowego, ale tak samo jest w języku C. Rozważmy przykład takiej, wyglądającej zupełnie niewinnie, deklaracji:

int var = 1.0;

Wartość po prawej stronie jest liczbą zmiennoprzecinkową. Co za tym idzie, kompilator co prawda nie musi, ale może wstawić do kodu odwołanie do biblioteki obsługującej obliczenia zmiennoprzecinkowe w celu dokonania konwersji typu. Jeszcze gorszy skutek może pociągnąć za sobą nieuważne skorzystanie w programie z funkcji printf() - nie każdy się tego spodziewa, ale jest ona dość rozbudowana. Ale takie pomyłki raczej nie skłonią nikogo do twierdzenia, że język C nie nadaje się do oprogramowywania systemów jednoukładowych. Podobnie sprawa wygląda z nieprzemyślanym użyciem pewnych konstrukcji C++.

Biblioteki

Jedną z korzyści, jakie przynosi stosowanie C++, jest możliwość użycia bibliotek klas. Język zorientowany obiektowo sprawia, że biblioteki klas są łatwiejsze w użyciu niż ich odpowiedniki strukturalne (w języku C), trudniej jest także ich nadużywać. Ale tak samo jak w przypadku bibliotek języka C niektóre z nich powodują znaczny rozrost programu, a to nawet mimo (o czym też już wspominano powyżej) że współczesne linkery potrafią wyłuskać z bibliotek i dołączyć do programu tylko te metody, którymi program rzeczywiście się posługuje.

W każdym wypadku przed podjęciem decyzji o użyciu konkretnej biblioteki, należy zbadać, jaki to będzie miało wpływ na wielkość gotowej aplikacji. Dotyczy to zresztą nie tylko bardzo wymyślnych, specjalistycznych bibliotek - na początek można zbadać, jakiej wielkości w danym środowisku będzie jakiś trywialny program w rodzaju "Hello world": może się on okazać niespodziewanie duży w porównaniu z odpowiednikiem w C.

Nowsze definicje języka C++, tj. C++11 i C++14, wprowadziły do niego parę udogodnień, które, jak te powyżej opisane, ograniczają się częściowo tylko do składni i tym samym nie mają wpływu na docelową wielkość i wydajność kodu, inne natomiast jak najbardziej, zwłaszcza jeśli dojdzie do ich lekkomyślnego nadużycia.

Nie ma tu miejsca na ich szczegółowe omawianie. W ramach ogólnej konkluzji można stwierdzić, że negatywne opinie na temat przydatności C++ dla systemów jednoukładowych są mocno przesadzone: do złych skutków prowadzi nie tyle samo w sobie użycie tego języka, ile nieuważne korzystanie z jego konstrukcji i uleganie przez programistów nawykom, których nabrali podczas pracy na większych komputerach. Pisząc program w C++, zwłaszcza nigdy nie należy zapominać o tym, że maszyna docelowa dysponuje ograniczonymi zasobami, a w razie wątpliwości trzeba zawsze sprawdzać, jak użycie danej konstrukcji C++ przekłada się na wielkość i wydajność kodu wynikowego.

Konrad Kokoszkiewicz