Posts tagged ‘C/C++’

Nieoczekiwana zmiana składni

2008-09-25 8:54

Jedną ze szczególnych cech jest C++ jest obecność różnych “nadmiarowych” właściwości, które pojawiły się w języku właściwie przypadkiem. Bardziej znanym przykładem jest chociażby metaprogramowanie za pomocą szablonów, czego pierwszym przykładem jest słynny kod generujący liczby pierwsze na wyjściu kompilatora. Z początku było to nic innego, jak niezamierzony skutek uboczny procesu rozwijania szablonów, acz teraz zostało to oczywiście świadomie rozwinięte.
Podobnie jest na przykład z preprocesorem, jako że C i C++ są jednymi z niewielu języków, które taki mechanizm posiadają. Na początku służył głównie do deklarowania “stałych”, potem także makr naśladujących funkcje… A przecież można go też użyć do niemal całkowitej zmiany składni języka – tak, by zaczął on udawać inny. Przykładowo, za pomocą kilku poniższych #define‘ów:
#include
#include
#define IF if(
#define THEN )
#define BEGIN {
#define END }
#define WRITELN std::cout<< #define READLN(x) std::getline(std::cin,x)[/cpp] możemy sprawić, że w C++ będzie pisało się prawie jak w Pascalu: [cpp]WRITELN (“Podaj hasło:”); READLN (pass); IF pass == “strasznietajnehaslo” THEN BEGIN WRITELN (“OK”); END;[/cpp] Nie wszystkie niuanse składni innego języka da się oczywiście zasymulować przy pomocy makr, ale w praktyce udaje się bardzo wiele. Mimo to polecam jednak programowanie w C++ przy pomocy C++, a nie Pascala czy Basica ;)

Tags: , ,
Author: Xion, posted under Programming » 9 comments

Metody wirtualne i override

2008-08-29 15:43

W wielu językach obiektowych występuje słowo kluczowe override, którego dodawanie jest obowiązkowe podczas nadpisywania metod wirtualnych w klasach pochodnych (w przeciwnym wypadku generowanie jest ostrzeżenie). Jak można się spodziewać, C++ do takich języków nie należy :) A szkoda, bo wymóg stosowania override zapobiega pomyłkom przy nadpisywaniu (np. literówkom w nazwach metod), które można wykryć dopiero w trakcie działania programu, gdy ze zdziwieniem odkrywamy, że zamiast nowej wersji metody wywoływana jest stara.

Można częściowo temu zaradzić, ale sposób jest brzydki. Polega on na stworzeniu makra zawierającego całą definicję klasy (albo przynajmniej deklaracje jej metod wirtualnych):

  1. #define DECLARE_FOO(__suffix__) \
  2.     public: \
  3.         virtual void AbstractDoSomething() __suffix__; \
  4.         virtual void DoSomethingElse();

Następnie używamy go zarówno w definicji klasy bazowej, jak i pochodnych:

  1. class Foo
  2. {
  3.     DECLARE_FOO(=0)
  4. };
  5.  
  6. class Bar : public Foo
  7. {
  8.     DECLARE_FOO( )
  9. };

Dodatkowy parametr pozwala odróżnić obie sytuacje, co jest koniecznie w przypadku metod czysto wirtualnych (które nie posiadają implementacji).

Sztuczka jest być może pomysłowa, ale jednocześnie dość przygnębiająca. Dlaczego bowiem tak prosta sprawa jak zapobieganie pomyłkom przy nadpisywaniu metod wirtualnych musi być w C++ realizowana za pomocą brzydkich makr?…

Tags: , ,
Author: Xion, posted under Programming » 9 comments

Czy void jest typem

2008-08-05 17:21

Jak wiadomo, w C i C++ nie ma procedur. Są tylko funkcje, czyli podprogramy obliczające i zwracające jakąś wartość. Jednak możliwe jest zadeklarowanie takiej funkcji, która w istocie nic nie zwraca; służy do tego słowo kluczowe void, rzecz jasna. Pełni ono wówczas rolę nazwy typu zwracanego przez funkcję:

  1. void Procedure { /* ... */ }

Ktoś mniej doświadczony (lub o mniej schematycznym podejściu do C++) mógłby wobec tego spytać: Czy void jest wobec tego rzeczywiście typem, takim jak chociażby int czy char, czy jednak jest to tylko dziwne słowo na oznaczenie procedur w C++?

Odpowiedzią na powyższe pytanie może być tylko stanowcze… i tak, i nie :)
Z jednej strony wiemy na przykład, że void może występować w nagłówkach funkcji, i to nie tylko w miejscu typu wartości zwracanej, ale też na liście parametrów. Jednakże w tym drugim przypadku oznacza to jedynie to, iż funkcja tak naprawdę nie przyjmuje żadnych argumentów. W C++ można spokojnie pominąć to użycie void, osiągając dokładnie ten sam efekt – funkcji bezparametrowej. (Warto aczkolwiek zaznaczyć, że w C jest inaczej).
Inną rolą void sugerującą jego “typowość” jest istnienie wskaźników typu void*. Do tego typu można niejawnie skonwertować dowolny wskaźnik:

  1. void* p = new CFoo;

Operacja odwrotna nie jest jednak możliwa (wymaga co najmniej static_cast), a sam void* nie jest tak naprawdę pełnoprawnym wskaźnikiem – nie pozwala bowiem na dereferencję. Ponadto nie istnieje też typ referencyjny void&.
Wreszcie, chyba najbardziej egzotycznym użyciem void jest rzutowanie na niego wyrażenia innego typu. Jest to jak najbardziej możliwe i dotyczy zwykle sytuacji podobnych do tej:

  1. // Pokaż okno komunikatu tylko z przyciskiem OK
  2. static_cast<void>(MessageBox(NULL, _TEXT("Błąd!"), NULL, MB_OK | MB_ICONERROR));

Używamy tutaj funkcji, która zwraca rezultat (int), ale on nas nie interesuje. Pokazujemy przecież komunikat z jednym przyciskiem OK (więc nie musimy pobierać decyzji użytkownika), a ponadto nie ma tutaj żadnej wyobrażalnej możliwości wystąpienia błędu, o którym mógłby nas poinformować rezultat funkcji. Niektóre kompilatory z włączonym maksymalnym poziomem ostrzeżeń mogą aczkolwiek ostrzegać o ignorowaniu potencjalnie przydatnego rezultatu funkcji; rzutowanie na void pozwala takich ostrzeżeń uniknąć. Jednak sam fakt, iż takie rzutowanie jest możliwe, nie implikuje od razu możliwości zadeklarowania zmiennej typu void. To w jest rzecz jasna niemożliwe.

Cóż więc z tym fantem zrobić? No cóż, sprawa jest raczej śliska. Zapewne dałoby się jeszcze znaleźć przynajmniej po kilka argumentów zarówno za tym, że void typem jest, jak i że nie jest. Ale od rozstrzygnięcia ważniejsze jest raczej to, aby z tego… słowa kluczowego (pozostańsmy przy neutralnej wersji) umieć korzystać. Co, swoją drogą, nie jest takie trudne ;)

Tags: ,
Author: Xion, posted under Programming » Comments Off on Czy void jest typem

Prawie jak mapa

2008-08-04 17:12

Każda porządna biblioteka pojemników posiada kontener w typie mapy lub słownika, który służy do przechowywania par klucz-wartość, czyli odwzorowywania jednych na drugie. Zwykle pojemnik taki jest zaimplementowany przy pomocy odpowiednio zarządzanej struktury drzewiastej. I tak np. w C++ mamy od tego klasę std::map, w .NET – System.Collections.Generic.Dictionary, a w Javie cały zestaw klas implementujących interfejs java.util.Map.

Czasami jednak korzystanie z tego typu rozwiązań może być strzelaniem z armaty do komara. Jeśli bowiem:

  • nasze klucze nie są typami złożonymi (np. łańcuchami znaków), a raczej wartościami wyliczeniowymi
  • nie jest ich zbyt dużo, zwłaszcza w stosunku do ilości pamięci, którą możemy zająć
  • przypisane kluczom wartości nie są dużymi obiektami
  • istnieją dla nich rozsądne ustawienia domyślne

to możemy zastosować o wiele prostsze rozwiązanie. Polega ono na użyciu typu wyliczeniowego dla kluczy:

  1. enum KEYS { KEY_0, KEY_1, /*... */ KEY_COUNT };

oraz zwykłej tablicy do przechowywania wartości (tutaj typu bool):

  1. bool dictionary[KEY_COUNT];

Proste i skuteczne, a i nieco wygodniejsze składniowo niż standardowe std::map. A także wybitnie mało odkrywcze; podejrzewam, że każdy średnio zaawansowany programista miał okazję zetknąć się z podobną “sztuczką”. Co jednak nie znaczy, że nie warto o niej czasem wspomnieć i zrobić dobry uczynek w postaci propagowania dobrych i sprawdzonych rozwiązań ;-)

Tags: , ,
Author: Xion, posted under Programming » 1 comment

Przeciążanie przecinka

2008-07-31 16:34

Przecinek to taki niepozorny znak, którego grubo ponad 90% zastosowań ogranicza się zapewne do oddzielania parametrów funkcji (czy to w deklaracji, czy to w wywołaniu). Niektóre języki używają też go do separowania indeksów wielowymiarowych tablic, ale do nich C++ akurat nie należy.
Zamiast tego przecinek jest tam operatorem, o czym rzadko się pamięta. Być może dlatego, że ma on najniższy priorytet spośród wszystkich operatorów. Wydaje się też, że domyślne jego działanie (obliczenie wszystkich argumentów, a następnie zwrócenie ostatniego) nie jest specjalnie przydatne – jeśli w ogóle.

Można je jednak zmienić. Tak, tak – operator przecinka można przeciążać, jakkolwiek zaskakujące i przekombinowane by się to wydawało. W rzeczywistości jest on bowiem zwyczajnym operatorem binarnym o łączności lewostronnej i wspomnianym najniższym priorytecie. A przeciążyć można go, pisząc funkcję operator,() – dokładnie tak samo, jak każdego innego dwuargumentowego operatora.
Do czego może to służyć? Otóż względnie typową sztuczką jest użycie przedefiniowanego przecinka do konstrukcji obiektów złożonych z wielu elementów, np. wektorów lub macierzy. Oto przykład przeciążenia dla standardowej klasy vector:

  1. #include <vector>
  2. template <typename T> std::vector<T> operator , (std::vector<T> v, const T& obj)
  3. {
  4.     v.push_back (obj);
  5.     return v;
  6. }

Pozwala to na inicjalizację wektora w taki oto nietypowy sposób:

  1. std::vector<int> ints = (std::vector<int>(), 1, 2, 3, 4, 5);

Pierwszy element tego ciągu (pusty wektor) jest konieczny, gdyż w przeciwnym razie użyty zostałby standardowy operator przecinka.

Na pierwszy rzut może to wyglądać efektownie. Pamiętajmy jednak, że przecinek ten pozostaje wciąż przecinkiem, zachowując chociażby swoją główną cechę szczególną, czyli priorytet. W szczególności próba dodania do wektora kolejnych elementów w ten sposób:

  1. ints = ints, 6, 7, 8, 9;

zakończy się niezupełnie po naszej myśli…
Mimo to być może warto spróbować wymyślić dla tego operatora jakieś własne, w miarę sensowne zastosowanie. Możliwe, że okaże się on wcale nie tak nieużyteczny, na jakiego zdaje się wyglądać :]

Tags: , ,
Author: Xion, posted under Programming » 4 comments

Jak działają metody wirtualne

2008-07-24 17:16

Zauważyłem, że niektóre osoby mają problem z właściwym wykorzystaniem metod wirtualnych i polimorfizmu, nie widząc czasami możliwości ich wykorzystania nawet w najbardziej oczywistych przypadkach. Być może powodem jest coś w rodzaju braku zaufania do całego mechanizmu, który jest spowodowany wrażeniem, że kompilator dokonuje przy jego okazji jakichś pseudomagicznych sztuczek.

A tak naprawdę z metodami wirtualnymi sprawa jest dosyć prosta. Pomiędzy nimi a zwykłymi metodami różnica polega wyłącznie na sposobie ich wywoływania.
W tym pierwszym przypadku użycie zwykłej metody względem obiektu:

  1. CObject* pObject = new CObject;
  2. // ...
  3. pObject->Method();

jest zamieniane w wynikowym kodzie na normalne wywołanie funkcji z dodatkowym parametrem:

  1. CObject_Method (pObject); // nazwa tej funkcji jest czysto umowna

Ten parametr jest potem dostępny jako wskaźnik this wewnątrz metody i pozwala odwoływać się do innych składowych obiektu.

W przypadku metod wirtualnych wywołanie jest natomiast dwuetapowe i polega na wykorzystaniu pewnych dodatkowych informacji. Są nimi: tablica funkcji wirtualnych (zwana vtable) oraz wskaźnik na nią (czyli vptr). Tablica występuje tylko w jednej kopii na całą klasę i zawiera tyle elementów, ile funkcji wirtualnych klasa ta posiada. Jej elementami są po prostu adresy w pamięci tych właśnie funkcji: pierwsza funkcja wirtualna ma więc adres zapisany w elemencie vtable o indeksie 0, druga – o indeksie 1, itd.


Tablica metod wirtualnych i wskaźnik na nią (Źródło)

Vptr jest natomiast daną trzymaną wraz z każdym obiektem, podobnie jak zwykłe pola tego obiektu (często vptr jest umieszczany w pamięci tuż po nich). Jest on niczym innym, jak wskaźnikiem na vtable i służy obiektowi, gdy ten chce wywołać którąś ze swoich metod wirtualnych. Dokładniej wygląda to mniej więcej tak:

  1. // (wywołanie pObject->Method(), jeśli Method() jest wirtualna)
  2. pMethod = pObject->_vptr[0]; // załóżmy, że metoda ta ma indeks 0
  3. (*pMethod)(pObject);

Widać więc, że polega to po prostu na pobraniu odpowiedniego adresu metody z vtable, kierując się indeksem ustalanym w czasie kompilacji, zwykle na podstawie kolejności deklaracji metod wirtualnych w bloku class.

Dlaczego jednak raz może być wywołana wersja metody z klasy bazowej, a raz z pochodnej?… Tego nietrudno się już chyba domyślić: wszystko zależy od tego, na jaką tablicę pokazuje vptr. Jego ustawienie następuje automatycznie w trakcie tworzenia obiektu; wtedy wiadomo oczywiście, jakiego on jest typu. Potem jednak może on być dostępny zarówno przez wskaźnik do swojej klasy, jak i do klasy bazowej. W obu przypadkach metody wirtualne będą działały poprawnie, gdyż w ich wywołaniu będzie pośredniczył vptr.

Tak to wszystko wygląda, w wielkim skrócie rzecz jasna :) Całość nie jest może trywialna ze względu na pewne kombinacje ze wskaźnikami do funkcji, jednak nie zaszkodzi mieć przynajmniej ogólne pojęcie o tym, jak to właściwie działa. Istnieje szansa, że dzięki temu będziemy potrafili korzystać nieco lepiej z metod wirtualnych, a przy okazji trochę więcej rozumieć z tajemniczych dyskusji “ekspertów C++” przerzucających się takimi tajemniczymi terminami jak vtable i vptr ;-]

Tags: ,
Author: Xion, posted under Programming » 5 comments

Wyliczenia kwalifikowane

2008-07-01 23:26

W C++ typy wyliczeniowe deklaruje się zwykle poprzez coś podobnego do poniższego kawałka kodu:

  1. enum Sides { Left = -1, Middle = 0, Right = 1 };

Jego skutkiem jest jednak to, że nazwy stałych typu (tutaj: Left, Middle i Right) będą widoczne w całej przestrzeni nazw zawierającej daną deklarację enum. Jeśli więc przypadkiem jest ona globalna, to całkiem łatwo może ona spowodować konflikt chociażby z innym typem w rodzaju:

  1. enum Keys { Left, Right, Up, Down, /* ... */ };

Aby zapobiegać takim sytuacjom, w Javie i C# stałe wyliczeniowe muszą być kwalifikowane nazwą odpowiedniego typu – używa się więc Sides.Left i Keys.Left. W C++ jest jednak inaczej, gdyż blok enum sam w sobie nie tworzy zasięgu (w przeciwieństwie np. do bloków class).

Można temu częściowo zaradzić w następujący sposób:

  1. struct Sides
  2. {
  3.     enum _Enum { Left = -1, Middle = 0, Right = 1 };
  4. };
  5. typedef Sides::_Enum Side;

dzięki czemu możemy z naszego enuma korzystać tak:

  1. Side foo;
  2. foo = Sides::Left;  // OK
  3. foo = 1; // błąd - nie można przypisać liczby
  4. foo = Right; // błąd - Right nie jest w przestrzeni globalnej

Różnica względem wspomnianych dwóch języków polega na tym, że nazwa typu wyliczeniowego (Side) oraz kwalifikator stałych (Sides) nie są takie same. Wydaje się jednak (przynajmniej mi się tak wydaje :]), że w tym przypadku takie rozróżnienie jest logicznie poprawne i wygląda nawet czytelniej niż gdyby obie nazwy były identyczne.
Trik ten można naturalnie opakować w makro, które umożliwi łatwe tworzenie typów wyliczeniowych z kwalifikowanymi nazwami stałych. Nie poprawi to oczywiście funkcjonalności enumów w C++, ale przynajmniej sprawi, że będą ładniej wyglądały :D

Tags: , ,
Author: Xion, posted under Programming » 10 comments
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.