Widzę zbyt wielu programistów C, którzy nienawidzą C ++. Zajęło mi sporo czasu (lat), aby powoli zrozumieć, co jest dobre, a co złe. Myślę, że najlepszym sposobem na wyrażenie tego jest:
Mniej kodu, brak narzutu w czasie wykonywania, większe bezpieczeństwo.
Im mniej kodu piszemy, tym lepiej. Szybko staje się to jasne u wszystkich inżynierów dążących do doskonałości. Naprawiasz błąd w jednym miejscu, nie w wielu - raz wyrażasz algorytm i ponownie go używasz w wielu miejscach, itd. Grecy mają nawet powiedzenie wywodzące się od starożytnych Spartan: „powiedzieć coś krótszymi słowami, znaczy znaczy że jesteś o tym mądry ”. Faktem jest, że przy poprawnym użyciu C ++ pozwala wyrazić się w znacznie mniejszym kodzie niż C, bez kosztowania szybkości działania, a jednocześnie jest bezpieczniejszy (tzn. Wychwytuje więcej błędów w czasie kompilacji) niż C.
Oto uproszczony przykład z mojego renderera : Podczas interpolacji wartości pikseli na linii skanowania trójkąta. Muszę zacząć od współrzędnej X x1 i osiągnąć współrzędną X x2 (od lewej do prawej strony trójkąta). I na każdym kroku, na każdym mijanym pikselu muszę interpolować wartości.
Kiedy interpoluję światło otoczenia docierające do piksela:
typedef struct tagPixelDataAmbient {
int x;
float ambientLight;
} PixelDataAmbient;
...
// inner loop
currentPixel.ambientLight += dv;
Kiedy interpoluję kolor (nazywany cieniowaniem „Gouraud”, gdzie pola „czerwony”, „zielony” i „niebieski” są interpolowane wartością kroku na każdym pikselu):
typedef struct tagPixelDataGouraud {
int x;
float red;
float green;
float blue; // The RGB color interpolated per pixel
} PixelDataGouraud;
...
// inner loop
currentPixel.red += dred;
currentPixel.green += dgreen;
currentPixel.blue += dblue;
Kiedy renderuję w cieniu „Phong”, nie interpoluję już intensywności (ambientLight) ani koloru (czerwony / zielony / niebieski) - interpoluję normalny wektor (nx, ny, nz) i na każdym kroku muszę ponownie -obliczyć równanie oświetlenia w oparciu o interpolowany wektor normalny:
typedef struct tagPixelDataPhong {
int x;
float nX;
float nY;
float nZ; // The normal vector interpolated per pixel
} PixelDataPhong;
...
// inner loop
currentPixel.nX += dx;
currentPixel.nY += dy;
currentPixel.nZ += dz;
Pierwszym instynktem programistów C byłoby „cholera, napisz trzy funkcje interpolujące wartości i wywołaj je w zależności od ustawionego trybu”. Przede wszystkim oznacza to, że mam problem z typem - z czym mam pracować? Czy moje piksele to PixelDataAmbient? PixelDataGouraud? PixelDataPhong? Och, czekaj, sprawny programista C mówi: użyj unii!
typedef union tagSuperPixel {
PixelDataAmbient a;
PixelDataGouraud g;
PixelDataPhong p;
} SuperPixel;
... a następnie masz funkcję ...
RasterizeTriangleScanline(
enum mode, // { ambient, gouraud, phong }
SuperPixel left,
SuperPixel right)
{
int i,j;
if (mode == ambient) {
// handle pixels as ambient...
int steps = right.a.x - left.a.x;
float dv = (right.a.ambientLight - left.a.ambientLight)/steps;
float currentIntensity = left.a.ambientLight;
for (i=left.a.x; i<right.a.x; i++) {
WorkOnPixelAmbient(i, dv);
currentIntensity+=dv;
}
} else if (mode == gouraud) {
// handle pixels as gouraud...
int steps = right.g.x - left.g.x;
float dred = (right.g.red - left.g.red)/steps;
float dgreen = (right.g.green - left.a.green)/steps;
float dblue = (right.g.blue - left.g.blue)/steps;
float currentRed = left.g.red;
float currentGreen = left.g.green;
float currentBlue = left.g.blue;
for (j=left.g.x; i<right.g.x; j++) {
WorkOnPixelGouraud(j, currentRed, currentBlue, currentGreen);
currentRed+=dred;
currentGreen+=dgreen;
currentBlue+=dblue;
}
...
Czy odczuwasz chaos?
Po pierwsze, jedna literówka jest wszystkim, co jest potrzebne do awarii mojego kodu, ponieważ kompilator nigdy nie zatrzyma mnie w sekcji „Gouraud” funkcji, aby faktycznie uzyskać dostęp do „.a”. wartości (otoczenia). Błąd, który nie został przechwycony przez system typu C (to znaczy podczas kompilacji), oznacza błąd, który pojawia się w czasie wykonywania i będzie wymagał debugowania. Czy zauważyłeś, że korzystam left.a.green
z obliczeń „dgreen”? Kompilator na pewno ci tego nie powiedział.
Potem wszędzie jest powtórzenie - for
pętla istnieje tyle razy, ile jest trybów renderowania, cały czas wykonujemy „prawo minus lewo podzielone przez kroki”. Brzydki i podatny na błędy. Czy zauważyłeś, że porównuję za pomocą „i” w pętli Gourauda, kiedy powinienem był użyć „j”? Kompilator znów jest cichy.
Co z instrukcją if / else / ladder dla trybów? Co jeśli dodam nowy tryb renderowania za trzy tygodnie? Czy będę pamiętać, aby obsługiwać nowy tryb we wszystkich „if mode ==” w całym moim kodzie?
Teraz porównaj powyższą brzydotę z tym zestawem struktur C ++ i funkcją szablonu:
struct CommonPixelData {
int x;
};
struct AmbientPixelData : CommonPixelData {
float ambientLight;
};
struct GouraudPixelData : CommonPixelData {
float red;
float green;
float blue; // The RGB color interpolated per pixel
};
struct PhongPixelData : CommonPixelData {
float nX;
float nY;
float nZ; // The normal vector interpolated per pixel
};
template <class PixelData>
RasterizeTriangleScanline(
PixelData left,
PixelData right)
{
PixelData interpolated = left;
PixelData step = right;
step -= left;
step /= int(right.x - left.x); // divide by pixel span
for(int i=left.x; i<right.x; i++) {
WorkOnPixel<PixelData>(interpolated);
interpolated += step;
}
}
Teraz spójrz na to. Nie produkujemy już zupy typu uni: mamy określone typy dla każdego trybu. Ponownie używają swoich wspólnych elementów (pola „x”), dziedzicząc po klasie bazowej ( CommonPixelData
). A szablon sprawia, że kompilator UTWORZA (czyli generuje kod) trzy różne funkcje, które sami napisalibyśmy w C, ale jednocześnie bardzo rygorystycznie pod względem typów!
Nasza pętla w szablonie nie może goof i uzyskiwać dostępu do nieprawidłowych pól - jeśli to zrobimy, kompilator będzie szczekał.
Szablon wykonuje wspólną pracę (pętla, za każdym razem zwiększając się o „krok”) i może to zrobić w sposób, który po prostu NIE MOŻE powodować błędów w czasie wykonywania. Interpolacja od rodzaju ( AmbientPixelData
, GouraudPixelData
, PhongPixelData
) odbywa się za operator+=()
które dodamy w elemencie - co w zasadzie dyktują jak każdy typ jest interpolowana.
Czy widzisz, co zrobiliśmy z WorkOnPixel <T>? Chcemy wykonać inną pracę dla każdego typu? Po prostu nazywamy specjalizację szablonu:
void WorkOnPixel<AmbientPixelData>(AmbientPixelData& p)
{
// use the p.ambientLight field
}
void WorkOnPixel<GouraudPixelData>(GouraudPixelData& p)
{
// use the p.red/green/blue fields
}
To znaczy - funkcja do wywołania jest ustalana na podstawie typu. W czasie kompilacji!
Aby ponownie sformułować:
- minimalizujemy kod (poprzez szablon), ponownie wykorzystując wspólne części,
- nie używamy brzydkich hacków, utrzymujemy ścisły system typów, dzięki czemu kompilator może nas zawsze sprawdzać.
- a co najważniejsze: żadne z naszych działań nie miało ŻADNEGO wpływu na środowisko uruchomieniowe. Ten kod będzie działał TYLKO tak szybko, jak równoważny kod C - w rzeczywistości, jeśli kod C używał wskaźników funkcji do wywoływania różnych
WorkOnPixel
wersji, kod C ++ będzie SZYBSZY niż C, ponieważ kompilator wprowadzi WorkOnPixel
specjalizację szablonu specyficzną dla typu połączenie!
Mniej kodu, brak narzutu w czasie wykonywania, większe bezpieczeństwo.
Czy to oznacza, że C ++ jest językiem typu „wszystko na końcu”? Oczywiście nie. Nadal musisz mierzyć kompromisy. Ignoranci używają C ++, kiedy powinni napisać skrypt Bash / Perl / Python. Zadowoleni z wyzwalaczy nowicjusze C ++ utworzą głęboko zagnieżdżone klasy z wirtualnym wielokrotnym dziedziczeniem, zanim będzie można je zatrzymać i wysłać. Będą używać złożonego metaprogramowania Boost, zanim zdadzą sobie sprawę, że nie jest to konieczne. Będą nadal używać char*
, strcmp
i makr zamiast std::string
i szablonów.
Ale to nic więcej niż ... patrz, z kim pracujesz. Nie ma języka, który chroniłby cię przed niekompetentnymi użytkownikami (nie, nawet Java).
Studiuj i używaj C ++ - po prostu nie przesadzaj.