Vedo troppi programmatori C che odiano il C ++. Mi ci è voluto un po 'di tempo (anni) per capire lentamente cosa è buono e cosa è male. Penso che il modo migliore per esprimere sia questo:
Meno codice, nessun sovraccarico di runtime, più sicurezza.
Meno codice scriviamo, meglio è. Questo diventa rapidamente chiaro in tutti gli ingegneri che lottano per l'eccellenza. Risolvi un bug in un posto, non in molti: esprimi un algoritmo una volta e lo riutilizzi in molti luoghi, ecc. I greci hanno persino un modo di dire, fatto risalire agli antichi spartani: "dire qualcosa in meno parole, significa che sei saggio al riguardo ". E il fatto è che, se usato correttamente , C ++ ti permette di esprimerti in un codice molto meno di C, senza costare la velocità di runtime, pur essendo più sicuro (cioè catturando più errori in fase di compilazione) di quanto lo sia C.
Ecco un esempio semplificato dal mio renderer : quando si interpolano i valori dei pixel attraverso la linea di scansione di un triangolo. Devo partire da una coordinata X x1 e raggiungere una coordinata X x2 (da sinistra a destra di un triangolo). E attraverso ogni passaggio, attraverso ogni pixel che passo, devo interpolare i valori.
Quando interpolo la luce ambientale che raggiunge il pixel:
typedef struct tagPixelDataAmbient {
int x;
float ambientLight;
} PixelDataAmbient;
...
// inner loop
currentPixel.ambientLight += dv;
Quando interpolo il colore (chiamato ombreggiatura "Gouraud", in cui i campi "rosso", "verde" e "blu" sono interpolati da un valore di passo per ogni pixel):
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;
Quando eseguo il rendering in ombreggiatura "Phong", non interpolo più un'intensità (luce ambientale) o un colore (rosso / verde / blu) - Interpolo un vettore normale (nx, ny, nz) e ad ogni passo, devo ri -calcolare l'equazione dell'illuminazione, in base al vettore normale interpolato:
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;
Ora, il primo istinto dei programmatori C sarebbe "diamine, scrivere tre funzioni che interpolano i valori e chiamarli in base alla modalità impostata". Prima di tutto, questo significa che ho un problema di tipo: con cosa lavoro? I miei pixel PixelDataAmbient sono? PixelDataGouraud? PixelDataPhong? Oh, aspetta, dice l'efficiente programmatore C, usa un sindacato!
typedef union tagSuperPixel {
PixelDataAmbient a;
PixelDataGouraud g;
PixelDataPhong p;
} SuperPixel;
..e poi hai una funzione ...
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;
}
...
Senti che il caos sta scivolando dentro?
Prima di tutto, è sufficiente un errore di battitura per bloccare il mio codice, poiché il compilatore non mi fermerà mai nella sezione "Gouraud" della funzione, per accedere effettivamente a ".a". valori (ambientali). Un bug non rilevato dal sistema di tipo C (cioè durante la compilazione) indica un bug che si manifesta in fase di esecuzione e richiederà il debug. Hai notato che left.a.green
accedo al calcolo di "dgreen"? Il compilatore sicuramente non te lo ha detto.
Quindi, c'è ripetizione ovunque: il for
loop è lì per tutte le volte che ci sono modalità di rendering, continuiamo a fare "destra meno sinistra divisa per passi". Brutto e soggetto a errori. Hai notato che ho confrontato usando "i" nel loop di Gouraud, quando avrei dovuto usare "j"? Il compilatore è di nuovo silenzioso.
Che dire dell'if / else / ladder per le modalità? Cosa succede se aggiungo una nuova modalità di rendering in tre settimane? Ricorderò di gestire la nuova modalità in tutto il "if mode ==" in tutto il mio codice?
Ora confronta la bruttezza sopra, con questo set di strutture C ++ e una funzione template:
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;
}
}
Ora guarda questo. Non creiamo più una zuppa di tipo sindacale: abbiamo tipi specifici per ogni modalità. Riutilizzano le loro cose comuni (il campo "x") ereditando da una classe base ( CommonPixelData
). E il template rende il compilatore CREATE (ovvero generare codice) le tre diverse funzioni che avremmo scritto in C, ma allo stesso tempo, essendo molto severi sui tipi!
Il nostro ciclo nel modello non può andare in giro e accedere a campi non validi - il compilatore abbaia se lo facciamo.
Il modello esegue il lavoro comune (il ciclo, aumentando di "passo" in ogni momento) e può farlo in un modo che semplicemente NON può causare errori di runtime. L'interpolazione per ogni tipo ( AmbientPixelData
, GouraudPixelData
, PhongPixelData
) è fatto con il operator+=()
che l'aggiungeremo nei struct - che fondamentalmente dettano come ogni tipo viene interpolato.
E vedi cosa abbiamo fatto con WorkOnPixel <T>? Vogliamo fare lavori diversi per tipo? Chiamiamo semplicemente una specializzazione modello:
void WorkOnPixel<AmbientPixelData>(AmbientPixelData& p)
{
// use the p.ambientLight field
}
void WorkOnPixel<GouraudPixelData>(GouraudPixelData& p)
{
// use the p.red/green/blue fields
}
Cioè - la funzione da chiamare, viene decisa in base al tipo. In fase di compilazione!
Per riformularlo di nuovo:
- minimizziamo il codice (tramite il modello), riutilizzando parti comuni,
- non usiamo brutti hack, manteniamo un sistema di tipi rigoroso, in modo che il compilatore possa controllarci in ogni momento.
- e soprattutto: nulla di ciò che abbiamo fatto ha QUALSIASI impatto di runtime. Questo codice verrà eseguito SOLO più velocemente dell'equivalente codice C - in effetti, se il codice C utilizzava i puntatori a funzione per chiamare le varie
WorkOnPixel
versioni, il codice C ++ sarà più veloce di C, perché il compilatore incorporerà la WorkOnPixel
specializzazione del modello specifico del tipo chiamata!
Meno codice, nessun sovraccarico di runtime, più sicurezza.
Questo significa che il C ++ è l'essere tutti e terminare tutte le lingue? Ovviamente no. Devi ancora misurare i compromessi. Le persone ignoranti useranno C ++ quando avrebbero dovuto scrivere uno script Bash / Perl / Python. I neofiti del C ++ in grado di innescare creeranno classi annidate profonde con ereditarietà multipla virtuale prima che tu possa fermarle e inviarle a pacchetti. Utilizzeranno la complessa meta-programmazione Boost prima di rendersi conto che ciò non è necessario. Essi utilizzano ancora char*
, strcmp
e le macro, invece di std::string
e modelli.
Ma questo non dice altro che ... guarda con chi lavori. Non esiste un linguaggio per proteggerti dagli utenti incompetenti (no, nemmeno Java).
Continua a studiare e ad usare il C ++ - semplicemente non sovrascrivere.