Cel standardu.
Celem standardu jest wskazanie ogólnie przyjętych dobrych praktyk zmierzających do bardziej czytelnego i łatwiejszego w zarządzaniu kodu źródłowego, a tym samym do promocji tworzenia wysokiej jakości oprogramowania w sposób profesjonalny.
Zalecenia ogólne.
MYŚL!
Standard kodowania powstaje często, żeby pomóc, a nie po to żeby przeszkodzić. Także ten standard powstał z zamierzeniem bycia przydatnym. Wiadomo jednak, że nie każdy i nie zawsze przewidzi, co się w danej sytuacji najlepiej sprawdzi. Nie ma jednego najlepszego wyjścia które działa w każdych sytuacjach, ba, czasami w ogóle nie ma dobrego wyjścia z jakiejś sytuacji. Dlatego należy być świadomym, że ten standard opisuje praktyki które ZAZWYCZAJ prowadzą do lepszego kodu, natomiast nie zwalnia nikogo z myślenia - jeśli dojdziesz do wniosku, że znalazłeś się w sytuacji, w której zgodność ze standardem prowadzi do gorszego rozwiązania - złam ten standard.
Konwencje nazewnicze.
Nazwy klas wyjątków powinny mieć suffiks Exception.
Specjalne oznaczenie klas wyjątków zwiększa czytelność kodu. Poza tym takie oznaczanie klas wyjątków jest powszechną praktyką w językach obiektowych.
Przykłady: IOException, AccessException, WrongSecutiryLevelException.
Nazwy typów wyliczeniowych (enum) powinny przestrzegać tej samej konwencji nazewniczej co klasy, natomiast składniki tych typów powinny przestrzegać tej samej konwencji nazewniczej co stałe.
dziedzina: Konwencje nazewnicze.
Przykład:
enum AnimalType { BLACK_CAT = 3, WHITE_TIGER, BROWN_RABBIT };
Konwencje strukturalne i styl kodowania.
Każde celowo puste ciało pętli, funkcji etc. powinno być odpowiednio oznaczone.
Dzięki temu ktoś, kto będzie czytać kod i zobaczy np. pustą pętlę, nie będzie się zastanawiał, czy tak musi być, czy jest to przeoczenie programisty, czy też powinien to sam zaimplementować.
Przykład:
MyClass::MyClass (const MyClass& newMyClass) { // EMPTY BODY };
Alternatywnym sposobem używanym przez wielu programistów jest postawienie średnika w miejscu, które jest celowo pozostawione puste.
Przykład:
MyClass::MyClass (const MyClass& newMyClass) { ; };
Liczby zmiennoprzecinkowe nie powinny być porównywane za pomocą operatora ==.
Zamiast tego różnica dwóch liczb będzie porównywana pod względem większości/mniejszości z założoną tolerancją. Wynika to z błędów numerycznych, które wprowadzają operacje na liczbach zmiennoprzecinkowych.
double firstNumber = 0.5; double secondNumber = 0.4; secondNumber = secondNumber + 0.1; if(firstNumber == secondNumber) //To porównanie może zwrócić false!! //zamiast powyższego rozwiązania powinno się stosować poniższe: const double EPSILON = 0.00001; bool numbersEqualWithGivenTolerance = abs(firstNumber - secondNumber) < EPSILON; if(true == numbersEqualWithGivenTolerance) //ok
Zmienna będzie inicjalizowana w miejscu deklaracji, o ile istnieje dla niej rozsądna wartość początkowa.
Takie podejście pozwala oszczędzić kłopotów z niezainicjalizowanymi zmiennymi. Rozsądną wartością początkową jest również zero, tudzież makro NULL.
Każdy plik nagłówkowy powinien zawierać podstawową ochrone przed powtórnym załączeniem.
Powtórne załączenie może powodować błędy linkowania (typu "symbol xyz already defined in abc.obj").
Postać i konwencję nazweniczą mechanizmu ochrony przedstawia następujący przykład pliku MojaKlasa.h:
#ifndef __MOJA_KLASA_H__ #define __MOJA_KLASA_H__ //deklaracje #endif //__MOJA_KLASA_H__
Zmienne globalne nie powinny być definiowane.
Zmienne globalne naruszają zasadę kapsułkowania i projektową Zasadę Otwarte-Zamknięte. Poza tym są bardzo niebezpieczne z punktu widzenia wielowątkowości. Ta wskazówka nie dotyczy stałych globalnych.
Każda deklaracja funkcji, metody itp. powinna zawierać nie tylko typy, ale także nazwy parametrów.
Zapewnienie nazw parametrów ułatwia czytanie plików nagłówkowych, w których częściej szuka się informacji o funkcji niż pliki z implementacjami. Przykład:
//przykład deklaracji która nie stosuje się do reguły: string substr(size_t, size_t); //hmm, czyli jeden argument to indeks początku, a drugi - za-końca napisu? //przykład deklaracji, która stosuje się do reguły: string substr(size_t beginning, size_t amount); //aha! Drugi argument to jednak ilość znaków!
Unikaj "Magicznych liczb" - w miarę możliwości zastępuj je stałymi.
"Magiczne liczby" to liczby wpisane bezpośrednio w kod logiki aplikacji, np.
... currentTag = tagsArray[345]; //dlaczego 345 a nie np. 445? co to w ogóle znaczy? ... //123 linijka jednego z pozostałych 15 plików w naszym projekcie: newTag = tagsArray[345]; //jeśli miejsce elementu o który mi chodzi zmieni się, to będę musiał zmieniać //kod w dwóch miejscach... //A co jeśli zapomnę o tym drugim? Jest przecież tak daleko od pierwszego... //A jeśli miejsc wymagających zmiany będzie 20? Starczy mi chęci żeby je wszystkie znaleźć? ...
Taki styl kodowania czyni kod mniej odporny na zmiany (jeśli to co oznacza liczba się zmieni, to może się okazać konieczna zmiana nawet kilkunastu miejsc w kodzie), a także pogarsza czytelność.
Zamiast tego powinny być używane stałe:
... const unsigned int BOLD_TAG_INDEX; ... currentTag = tagsArray[BOLD_TAG_INDEX]; //aha, chodzi pewnie o jakiś znacznik pogrubienia tekstu! ...
Ta wskazówka odnosi się do liczba, które faktycznie oznaczają coś związanego z logiką aplikacji. "Magiczne liczby", które zawsze można stosować bez namysłu to np. 0, albo np. 1 w przypadku wskazywania ostatniego elementu w kolekcji (kolekcja[kolekcja.size() - 1]), albo będące integralną częścią dobrze znanego algorytmu (np. w wyszukiwaniu binarnym dzieląc przedział wyszukiwania 2 części).
Pętle i instrukcje warunkowe.
W porównaniu zmiennej ze stałą za pomocą operatora '==' stała powinna znajdować się po lewej strone porównania, a zmienna po prawej.
Ta praktyka zapobiega jednemu z najgłupszych i najdłużej wyszukiwanych błędów w kodzie, czyli napisanie '=' zamiast '=='. Spójrzmy na poniższy kod:
const int CONSTANT = 100; int variable = 123; if(CONSTANT == variable) ... //ok if(variable == CONSTANT) ... //ok, ale co się stanie, jeśli ktoś przez przypadek napisze tak: if(variable = CONSTANT) ... //przypisanie - zwróci wartość różną od zera. najczęściej nie o to nam chodzi. if(CONSTANT = variable) ... //kompilator tego nie przyjmie - nawet się nie skompiluje.
Pozwala to na wykrycie i precyzyjne namierzenie błędu już na etapie kompilacji.
Każda pętla lub intstrukcja warunkowa powinna dotyczyła bloku pomiędzy dwoma klamrami ({ i }).
Język C++ pozwala pominąć klamry w pętlach i instrukcjach warunkowych, jeśli dotyczą pojedynczej linijki zakończonej znakiem ';'. Mimo to zastosowanie nawet w tym przypadku klamr ma dwie zalety. Po pierwsze poprawia czytelność kodu, a po drugie sprawia, że na pewno nie zapomnimy o dostawieniu klamr, jeśli liczba instrukcji w pętli zwiększy się.
if(true == statement) { doSomething(); // 1: ok } if(true == statement) doSomething(); // 2: ok, ale co będzie jeśli dodamy więcej instrukcji? if(true == statement) { doSomething(); doSomethingElse(); } // w przypadku 1 musieliśmy dodać tylko instrukcję, // za to w przypadku 2 trzeba było pamiętać o dodaniu klamr. // Czy o 1 w nocy pamięta się o klamrach?
Każdy blok switch powinien zawierać etykietę default. Jeśli nie ma sensownej logiki do wynikania w tej etykiecie, to kod w niej zawarty będzie obsługiwał nieprzewidziane wartości sprawdzanego wyrazenia.
dziedzina: pętle i instrukcje warunkowe
Nie da się przewidzieć wszystkiego. Nawet, jeśli ktoś robi switch na zmienną wyliczeniową, która może przyjmować tylko kilka wartości, to przecież nikt nie powiedział, że nie trzeba będzie kiedyś tego typu rozszerzyć - wtedy taki default wychwyci ewentualne zapomnienia w uzupełnieniu instrukcji switch. Jeśli warunek default nie powinien się nigdy zdarzyć w danej konstrukcji switch, obsługa takiego nieprzewidzianego wypadku może polegać na wypisaniu w widocznym miejscu (ekran, plik logujący itp.) odpowiedniego komunikatu.
Każde porównanie powinno mieć charakter jawny.
Tego typu praktyka prowadzi do zwiększenia czytelności kodu. Dostarcza to więcej informacji o samej funkcji, o tym, zo naprawdę testujemy w danym warunku, a także jest bezpieczniejsze. Na przykład w poniższym kodzie:
if(getDeviceType()) //do czego służy ta funkcja? co tutaj oznacza 'deviceType'? { //instrukcje }
..widać tylko tyle, że zwrócona ma być niezerowa wartość. Dużo czytelniejsze jest następujące rozwiązanie, gdzie wartość, z którą porównujemy, jest wypisana jawnie:
if(CELLULAR_PHONE == getDeviceType()) //ok, jest jakaś stała i widać że dotyczy telefonu kom. { //instrukcje }
Jest kilka wyjątków od tej reguły, np. pętla nieskończona while(true) {}.
Elementy języka.
W C++ instrukcja goto nie powinna być używana.
W języku C instrukcja goto mogła być wykorzystywana do symulowania mechanizmu zbliżonego do wyjątków. W C++ nie ma takiej potrzeby, ponieważ wyjątki są częścią języka. Inne, bardzo rzadkie użycie goto polega na symulacji tranzycji w maszynach stanów.
W C++ natywnie wspierany jest mechanizm wyjątków, natomiast do maszyn stanów (poza sytuacjami wymagającymi bardzo dużej wydajności) można użyć wzorca projektowego State. Najważniejszy powód, by nie używać goto jest taki, że naruszają integralność kodu i są o wiele trudniejsze do debugowania.
Stałe powinny być definiowane za pomocą słowa kluczowego const zamiast #define.
Słowo kluczowe const wymaga jawnego sprecyzowania typu, co zapewnia silniejszą (lepszą) kontrolę typów. Zatem ten zapis:
#define SIZE_OF_STRUCT sizeof(double) + sizeof(int) + sizeof(char*)
można bez problemu zastąpić takim:
const unsigned int SIZE_OF_STRUCT = sizeof(double) + sizeof(int) + sizeof(char*);
Funkcje inline powinny być stosowane zamiast makr tam, gdzie się da.
Powód jest taki sam jak w przypadku sposobu definiowania stałych: funkcje inline zapewniają kontrolę typów, której nie mają makra. Zatem zamiast tego kodu:
#define max(a,b) (a > b) ? a : b
można napisać następujący:
template<class T> inline T maximum(T a, T b) { return (a > b) ? a : b; }
Jednak nie zawsze makro można zastąpić funkcję inline. Przykładem, kiedy nie jest to możliwe, są np. makra logujące wykorzystujące wbudowane makra __LINE__ i __FILE__ - wtedy trzeba zastosować makro.
Typ std::string powinien być wykorzystywany do przechowywania i przetwarzania napisów zamiast char*.
Typ string jest dużo wygodniejszy, bezpieczniejszy, obiektowy i ułatwia zastosowanie algorytmów biblioteki standardowej.
Rzutowanie w stylu C nie powinno być stosowane. Zamiast niego powinno być stosowane rzutowania w stylu C++ (static_cast, dynamic_cast, const_cast, reinterpret_cast).
Rzutowanie w stylu C pozwala zrzutować cokolwiek na cokolwiek - ne jest to bezpieczne, szczególnie w kontekście rzutowania obiektów lub struktur. C++ zapewnia własne rodzaje rzutowania, które są dużo bezpeczniejsze, niż rzutowanie w stylu C. Jeśli faktycznie konieczne jest rzutowanie między dwoma niezwiązanymi typami, powinno zostać użyte rzutowanie reinterpret_cast - będzie to stanowiło dodatkową informację dla czytającego kod, że właśnie o taki typ rzutowania nam chodzi.
Więcej informacji na temat rzutowania w C++:
http://www.intercon.pl/~sektor/cbx/appendix/casting.html
http://www.cplusplus.com/doc/tutorial/typecasting.html
Klasy i obiekty.
Każda klasa powinna mieć swój własny plik nagłówkowy oraz własny plik źródłowy (jesli to konieczne). Pliki te będą nazwane nazwą klasy z odpowiednim rozszerzeniem.
Wiele rozszerzeń języka C i C++ wymaga pisania wyodrębnionych deklaracji klas w plikach nagłówkowych. Najczęściej zdarza się to w programach wykorzystujących okienkowe interfejsy bądź zewnętrzne, zewnętrznie kompilowane biblioteki.
Przykład: Biblioteki graficzne Qt firmy Trolltech wymagają skompilowania plików .h z dokładną deklaracją, aby stworzyć pliki .moc (Meta Object Compiler), które służą do obsługi zdarzeń Qt w skonstruowanych klasach C++.
Poza powyższym, podział kodu między nagłówek z deklaracją oraz plik implementacyjny jest ogólnie przyjętą konwencją kodowania w C++. Nie wspominam już o tym, że definicje metod w ciałach klasy automatycznie stają się inline, odbierając nam możliwość dokonania wyboru.
UWAGA! Ta wskazówka dotyczy klas, natomiast NIE dotyczy szablonów klas. W szablonach klas (template classes) nie da się rozdzielić deklaracji od implementacji bez kilku zabiegów, które i tak nie załatwiają całej sprawy albo naruszają przyjęte konwencje, dlatego sądzę, że nie ma sensu wymagać od wszystkich, żeby stosowali rozdzielenie w tym przypadku.
Więcej informacji na ten temat dla zainteresowanych:
http://www.parashift.com/c++-faq-lite/templates.html#faq-35.12
Deklaracja klasy będzie zawierać pojedyncze bloki deklaracji public, protected i private. W deklaracji klasy będą wypisane w pierwszej kolejności składniki publiczne, następnie chronione, a na końcu prywatne.
Składniki publiczne są ważne z punktu widzenia użytkownika klasy, zatem powinny być widoczne w pierwszej kolejności. Składniki chronione mogą stanowić przedmiot zainteresowania dla kogoś, kto chce dziedziczyć z danej klasy. Składniki prywatne są zazwyczaj interesujące dla najmniejszej liczby osób, dlatego powinny być umieszczone na końcu deklaracji klasy.
Przykład:
class MojaKlasa { public: //deklaracje składników publicznych. protected: //deklaracje składników chronionych. private: //deklaracje składników prywatnych. };
Każdy konstruktor klasy, który przyjmuje jeden argument, lub którego wszystkie argumenty poza pierwszym mają wartości domyślne, powinien być zadeklarowany za pomocą słowa kluczowego explicit.
Nieoznaczenie takiego konstruktora jako explicit może doprowadzić do niepożądanych i nieintuicyjnych zachowań obiektów takiej klasy. Weźmy poniższy przykład działajacego kodu:
class Stack { public: Stack(int initSize, string name = "taki fajny stosik") { size = initSize; } //... private: int size; //... }; int main(void) { //Tworzenie obiektu - wywołanie konstruktora: Stack myStack(120, "moj stosik"); // //Ups! wywołujemy ten sam konstruktor po raz kolejny, nadpisując obiekt: myStack = 6; return 0; }
W powyższym przypadku miało miejsce niejawne rzutowanie wartości typu
int na wartość typu Stack. Deklaracja metody jako explicit wymusza jawne
rzutowanie, chroniąc programistę przed tego typu niespodziankami już na
etapie kompilacji:
class Stack { public: explicit Stack(int initSize, string name = "taki fajny stosik") { size = initSize; } //... private: int size; //... }; int main(void) { //Tworzenie obiektu - wywołanie konstruktora: Stack myStack(120, "moj stosik"); // //Edytor przyjmie wszystko, ale kompilator nie przełknie tej linijki: myStack = 6; //Błąd! Niejawne rzutowanie zabronione! return 0; }
Wszystkie pola klas powinny być zadeklarowane jako prywatne (private).
Udostępnianie pól klasy na zewnątrz (także klasom dziedziczącym) jest sprzeczne z ideą obiektowości (a konkretnie - z ukrywaniem danych) oraz zasadami dobrego projektowania obiektowego (konkretnie - z Zasadą Otwarte-Zamknięte (ang. Open-Closed Principle)). Pola powinny być dostępne przez odpowiednie metody, zwane getterami i setterami (lub accesorami i mutatorami), które będą publiczne (jeśli wartości mają być dostępne dla każdego), lub chronione (jeśli wartości mają być dostępne tylko dla podklas naszej klasy).
Kolejność w jakiej należy rozważyć sposoby przekazywania obiektów do procedur to: stała referencja, referencja, wartość.
Przekazywanie zmiennej obiektowej poprzez wartość powoduje stworzenie lokalnej kopii obiektu o zasięgu procedury. Utworzenie takiej kopii zabiera czas i pamięć, szczególnie w przypadku obiektów o dużej ilości pól (jak wiadomo, metody są umieszczane w pamięci raz dla danej klasy, więc nie wpływają na wydajność).
Przekazywanie obiektów przez referencję powoduje, że możliwa jest modyfikacja obiektu w procedurze. Żeby się przed tym uchronić, należy stosować stałe referencje.
Przykład:
class MojaKlasa { public: setAnInt(int newInt) { anInt = newInt; } protected: private: int anInt; }; void foo(MojaKlasa& obiekt) { obiekt.setAnInt(123); //zmienia przekazany obiekt. } void bar(const MojaKlasa& obiekt) { obiekt.setAnInt(616); //Błąd!! Nie da się zmieniać referencji do stałej. //nasz obiekt jest zabezpieczony od zmian wewnątrz funkcji bar(). }
należy zaznaczyć, że, w przeciwieństwie do wskaźników, poniższe dwa sposoby zapisu referencji są równoważne:
const Typ& obiekt; Typ& const obiekt;
Oczywiście wykorzystanie któregoś z tych sposobów zależy od potrzeb. Ta wskazówka jest tylko sugestią, że wszystko co się da, podajemy przez stałą referencję. Jak się nie da, to przez referencję, a jak tego nie da się zrobić (albo z pewnych względów jest to mało komfortowe), to dopiero przez wartość.
Własne klasy wyjątków powinny dziedziczyć po jednej ze standardowych klas wyjątków.
Ta praktyka pozwala aplikacjom dobrze współpracować ze standardem C++ oraz z aplikacjami, które na tym standardzie polegają. Dziedzicząc ze standardowej klasy wyjątku mamy pewność, że kod, który łapie standardowe wyjątki, złapie też nasze.
W większości wypadków dziedziczenie po exception powinno być wystarczające.
Dla przypomnienia - hierarchia wyjątków standardowych w C++ przedstawia się następująco (wcięcie oznacza dziedziczenie, np. domain_error dziedziczy po logic_error, ktory dziedziczy po exception):
exception
logic_error
domain_error
invalid_argument
length_error
out_of_range
runtime_error
range_error
overflow_error
underflow_error
bad_alloc
bad_cast
bad_exception
bad_typeid
ios_base::failure
Warto pamiętać, że niektóre typy wyjątków wymagają załączenia odpowiednich plików nagłówkowych biblioteki standardowej.
Więcej na ten temat:
C++ Biblioteka Standardowa. Podręcznik programisty autorstwa Nicolaia M. Josuttisa.
http://www.freshsources.com/Except1/ALLISON.HTM
http://wwwasd.web.cern.ch/wwwasd/lhc++/RW/stdlibcr/exc_9785.htm#Description
http://www.cplusplus.com/doc/tutorial/exceptions.html
Każda klasa powinna deklarować destruktor witrualny.
Destruktory wirtualne pozwalają odpowiednio usunąć obiekt wskazywany wskaźnikiem klasy, po której on dziedziczy (innymi słowy - klasy - przodka). Mówi się, że jeśli klasa ma chociaż jedną metodę wirtualną, powinna mieć wirtualny destruktor, ale w zasadzie nic nie stoi na przeszkodzie, żeby destruktor był zawsze wirtualny. Co prawda cierpi na tym nieznacznie wydajność programu (późne wiązanie), ale za to kod jest bezpieczniejszy i mniej podatny na błędy.
Ponadto taki destruktor powinien być opatrzony klauzulą throw(), tak, by niemożliwe było rzucenie z niego wyjątku.
Przykład deklaracji takiego destruktora:
virtual ~MojaKlasa() throw();
Każda klasa powinna implementować konstruktor kopiujący.
Nie polegamy na domyślnym konstruktorze kopiującym. Jeśli konstruktor kopiujący nie powinien być używany, będzie deklarowany jako prywatny i będzie posiadać pustą implementację.
Każda klasa powinna mieć zadeklarowany operator przypisania. Jeśli nie jest potrzebny - będzie prywatny. Jeśli jest potrzebny - jego pożądane zachowanie będzie jawnie zdefiniowane.
Zapewnienie klasie możliwości przypisania wartości jednego obiektu do drugiego musi być decyzją świadomą. Każda klasa implementuje domyślny niejawny operator przypisania, choć jest wiele klas, których obiektów nie chcemy do siebie przypisywać. Można zagwarantować, że nikt nigdy nie użyje operatora przypisania na obiektach naszej klasy, deklarując pusty, prywatny operator przypisania '='. Jeśli natomiast jest potrzebny - należy zapewnić jawną definicję jego działania.
Projektowanie obiektowe.
Każda klasa powinna stosować się do Zasady Podstawiania Liskov.
szczegóły w odpowiednim artykule