Veo demasiados programadores de C que odian C ++. Me llevó bastante tiempo (años) comprender lentamente qué es bueno y qué tiene de malo. Creo que la mejor manera de expresarlo es esta:
Menos código, sin gastos generales de tiempo de ejecución, más seguridad.
Cuanto menos código escribamos, mejor. Esto se aclara rápidamente en todos los ingenieros que luchan por la excelencia. Usted arregla un error en un lugar, no en muchos: expresa un algoritmo una vez y lo reutiliza en muchos lugares, etc. Los griegos incluso tienen un dicho, que se remonta a los antiguos espartanos: "decir algo en menos palabras, significa que eres sabio al respecto ". Y el hecho es que, cuando se usa correctamente , C ++ le permite expresarse en mucho menos código que C, sin costar la velocidad de tiempo de ejecución, a la vez que es más seguro (es decir, detecta más errores en tiempo de compilación) que C.
Aquí hay un ejemplo simplificado de mi renderizador : al interpolar valores de píxeles a través de la línea de exploración de un triángulo. Tengo que comenzar desde una coordenada X x1, y alcanzar una coordenada X x2 (desde el lado izquierdo al derecho de un triángulo). Y en cada paso, en cada píxel que paso, tengo que interpolar valores.
Cuando interpolo la luz ambiental que alcanza el píxel:
typedef struct tagPixelDataAmbient {
int x;
float ambientLight;
} PixelDataAmbient;
...
// inner loop
currentPixel.ambientLight += dv;
Cuando interpolo el color (llamado sombreado "Gouraud", donde los campos "rojo", "verde" y "azul" se interpolan por un valor de paso en cada píxel):
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;
Cuando renderizo en el sombreado "Phong", ya no interpolo una intensidad (luz ambiental) o un color (rojo / verde / azul); interpolo un vector normal (nx, ny, nz) y en cada paso, tengo que volver -calcule la ecuación de iluminación, basada en el vector normal interpolado:
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;
Ahora, el primer instinto de los programadores de C sería "diablos, escribir tres funciones que interpolan los valores y llamarlos según el modo establecido". En primer lugar, esto significa que tengo un problema de tipo: ¿con qué trabajo? ¿Mis píxeles son PixelDataAmbient? PixelDataGouraud? PixelDataPhong? Oh, espera, dice el eficiente programador C, ¡usa una unión!
typedef union tagSuperPixel {
PixelDataAmbient a;
PixelDataGouraud g;
PixelDataPhong p;
} SuperPixel;
..y luego, tienes una función ...
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;
}
...
¿Sientes el caos deslizándose?
En primer lugar, un error tipográfico es todo lo que se necesita para bloquear mi código, ya que el compilador nunca me detendrá en la sección "Gouraud" de la función, para acceder realmente al ".a". (ambiente) valores. Un error no detectado por el sistema de tipo C (es decir, durante la compilación) significa un error que se manifiesta en tiempo de ejecución y requerirá depuración. ¿ left.a.green
Notó que estoy accediendo en el cálculo de "dgreen"? El compilador seguramente no te lo dijo.
Luego, hay repetición en todas partes: el for
bucle está allí tantas veces como haya modos de renderizado, seguimos haciendo "derecha menos izquierda dividida por pasos". Feo y propenso a errores. ¿Notó que comparo el uso de "i" en el bucle de Gouraud, cuando debería haber usado "j"? El compilador vuelve a estar en silencio.
¿Qué pasa con el if / else / ladder para los modos? ¿Qué sucede si agrego un nuevo modo de renderizado en tres semanas? ¿Recordaré manejar el nuevo modo en todos los "if mode ==" en todo mi código?
Ahora compare la fealdad anterior, con este conjunto de estructuras C ++ y una función de plantilla:
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;
}
}
Ahora mira esto. Ya no hacemos una sopa tipo union: tenemos tipos específicos para cada modo. Reutilizan sus cosas comunes (el campo "x") heredando de una clase base ( CommonPixelData
). Y la plantilla hace que el compilador CREE (es decir, genere código) las tres funciones diferentes que habríamos escrito nosotros en C, pero al mismo tiempo, ¡es muy estricto con los tipos!
Nuestro bucle en la plantilla no puede funcionar y acceder a campos no válidos: el compilador ladrará si lo hacemos.
La plantilla realiza el trabajo común (el ciclo, que aumenta en "paso" cada vez), y puede hacerlo de una manera que simplemente NO PUEDE causar errores de tiempo de ejecución. La interpolación según el tipo ( AmbientPixelData
, GouraudPixelData
, PhongPixelData
) se realiza con la operator+=()
que vamos a añadir en las estructuras - que básicamente dictan cómo se interpola cada tipo.
¿Y ves lo que hicimos con WorkOnPixel <T>? ¿Queremos hacer un trabajo diferente por tipo? Simplemente llamamos una especialización de plantilla:
void WorkOnPixel<AmbientPixelData>(AmbientPixelData& p)
{
// use the p.ambientLight field
}
void WorkOnPixel<GouraudPixelData>(GouraudPixelData& p)
{
// use the p.red/green/blue fields
}
Es decir, la función a llamar se decide en función del tipo. ¡En tiempo de compilación!
Para reformularlo de nuevo:
- minimizamos el código (a través de la plantilla), reutilizando partes comunes,
- no usamos hacks feos, mantenemos un sistema de tipo estricto, para que el compilador pueda verificarnos en todo momento.
- y lo mejor de todo: nada de lo que hicimos tiene NINGÚN impacto en el tiempo de ejecución. Este código se ejecutará SOLO tan rápido como el código C equivalente; de hecho, si el código C usaba punteros de función para llamar a las diversas
WorkOnPixel
versiones, el código C ++ será MÁS RÁPIDO que C, porque el compilador incorporará la WorkOnPixel
especialización de plantilla específica del tipo ¡llamada!
Menos código, sin gastos generales de tiempo de ejecución, más seguridad.
¿Significa esto que C ++ es el todo y el fin de todos los lenguajes? Por supuesto no. Todavía tiene que medir las compensaciones. Las personas ignorantes usarán C ++ cuando deberían haber escrito un script Bash / Perl / Python. Los novatos en C ++ que se desencadenan crearán clases anidadas profundas con herencia virtual múltiple antes de que pueda detenerlos y enviarlos a empacar. Utilizarán la metaprogramación Boost compleja antes de darse cuenta de que esto no es necesario. TODAVÍA usarán char*
, strcmp
y macros, en lugar de std::string
y plantillas.
Pero esto no dice nada más que ... mira con quién trabajas. No existe un lenguaje que lo proteja de usuarios incompetentes (no, ni siquiera Java).
Sigue estudiando y usando C ++, solo que no lo diseñes en exceso.