Я бачу занадто багато програмістів на C, які ненавидять C ++. Мені знадобилося досить багато часу (років), щоб повільно зрозуміти, що добре, а що погано. Я думаю, що найкращий спосіб сформулювати це:
Менший код, відсутність накладних витрат, більша безпека.
Чим менше коду ми пишемо, тим краще. Це швидко стає зрозумілим у всіх інженерів, які прагнуть до досконалості. Ви виправляєте помилку в одному місці, не дуже багато - ви висловлюєте алгоритм один раз і повторно використовуєте його в багатьох місцях і т. Д. У греків навіть є приказка, простежена до стародавніх спартанців: "сказати щось менше словами, означає що ти мудрий у цьому ". І справа в тому, що при правильному використанні C ++ дозволяє висловити себе в набагато меншому коді, ніж на C, не витрачаючи на швидкість виконання, при цьому більш безпечно (тобто вловлюючи більше помилок під час компіляції), ніж C.
Ось спрощений приклад мого рендерінга : При інтерполяції значень пікселів по лінії трикутника. Я повинен почати з координати X x1 і досягти координати X x2 (зліва в праву частину трикутника). І через кожен крок, через кожен піксель, який я передаваю, я повинен інтерполювати значення.
Коли я інтерполюю зовнішнє світло, яке досягає пікселя:
typedef struct tagPixelDataAmbient {
int x;
float ambientLight;
} PixelDataAmbient;
...
// inner loop
currentPixel.ambientLight += dv;
Коли я інтерполюю колір (називається відтінком "Горо", де поля "червоний", "зелений" та "синій" інтерполюються на значення кроку на кожному пікселі):
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;
Коли я рендерую в затінку "Phong", я більше не інтерполюю інтенсивність (ambientLight) або колір (червоний / зелений / синій) - я інтерполюю нормальний вектор (nx, ny, nz), і на кожному кроці я повинен повторно -розрахувати рівняння освітлення на основі інтерпольованого нормального вектора:
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;
Тепер першим інстинктом програмістів на C було б "хек, запишіть три функції, які інтерполюють значення, і викликайте їх залежно від заданого режиму". Перш за все, це означає, що у мене є проблема типу - з чим я працюю? Чи мої пікселі PixelDataAmbient? PixelDataGouraud? PixelDataPhong? О, чекайте, каже ефективний програміст C, використовуйте союз!
typedef union tagSuperPixel {
PixelDataAmbient a;
PixelDataGouraud g;
PixelDataPhong p;
} SuperPixel;
..і тоді ви маєте функцію ...
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;
}
...
Чи відчуваєте ви, що хаос прослизає?
Перш за все, одна помилка - це все, що потрібно для збою мого коду, оскільки компілятор ніколи не зупинить мене в розділі функції "Gouraud", щоб фактично отримати доступ до ".a". (навколишнє) значення. Помилка, яка не потрапила в систему типу C (тобто під час компіляції), означає помилку, яка виявляється під час виконання і вимагатиме налагодження. Ви помітили, що я отримую доступ left.a.green
до обчислення "дгрена"? Компілятор напевно не сказав вам цього.
Потім повсюдно є повторення - for
цикл існує стільки разів, скільки є режими візуалізації, ми продовжуємо робити "правий мінус лівий, розділений на кроки". Некрасивий і схильний до помилок. Ви помітили, що я порівнюю, використовуючи "i" в циклі Гуро, коли я повинен був використовувати "j"? Компілятор знову мовчить.
А як щодо if / else / драбин для режимів? Що робити, якщо я додам новий режим візуалізації через три тижні? Чи пам’ятаю я обробляти новий режим у всьому «if mode ==» у всьому моєму коді?
Тепер порівняйте вищевказану потворність із цим набором структур C ++ та функцією шаблону:
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;
}
}
А тепер подивіться на це. Ми більше не робимо союз типу суп: у кожного режиму є конкретні типи. Вони повторно використовують свої загальні речі (поле "х"), успадкувавши від базового класу ( CommonPixelData
). І шаблон змушує компілятор СТВОРИТИ (тобто генерувати код) три різні функції, які ми написали б самі в C, але в той же час, будучи дуже суворими щодо типів!
Наш цикл у шаблоні не може шукати та отримувати доступ до недійсних полів - компілятор буде бракувати, якщо ми це зробимо.
Шаблон виконує загальну роботу (цикл, щоразу збільшуючись на "крок"), і може робити це таким чином, що просто НЕ МОЖЕ викликати помилки виконання. Інтерполяція по типу ( AmbientPixelData
, GouraudPixelData
, PhongPixelData
) робляться з , operator+=()
що ми додамо в структурах - які в основному диктують , як інтерполюються кожен тип.
І чи бачите ви, що ми робили з WorkOnPixel <T>? Ми хочемо робити різні роботи для кожного типу? Ми просто називаємо шаблонну спеціалізацію:
void WorkOnPixel<AmbientPixelData>(AmbientPixelData& p)
{
// use the p.ambientLight field
}
void WorkOnPixel<GouraudPixelData>(GouraudPixelData& p)
{
// use the p.red/green/blue fields
}
Тобто - функція викликати, вирішується виходячи з типу. Під час компіляції!
Щоб перефразовувати його ще раз:
- ми мінімізуємо код (за допомогою шаблону), використовуючи загальні частини,
- ми не використовуємо некрасиві хаки, ми тримаємо сувору систему типу, щоб компілятор міг перевіряти нас у будь-який час.
- і найкраще: жодне з того, що ми зробили, не впливає на виконання будь-якого часу. Цей код запуститься ДІЙКІСТЬ так само швидко, як і еквівалентний код C - адже, якщо C код використовував функціональні покажчики для виклику різних
WorkOnPixel
версій, код C ++ буде Швидше C, оскільки компілятор буде вбудовувати WorkOnPixel
спеціалізовану спеціалізацію шаблону дзвоніть!
Менший код, відсутність накладних витрат, більша безпека.
Чи означає це, що C ++ - це мовна система для всіх і кінцевих? Звичайно, ні. Ви все ще повинні оцінювати компроміси. Невідомі люди використовуватимуть C ++ тоді, коли вони повинні були написати сценарій Bash / Perl / Python. Новачки C ++, що спричиняють тригери, створять глибокі вкладені класи з віртуальним багатократним успадкуванням, перш ніж ви зможете зупинити їх та відправити їх упаковкою. Вони будуть використовувати складні метапрограмування Boost, перш ніж зрозуміти, що це не потрібно. Вони будуть по- , як і раніше використовувати char*
, strcmp
і макроси, а std::string
й шаблони.
Але це не означає нічого іншого, як ... дивіться, з ким ви працюєте. Немає мови, щоб захистити вас від некомпетентних користувачів (ні, навіть Java).
Продовжуйте вивчати та використовувати C ++ - просто не передумуйте.