Je vois beaucoup trop de programmeurs C qui détestent le C ++. Il m'a fallu un certain temps (années) pour comprendre lentement ce qui est bon et ce qui est mal à son sujet. Je pense que la meilleure façon de la formuler est la suivante:
Moins de code, pas de temps d’exécution, plus de sécurité.
Moins nous écrivons de code, mieux ce sera. Cela devient rapidement évident pour tous les ingénieurs qui visent l'excellence. Vous corrigez un bogue à un endroit, pas beaucoup - vous exprimez un algorithme une fois, et le réutilisez dans de nombreux endroits, etc. Les Grecs ont même un dicton remontant aux anciens Spartans: "dire quelque chose en moins de mots, de moyens que vous êtes sage à ce sujet ". Et le fait est que, utilisé correctement , le C ++ vous permet de vous exprimer avec beaucoup moins de code que le C, sans perdre de temps à la vitesse d'exécution, tout en étant plus sûr (c'est-à-dire, en capturant plus d'erreurs au moment de la compilation) que le C.
Voici un exemple simplifié de mon moteur de rendu : lors de l'interpolation de valeurs de pixels sur la ligne de balayage d'un triangle. Je dois partir d'une coordonnée X x1 et atteindre une coordonnée X x2 (de gauche à droite d'un triangle). Et à chaque étape, à chaque pixel que je passe, je dois interpoler des valeurs.
Lorsque j'interpole la lumière ambiante atteignant le pixel:
typedef struct tagPixelDataAmbient {
int x;
float ambientLight;
} PixelDataAmbient;
...
// inner loop
currentPixel.ambientLight += dv;
Lorsque j'interpole la couleur (appelée ombrage "Gouraud", où les champs "rouge", "vert" et "bleu" sont interpolés d'une valeur de pas à chaque 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;
Lorsque je fais un ombrage "Phong", je n’interpole plus une intensité (ambientLight) ou une couleur (rouge / vert / bleu) - j’interpole un vecteur normal (nx, ny, nz) et à chaque étape, je dois -calculer l'équation d'éclairage en fonction du vecteur normal interpolé:
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;
Maintenant, le premier instinct des programmeurs C serait "zut, écrit trois fonctions qui interpolent les valeurs, et les appelle en fonction du mode défini". Tout d’abord, cela signifie que j’ai un problème de type - avec quoi je travaille? Sont mes pixels PixelDataAmbient? PixelDataGouraud? PixelDataPhong? Oh, attendez, le programmeur C efficace dit: utilisez une union!
typedef union tagSuperPixel {
PixelDataAmbient a;
PixelDataGouraud g;
PixelDataPhong p;
} SuperPixel;
..et alors, vous avez une fonction ...
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;
}
...
Sentez-vous le chaos s'infiltrer?
Tout d’abord, il suffit d’une faute de frappe pour planter mon code, car le compilateur ne m’arrêtera jamais dans la section "Gouraud" de la fonction pour accéder réellement au fichier ".a". (ambiant) valeurs. Un bogue non détecté par le système de type C (c'est-à-dire lors de la compilation) désigne un bogue qui se manifeste au moment de l'exécution et qui nécessite un débogage. Avez-vous remarqué que je suis left.a.green
en train d' accéder au calcul de "dgreen"? Le compilateur ne vous l'a sûrement pas dit.
Ensuite, il y a de la répétition partout - la for
boucle est là autant de fois qu'il y a de modes de rendu, nous continuons à faire "juste moins gauche divisé par étapes". Moche et sujet aux erreurs. Avez-vous remarqué que je compare en utilisant "i" dans la boucle de Gouraud, alors que j'aurais dû utiliser "j"? Le compilateur est à nouveau silencieux.
Qu'en est-il du if / else / ladder pour les modes? Et si j'ajoutais un nouveau mode de rendu, dans trois semaines? Est-ce que je me souviendrai de gérer le nouveau mode dans tous les "if mode ==" dans tout mon code?
Maintenant comparons la laideur ci-dessus, avec cet ensemble de structures C ++ et une fonction 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;
}
}
Maintenant, regarde ça. Nous ne faisons plus de soupe d'union: nous avons des types spécifiques pour chaque mode. Ils réutilisent leur substance commune (le champ "x") en héritant d'une classe de base ( CommonPixelData
). Et le modèle fait au compilateur CRÉER (c’est-à-dire générer du code) les trois fonctions différentes que nous aurions nous-mêmes écrites en C, tout en étant très strictes concernant les types!
Notre boucle dans le modèle ne peut pas goof et accéder à des champs non valides - le compilateur aboyera si nous le faisons.
Le modèle effectue le travail commun (la boucle, augmentant chaque "étape"), et peut le faire d'une manière qui ne peut simplement PAS causer d'erreurs d'exécution. L'interpolation par type ( AmbientPixelData
, GouraudPixelData
, PhongPixelData
) se fait avec la operator+=()
que nous ajouterons dans les struct - qui dictent essentiellement la façon dont chaque type est interpolée.
Et voyez-vous ce que nous avons fait avec WorkOnPixel <T>? Nous voulons faire un travail différent par type? Nous appelons simplement une spécialisation de modèle:
void WorkOnPixel<AmbientPixelData>(AmbientPixelData& p)
{
// use the p.ambientLight field
}
void WorkOnPixel<GouraudPixelData>(GouraudPixelData& p)
{
// use the p.red/green/blue fields
}
C’est-à-dire que la fonction à appeler est déterminée en fonction du type. Au moment de la compilation!
Pour le reformuler à nouveau:
- nous minimisons le code (via le modèle), en réutilisant les parties communes,
- nous n'utilisons pas de hacks laids, nous gardons un système de types strict, afin que le compilateur puisse nous contrôler à tout moment.
- Et le meilleur de tous: rien de ce que nous avons fait n’a AUCUN impact sur l’exécution. Ce code fonctionnera JUSTE aussi rapidement que le code C équivalent. En fait, si le code C utilisait des pointeurs de fonction pour appeler les différentes
WorkOnPixel
versions, le code C ++ serait plus rapide que le code C, car le compilateur incorporerait la WorkOnPixel
spécialisation de modèle spécifique au type. appel!
Moins de code, pas de temps d’exécution, plus de sécurité.
Cela signifie-t-il que C ++ est le berceau et le tout des langages? Bien sûr que non. Vous devez encore mesurer les compromis. Les personnes ignorantes utiliseront C ++ alors qu'elles auraient dû écrire un script Bash / Perl / Python. Les débutants en C ++ déclencheurs créeront des classes imbriquées profondes avec un héritage multiple virtuel avant que vous puissiez les arrêter et les envoyer. Ils utiliseront la méta-programmation Boost complexe avant de se rendre compte que cela n'est pas nécessaire. Ils utiliseront ENCORE char*
, strcmp
et les macros, au lieu de std::string
et des modèles.
Mais cela ne dit rien de plus que… regarde avec qui tu travailles. Il n'y a pas de langage pour vous protéger des utilisateurs incompétents (non, pas même Java).
Continuez à étudier et à utiliser le C ++ - mais évitez de sur-concevoir.