Eu vejo muitos programadores em C que odeiam C ++. Levei algum tempo (anos) para entender lentamente o que é bom e o que é ruim. Eu acho que a melhor maneira de expressar isso é esta:
Menos código, sem sobrecarga de tempo de execução, mais segurança.
Quanto menos código escrevermos, melhor. Isso rapidamente fica claro em todos os engenheiros que buscam a excelência. Você corrige um bug em um lugar, não muitos - você expressa um algoritmo uma vez e o reutiliza em muitos lugares, etc. Os gregos têm até um ditado, que remonta aos antigos espartanos: "dizer algo em menos palavras, significa que você é sábio sobre isso ". E o fato é que, quando usado corretamente , o C ++ permite que você se expresse em muito menos código que C, sem custar a velocidade do tempo de execução, sendo mais seguro (ou seja, capturando mais erros em tempo de compilação) do que C é.
Aqui está um exemplo simplificado do meu renderizador : Ao interpolar valores de pixel na linha de varredura de um triângulo. Eu tenho que começar a partir de uma coordenada X x1 e alcançar uma coordenada X x2 (da esquerda para a direita de um triângulo). E em cada etapa, em cada pixel que passo, tenho que interpolar valores.
Quando interpolo a luz ambiente que atinge o pixel:
typedef struct tagPixelDataAmbient {
int x;
float ambientLight;
} PixelDataAmbient;
...
// inner loop
currentPixel.ambientLight += dv;
Quando interpolo a cor (chamada sombreamento "Gouraud", onde os campos "vermelho", "verde" e "azul" são interpolados por um valor de etapa em cada 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 renderizo no sombreamento "Phong", não interpolo mais uma intensidade (ambientLight) ou uma cor (vermelho / verde / azul) - interpolo um vetor normal (nx, ny, nz) e, a cada passo, tenho que voltar -calcular a equação da iluminação, com base no vetor 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;
Agora, o primeiro instinto dos programadores em C seria "heck, escreva três funções que interpolam os valores e os chame dependendo do modo definido". Primeiro de tudo, isso significa que tenho um problema de tipo - com o que trabalho? Meus pixels são PixelDataAmbient? PixelDataGouraud? PixelDataPhong? Oh, espere, diz o eficiente programador C, use uma união!
typedef union tagSuperPixel {
PixelDataAmbient a;
PixelDataGouraud g;
PixelDataPhong p;
} SuperPixel;
..e então, você tem uma função ...
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;
}
...
Você sente o caos entrando?
Antes de tudo, basta digitar um erro de digitação para travar meu código, pois o compilador nunca vai me parar na seção "Gouraud" da função para acessar realmente o ".a". valores (ambientais). Um erro não detectado pelo sistema do tipo C (ou seja, durante a compilação), significa um erro que se manifesta no tempo de execução e requer depuração. Você notou que estou acessando left.a.green
no cálculo de "dgreen"? O compilador certamente não lhe disse isso.
Então, há repetição em todos os lugares - o for
loop existe quantas vezes houver modos de renderização, continuamos fazendo "direita menos esquerda dividida por etapas". Feio e propenso a erros. Você notou que eu comparo usando "i" no loop Gouraud, quando eu deveria ter usado "j"? O compilador está novamente silencioso.
E o if / else / ladder para os modos? E se eu adicionar um novo modo de renderização em três semanas? Lembrarei de lidar com o novo modo em todo o "if mode ==" em todo o meu código?
Agora compare a feiúra acima, com este conjunto de estruturas C ++ e uma função de modelo:
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;
}
}
Agora veja isso. Não fazemos mais um tipo de sopa de união: temos tipos específicos para cada modo. Eles reutilizam suas coisas comuns (o campo "x") herdando de uma classe base ( CommonPixelData
). E o modelo torna o compilador CREATE (isto é, gera código) as três funções diferentes que teríamos escrito em C, mas, ao mesmo tempo, sendo muito rigoroso quanto aos tipos!
Nosso loop no modelo não pode brincar e acessar campos inválidos - o compilador latirá se o fizermos.
O modelo executa o trabalho comum (o loop, aumentando em "etapa" a cada vez)) e pode fazer isso de uma maneira que simplesmente NÃO PODE causar erros de tempo de execução. A interpolação por tipo ( AmbientPixelData
, GouraudPixelData
, PhongPixelData
) é feito com o operator+=()
que vamos adicionar os estruturas - que basicamente ditam como cada tipo é interpolada.
E você vê o que fizemos com o WorkOnPixel <T>? Queremos fazer um trabalho diferente por tipo? Simplesmente chamamos uma especialização de modelo:
void WorkOnPixel<AmbientPixelData>(AmbientPixelData& p)
{
// use the p.ambientLight field
}
void WorkOnPixel<GouraudPixelData>(GouraudPixelData& p)
{
// use the p.red/green/blue fields
}
Ou seja - a função a ser chamada é decidida com base no tipo. Em tempo de compilação!
Para reformular novamente:
- minimizamos o código (por meio do modelo), reutilizando partes comuns,
- não usamos hacks feios, mantemos um sistema de tipos rigorosos, para que o compilador possa nos verificar o tempo todo.
- e o melhor de tudo: nada do que fizemos teve QUALQUER impacto no tempo de execução. Esse código será executado APENAS tão rápido quanto o código C equivalente - na verdade, se o código C estiver usando ponteiros de função para chamar as várias
WorkOnPixel
versões, o código C ++ será MAIS RÁPIDO que o C, porque o compilador incluirá a WorkOnPixel
especialização de modelo específico do tipo ligar!
Menos código, sem sobrecarga de tempo de execução, mais segurança.
Isso significa que C ++ é o primeiro e o fim de todas as linguagens? Claro que não. Você ainda precisa medir as compensações. Pessoas ignorantes usarão C ++ quando deveriam ter escrito um script Bash / Perl / Python. Os novatos em C ++ com gatilho feliz criarão classes aninhadas profundas com herança múltipla virtual antes que você possa pará-los e enviá-los para empacotamento. Eles usarão a meta-programação complexa do Boost antes de perceber que isso não é necessário. Eles ainda usam char*
, strcmp
e macros, em vez de std::string
e modelos.
Mas isso não diz nada mais do que ... observe com quem você trabalha. Não há linguagem para protegê-lo de usuários incompetentes (não, nem mesmo Java).
Continue estudando e usando C ++ - apenas não exagere.