Original article: http://www.stroustrup.com/bs_faq2.html

Bjarne Stroustrup’s. Стиль і техніка C + +. Питання та відповіді

Це питання про стиль і техніку C++, які люди запитують мене найчастіше. Якщо у вас є кращі запитання або коментарі до відповідей, не соромтеся, напишіть мені ([email protected]). Будь ласка, пам’ятайте, що я не можу витрачати весь свій час на вдосконалення своїх домашніх сторінок.
З приводу більш загальних питань, див. мої “Загальні питання та відповіді”.

З приводу термінів і понять, див. мій “Глосарій C++“.

Будь ласка, зверніть увагу, що це просто набір питань і відповідей. Вони не замінять ретельно відібрані послідовності прикладів і пояснень, які можна знайти в хорошому підручнику. Також вони не дають докладні і точні специфікації, які ви знайдете в довіднику або стандарті. Дивіться “Дизайн і еволюція C++” для питань, пов’язаних з дизайном C++. Дивіться “Мова програмування C++” для питань про використання C++ і його стандартні бібліотеки.

Теми:

-Приступаючи до роботи

-Класи

-Ієрархія

-Шаблони і узагальнене програмування

-Пам’ять

-Винятки

-Інші особливості мови

-Інші деталі та стиль

Приступаючи до роботи:

-Як написати цю дуже просту програму?

-Чи можете ви порекомендувати стандарт кодування?

-Як прочитати рядок з введених даних?

-Як перетворити ціле число в рядок?

Класи
:
-Як об’єкти C++ позначаються в пам’яті?

-Чому “this” не є посиланням?

-Чому розмір порожнього класу не нуль?

-Як визначити постійну в класі?

-Чому не деструктор викликається в кінці області?

-“Друг” порушує інкапсуляції?

-Чому мій конструктор не працює?

Ієрархії класів
:
-Чому моя компіляція займає стільки часу?

-Чому я повинен помістити дані в своїх описах класу?

-Чому функції-члени не віртуальні за замовчуванням?

-Чому у нас немає віртуальних конструкторів?

-Чому деструктори не віртуальні за замовчуванням?

-Що таке чисто віртуальна функція?

-Чому у C++ немає остаточного ключового слова?

-Чи можу я викликати віртуальну функцію з конструктора?

-Чи можу я завадити людям отримувати дані з мого класу?

-Чому у C++ немає об’єкту універсального класу?

-А чи потрібне нам множинне наслідування?

-Чому не перевантажується робота для похідних класів?

-Чи можу я використовувати “new”, як і в мові Java?

Шаблони і узагальнене програмування
:
-Чому я не можу визначити обмеження для шаблонних параметрів?

-Чому я не можу призначити вектор <Apple>(яблуко) вектору <Fruit>(фрукт)?

-Чи “узагальнення” є тим, чим повинні були шаблони?

-Для чого використовувати sort(), коли у нас є “старий добрий qsort ()”?

-Що таке об’єкт функції?

-Що таке auto_ptr і чому там немає auto_array?

-Чому у C++ немає гетерогенних контейнерів?

-Чому стандартні контейнери такі повільні?

Пам’ять
:
-Як боротися з витоками пам’яті?

-Чому у C++ немає еквівалента realloc()?

-У чому різниця між new і malloc()?

-Чи можна змішувати стилі С і С++ у процесі виділення і зняття виділення?

-Чому я повинен використовувати приведення для перетворення з void *?

-Чи є “видалення розміщення”
?
-Чому не видалити нуль з його операнду?

-Що сталося з масивами?

Винятки
:
-Навіщо використовувати винятки?

-Як я можу використовувати винятки?

-Чому я не можу відновити роботу після знаходження винятку?

-Чому у C++ немає “остаточної” конструкції?

-Чи можу я запустити виняток з конструктора? З деструктора?

-Для чого не потрібно використовувати винятки?

Інші особливості мови
:
-Чи можу я написати “void main()”?

-Чому я не можу перевантажувати dot,::, sizeof, тощо?

-Чи можу я визначити свої оператори?

-Як викликати функцію C з C++?

-Як викликати функцію C++ з С?

-Чому у C++ є і покажчики, і посилання?

-Я повинен використовувати NolL чи 0?

-Яке значення у i+++ i++?

-Чому дещо залишилося невизначеним у C++?

-Що хорошого в static_cast?

-Отже, що не так при використанні макросів?

Інші деталі і стиль
:
-Як ви вимовляєте “cout”?

-Як ви вимовляєте “char”?

-Правильно “int* p;” чи “int *p;”?

-Який стиль макету найкраще підходить для мого коду?

-Як ви називаєте змінні? Чи рекомендуєте ви “угорські назви”?

-Я повинен використовувати для виклику посилання “call-by-value” чи “call-by-reference”?

-Потрібно ставити “const” до або після типу?


Як написати цю дуже просту програму?

Часто, особливо на початку семестру, я отримую багато питань про те, як писати дуже прості програми. Як правило, суть проблеми – це ввести кілька чисел, щось зробити з ними, і написати відповідь. Ось приклад програми, яка робить це:

        #include<iostream>
        #include<vector>
        #include<algorithm>
        using namespace std;
        int main()
        {
               vector<double> v;
               double d;
               while(cin>>d) v.push_back(d); //read elements
               if (!cin.eof()) {             //check if input failed
                       cerr << "format errorn";
                       return 1;     //error return
               }
               cout << "read " << v.size() << " elementsn";
               reverse(v.begin(),v.end());
               cout << "elements in reverse order:n";
               for (int i = 0; i<v.size();++i) cout << v[i] << 'n';
               return 0;//success return
        }

Ось кілька зауважень з приводу цієї програми:

- Це програма Стандарту ISO C++ з використанням стандартної бібліотеки. Стандартні бібліотеки об’єктів описані в просторі імен std в заголовках без суфіксу.h.

- Якщо ви хочете виконати її на машині з Windows, вам потрібно скомпілювати її як “консольний додаток”. Не забудьте дати вашому файлу з вихідними даними суфікс.cpp або компілятор може думати, що це джерело C (не C++).

- Так, main() перетворюється на int.

- Зчитування в стандартному векторі гарантує, що ви не переповнити ніякий буфер. Зчитування в масив без “дурних помилок” не під силу абсолютним новачкам – до того часу, як ви отримаєте це право, ви вже не є новачком. Якщо ви сумніваєтеся в цьому твердженні, я пропоную вам прочитати мою статтю “Вивчення стандарту C++ в якості нової мови”, яку можна завантажити з мого списку публікацій.

-!cin.eof() – це перевірка формату потоку. Зокрема, вона перевіряє, чи цикл закінчився, дійшовши до кінця файлу (якщо ні, то ви не отримаєте дані очікуваного типу/формату). Для отримання додаткової інформації, подивіться “стан потоку” в підручнику C++.

- Вектор знає свій розмір, так що я не повинен рахувати елементи.

- Так, я знаю, що маю оголосити, що i є вектором <double>::size_type, а не просто int, щоб уникнути попереджень від деяких гіпер-підозрілий компіляторів, але в даному випадку, я вважаю, що це є занадто педантичним і відволікає.

- Ця програма не містить явного управління пам’яттю, і це призводить до витоку пам’яті. Вектор використовує пам’ять для зберігання використаних елементів. Коли вектору потрібно більше пам’яті для елементів, він виділяє більше, коли вектор виходить за межі, він звільняє цю пам’ять. Таким чином, користувачеві не потрібно мати справу з виділенням і звільненням пам’яті для векторних елементів.

- Для читання рядків див. “Як прочитати рядок з введених даних?

- Програма закінчує зчитувані вхідні дані, коли бачить “кінець файла”. Якщо ви запускаєте програму з клавіатури на машині з Unix, “кінець файла” – це Ctrl-D. Якщо ви працюєте на машині з Windows, що у зв’язку з помилкою не розпізнає символ “кінець файлу”, то краще надати перевагу трохи складнішому варіанту програми, яка завершує введення даних зі словом “кінець”:

        #include<iostream>
        #include<vector>
        #include<algorithm>
        #include<string>
        using namespace std;

        int main()
        {
               vector<double> v;

               double d;
               while(cin>>d) v.push_back(d); //read elements
               if (!cin.eof()) {             //check if input failed
                       cin.clear();          //clear error state
                       string s;
                       cin >> s;             //look for terminator string
                       if (s!= "end") {
                               cerr << "format errorn";
                               return 1;     //error return
                       }
               }

               cout << "read " << v.size() << " elementsn";

               reverse(v.begin(),v.end());
               cout << "elements in reverse order:n";
               for (int i = 0; i<v.size();++i) cout << v[i] << 'n';

               return 0;//success return
        }

Додаткові приклади того, як використовувати стандартну бібліотеку, щоб зробити прості речі простішими, див. “Подорож стандартною бібліотекою”, глава TC++PL3 (доступна для завантаження).

Чи можете ви порекомендувати стандарт кодування?

Суть стандарту кодування C++ полягає в наданні набору правил для використання C++ для конкретних цілей в певному середовищі. З цього випливає, що не може бути одного стандарту кодування для всіх видів і всіх користувачів. Для певного додатка (або компанії, області застосування і т.д.), хороший стандарт кодування – це краще, ніж відсутність стандарту кодування. З іншого боку, я бачив багато прикладів, які показують, що поганий стандарт кодування – це гірше, ніж відсутність стандарту кодування.
Будь ласка, виберайте ваші правила з обережністю і з твердим знанням вашій області застосування. Деякі з найгірших стандартів кодування (не буду називати імен, “щоб захистити винних”) були написані людьми без твердих знань про C++, разом з відносним незнанням області застосування (вони були більше “експертами”, ніж розробниками), і помилковим переконанням, що додаткові обмеження обов’язково є кращим варіантом. Гарним прикладом останньої такої помилки є те, що існують деякі функції для допомогти програмістам використовувати ще гірші функції. У будь-якому випадку, пам’ятайте, що безпека, продуктивність і т.д. є сумою всіх частин процесу проектування та розробки – а не індивідуальних особливостей мови, або навіть цілих мов.
З цими застереженнями, я рекомендую три речі:
-Подивіться книгу Саттер і Александреску: “Стандарти кодування C++” (Addison-Wesley, ISBN 0-321-11358). У ній є хороші правила, але дивіться на неї перш за все як набір мета-правил. Тобто, вважайте її прикладом того як повинен виглядати хороший, точний набір правил кодування. Якщо ви пишете стандарт кодування, ви ігноруєте цю книгу на свій страх і ризик.
-Подивіться на стандарти кодування C++ JSF для повітряних апаратів”. Я вважаю, це дуже хороший набір правил для коду, критичного з точку зору безпеки і продуктивності. Якщо ви використовуєте вбудовувані системи програмування, ви повинні це взяти до уваги. Застереження: я доклав руку до розробки цих правил, так що ви можете вважати мене упередженим. З іншого боку, будь ласка, надішліть мені конструктивні зауваження з цього приводу. Такі коментарі можуть привести до покращення – всі хороші стандарти регулярно переглядається і оновлюється на основі досвіду та змін в робочому середовищі. Якщо ви не створюєте жорстких систем в реальному часі та систем, критичних з точки зору безпеки, ви вважатимете ці правила надмірно обмежувальними – тому що в адному випадку ці правила не для вас (принаймні, не всі з цих правил).
-Не використовуйте стандарти кодування C (навіть дещо модифіковані для C++) і не використовуйте стандарти кодування C++ десятирічної давності (навіть якщо вони були хорошими на той час). C++ не є (просто) C, і стандарти бібліотеки C++ не є (просто) попередніми стандартами C++.

Як прочитати рядок з введених даних?

Ви можете прочитати одне, виділене пробілами слово так:

        #include<iostream>
        #include<string>
        using namespace std;

        int main()
        {
               cout << "Please enter a word:n";

               string s;
               cin>>s;

               cout << "You entered " << s << 'n';
        }

Зверніть увагу, що немає явного управління пам’яттю і немає фіксованого розміру буфера, який ви, можете переповнити.
Якщо вам дійсно потрібний цілий рядок (а не тільки одне слово), ви можете зробити наступне:

        #include<iostream>
        #include<string>
        using namespace std;

        int main()
        {
               cout << "Please enter a line:n";

               string s;
               getline(cin,s);

               cout << "You entered " << s << 'n';
        }

Для короткого вступу в стандартні бібліотеки об’єктів, такі як iostream і string, див. Главу 3 TC++PL3 (доступна онлайн). Для більш детального порівняння простого використання C і C++ для введення даних, див. “Вивченная стандарту C++ як нової мови”, який можна завантажити з мого списку публікацій.

Як перетворити ціле число в рядок?

Найпростіше це зробити, використавши потік рядків:


        #include<iostream>
        #include<string>
        #include<sstream>
        using namespace std;

        string itos(int i)    //convert int to string
        {
               stringstream s;
               s << i;
               return s.str();
        }

        int main()
        {
               int i = 127;
               string ss = itos(i);
               const char* p = ss.c_str();

               cout << ss << " " << p << "n";
        }

Зазвичай, цей метод працює для перетворення будь-якого типу, який можна використовувати для виведення даних з допомогою << в рядку. Для опису потоків рядків, див. п. 21.5.3 книги “Мова програмування C++“.

Чому моя компіляція займає стільки часу?

Ви можете мати проблеми з компілятором. Він може бути старим, ви могли встановити його невірно, або комп’ютер може бути старовинним. Я не можу допомогти вам з такими проблемами.

Однак, більш ймовірно, що програма, яку ви намагаєтеся зібрати погано розроблена, так що компіляція змушує компілятор вивчати сотні заголовків файлів і десятки тисяч рядків коду. В принципі, цього можна уникнути. Якщо ця проблема в розробці бібліотеки вашого постачальника, ви не так багато можете зробити (за винятком зміни на кращу бібліотеку/постачальників), але ви можете організувати свій власний код, щоб звести до мінімуму перекомпіляції після змін. Проекти, які це роблять, як правило, кращі, зручніші, тому що вони відображать кращий поділ інтересів.

Розглянемо класичний приклад об’єктно-орієнтованих програм:

        class Shape {
        public:       //interface to users of Shapes
               virtual void draw() const;
               virtual void rotate(int degrees);
              //...
        protected:    //common data (for implementers of Shapes)
               Point center;
               Color col;
              //...
        };

        class Circle: public Shape {
        public:
               void draw() const;
               void rotate(int) { }
              //...
        protected:
               int radius;
              //...
        };

        class Triangle: public Shape {
        public:
               void draw() const;
               void rotate(int);
              //...
        protected:
               Point a, b, c;
              //...
        };

Ідея полягає в тому, щоб користувачі маніпулювали формами через відкритий інтерфейс форми, і щоб виконавці похідних класів (наприклад, коло і трикутник) поділяли аспекти виконання, представлені захищеними членами.
Є три серйозні проблеми, пов’язані з цією, здавалося б, простою ідеєю:
-Це не просто визначити спільні аспекти виконня, які будуть корисними для всіх похідних класів. З цієї причини набір захищених членів, швидше за все, потребуватиме змін набагато частіше, ніж відкритий інтерфейс. Наприклад, незважаючи на те, що “центр” є, можливо, основним поняттям для всіх форм, безглуздим є збереження точки “центр” для трикутника – для трикутників має сенс розрахувати центр, якщо і тільки якщо хтось висловлює зацікавленість у ньому.
-Захищені члени, швидше за все, залежатимуть від “реалізації” деталей, від яких користувачам форми не хотілося б залежати. Наприклад, багато (більшість?) кодів, які використовують форму будуть логічно незалежними від визначення “колір”, але наявність кольору в визначенні форми, ймовірно, буде потрібним для складання заголовків файлів і визначення поняття кольору операційної системи.
-Коли щось змінюється у захищеній частині, користувачам форми доведеться перекомпілювати – хоча тільки виконавці похідних класи мають доступ до захищених членів.
Таким чином, наявність “корисної для виконавців інформації” в базовому класі, який також виступає в якості інтерфейсу для користувачів, є джерелом нестабільності в реалізації, помилкових перекомпіляцій коду користувача (при зміні реалізації інформації), і надлишку заголовків файлів в призначеному для користувача коді (тому що “корисній для виконавців інформації” потрібні ці заголовки). Це явище іноді називають “хиткою проблемою базового класу”.
Очевидне рішення полягає в виключенні ” корисної для виконавців інформації” для класів, які використовуються в якості інтерфейсів для користувачів. Тобто, щоб зробити інтерфейс, чистий інтерфейс. Тобто, щоб представити інтерфейс у вигляді абстрактних класів:

        class Shape {
        public:       //interface to users of Shapes
               virtual void draw() const = 0;
               virtual void rotate(int degrees) = 0;
               virtual Point center() const = 0;
              //...

              //no data
        };

        class Circle: public Shape {
        public:
               void draw() const;
               void rotate(int) { }
               Point center() const { return cent; }
              //...
        protected:
               Point cent;
               Color col;
               int radius;
              //...
        };

        class Triangle: public Shape {
        public:
               void draw() const;
               void rotate(int);
               Point center() const;
              //...
        protected:
               Color col;
               Point a, b, c;
              //...
        };

Користувачі тепер ізольовані від змін в реалізації похідних класів. Я бачив як ця техніка зменшувала витрати часу на порядок величин.
Але що робити, якщо дійсно є деяка інформація, яка є спільною для всіх похідних класів (або просто кількох похідних класів)? Просто зробити цю інформацію класом і отримати реалізацію класу також:

        class Shape {
        public:       //interface to users of Shapes
               virtual void draw() const = 0;
               virtual void rotate(int degrees) = 0;
               virtual Point center() const = 0;
              //...

              //no data
        };

        struct Common {
               Color col;
              //...
        };

        class Circle: public Shape, protected Common {
        public:
               void draw() const;
               void rotate(int) { }
               Point center() const { return cent; }
              //...
        protected:
               Point cent;
               int radius;
        };

        class Triangle: public Shape, protected Common {
        public:
               void draw() const;
               void rotate(int);
               Point center() const;
              //...
        protected:
               Point a, b, c;
        };

Чому розмір порожнього класу не нуль?

Щоб переконатися, що адреси двох різних об’єктів будуть різними. З тієї ж причини, “нове” завжди повертає покажчик на різні об’єкти. Подумайте:


        class Empty { };

        void f()
        {
               Empty a, b;
               if (&a == &b) cout << "impossible: report error to compiler supplier";

               Empty* p1 = new Empty;
               Empty* p2 = new Empty;
               if (p1 == p2) cout << "impossible: report error to compiler supplier";
        }

Існує цікаве правило, яке говорить, що порожній базовий клас не повинен бути представлений окремим байтом:


        struct X: Empty {
               int a;
              //...
        };

        void f(X* p)
        {
               void* p1 = p;
               void* p2 = &p->a;
               if (p1 == p2) cout << "nice: good optimizer";
        }

Ця оптимізація є безпечною і може бути найбільш корисною. Вона дозволяє програмістам використовувати порожні класи для представлення дуже простих понять без додаткових витрат. Деякі сучасні компілятори підтримують цю “оптимізацію порожнього базового класу”.

Чому я повинен помістити дані в своїх описах класу?

Ви не повинні. Якщо ви не хочете бачити дані в інтерфейсі, не вставляйте їх в клас, який визначає інтерфейс, а натомість розмістіть їх в похідних класах. Див. “Чому моя компіляція займає стільки часу?“.

Іноді, ви дійсно хочете мати представлення даних у класі. Розглянемо класовий комплекс:

        template<class Scalar> class complex {
        public:
               complex(): re(0), im(0) { }
               complex(Scalar r): re(r), im(0) { }
               complex(Scalar r, Scalar i): re(r), im(i) { }
              //...

               complex& operator+=(const complex& a)
                       { re+=a.re; im+=a.im; return *this; }
              //...
        private:
               Scalar re, im;
        };

Цей тип призначений для використання здебільшого по вбудованому принципу, і у описі має бути пояснення, щоб уможливити створення справді локальних об’єктів (тобто об’єктів, розміщенних в стеку, а не вгорі) і для забезпечення належного вбудовування простих операцій. Істинно локальні об’єкти і вбудовування необхідні для виконання комплексу, близького до того, що міститься в мовах з вбудованим комплексним типом.

Чому функції-члени не віртуальні за замовчуванням?

Тому що більшість класів не призначені для використання в якості базових класів. Див., наприклад, класовий комплекс.

Крім того, об’єктам класу з віртуальною функції потрібен простір, необхідний віртуальному механізму виклику функцій – як правило, одне слово на об’єкт. Ці витрати можуть бути значними, і може встати на шляху сумісності з даними з інших мов (наприклад, C і Fortran).

Дивіться “Дизайн і еволюція C++” для більшого обгрунтування дизайну.

Чому деструктори не віртуальні за замовчуванням?

Тому що більшість класів не призначені для використання в якості базових класів. Віртуальні функції мають сенс тільки в классах, які призначені для використання в якості інтерфейсу до об’єктів похідних класів (як правило, виділені зверху і доступні через покажчики або посилання).

Отже, коли слід зробити деструктор віртуальним? Кожного раза, коли клас має хоча б одну віртуальну функцію. Наявність віртуальних функцій показує, що клас призначений для роботи в якості інтерфейсу для похідних класів, і коли він таким і є, об’єкт похідного класу може бути знищений через покажчик до бази. Наприклад:

        class Base {
              //...
               virtual ~Base();
        };

        class Derived: public Base {
              //...
               ~Derived();
        };

        void f()
        {
               Base* p = new Derived;
               delete p;     //virtual destructor used to ensure that ~Derived is called
        }

Якби деструктор бази не був віртуальним, деструктор похідних не можна було б викликати, і, ймовірно, отримати поганий ефект, такий як невивільнення ресурсів, що належать похідній

Чому у нас немає віртуальних конструкторів?

Віртуальний виклик являє собою механізм для виконня певної часткової інформації. Зокрема, “віртуальний” дозволяє викликати функцію, знаючи тільки інтерфейс, а не точний тип об’єкта. Щоб створити об’єкт, потрібна повна інформація. Зокрема, ви повинні знати точний тип, що ви хочете створити. Отже, “виклик конструктора” не може бути віртуальним.

Методи непрямого використання, коли ви хочете створити об’єкт, часто називають “Віртуальним конструктором”. Див, наприклад, п. 15.6.2 “TC++ PL3″.

Наприклад, ось техніка для створення об’єкта відповідного типу, використовуючи абстрактний клас:

        struct F {    //interface to object creation functions
               virtual A* make_an_A() const = 0;
               virtual B* make_a_B() const = 0;
        };

        void user(const F& fac)
        {
               A* p = fac.make_an_A();       //make an A of the appropriate type
               B* q = fac.make_a_B();//make a B of the appropriate type
              //...
        }

        struct FX: F {
               A* make_an_A() const { return new AX();       }//AX is derived from A
               B* make_a_B() const { return new BX();        }//BX is derived from B
        };

        struct FY: F {
               A* make_an_A() const { return new AY();       }//AY is derived from A
               B* make_a_B() const { return new BY();        }//BY is derived from B
        };

        int main()
        {
               FX x;
               FY y;
               user(x);      //this user makes AXs and BXs
               user(y);      //this user makes AYs and BYs

               user(FX());   //this user makes AXs and BXs
               user(FY());   //this user makes AYs and BYs
              //...
        }

Це варіант, що часто називають “фабрикою шаблонів”. Справа в тому, що user() повністю ізольований від знання таких класів як AX і AY.

Що таке чисто віртуальна функція?

Чисто віртуальна функція є функцією, яка повинна бути підмінена в похідному класі, і не повинна бути визначена. Віртуальна функція називається “чистою”, якщо використовується цікавий синтаксис “= 0″. Наприклад:

        public:
               void f1();            //not virtual
               virtual void f2();    //virtual, not pure
               virtual void f3() = 0;//pure virtual
        };

        Base b;//error: pure virtual f3 not overridden

Тут Base являє собою абстрактний клас (тому що вона має чисто віртуальні функції), тому ніяких об’єктів базового класу не може бути створено безпосередньо: Base (явно) має бути базовим класом. Наприклад:


        class Derived: public Base {
              //no f1: fine
              //no f2: fine, we inherit Base::f2
               void f3();
        };

        Derived d;    //ok: Derived::f3 overrides Base::f3

Абстрактні класи є надзвичайно корисними для визначення інтерфейсів. Насправді, клас з чисто віртуальними функціями часто називають інтерфейсом.
Ви можете визначити чисто віртуальні функції:

        Base::f3() {/*... */}

Час від часу це дуже корисно (для реалізації простих загальних деталей для похідних класів), але Base:: f3() все одно повинна бути змінена в деяких похідних класах.
Якщо ви не зміните чисто віртуальну функцію в похідному класі, що похідний клас стає абстрактним:

        class D2: public Base {
              //no f1: fine
              //no f2: fine, we inherit Base::f2
              //no f3: fine, but D2 is therefore still abstract
        };

        D2 d;  //error: pure virtual Base::f3 not overridden

Чому не перевантажується робота для похідних класів?

Це питання (в різних варіантах), як правило, викладено ось так:


        #include<iostream>
        using namespace std;

        class B {
        public:
               int f(int i) { cout << "f(int): "; return i+1; }
              //...
        };

        class D: public B {
        public:
               double f(double d) { cout << "f(double): "; return d+1.3; }
              //...
        };

        int main()
        {
               D* pd = new D;

               cout << pd->f(2) << 'n';
               cout << pd->f(2.3) << 'n';
        }

який буде виконувати:

f(double): 3.3
f(double): 3.6

а не

f(int): 3
f(double): 3.6

як здогадалися (помилково) деякі люди.
Іншими словами, немає дозволу на перевантаження між D і B. Компілятор шукає в області D, знаходить єдину функцію “double f(double)” і викликає її. Вона ніколи не заважає області (вбудованій) B. В C++ немає перевантаження між областями – області похідного класу не є винятком із цього загального правила. (Див. “D & E” або “TC++ PL3” детальніше).
Але якщо я хочу створити перевантажених набір всіх моїх f() функцій з бази та похідного класу? Це легко зробити за допомогою використання наступного опису:

        class D: public B {
        public:
               using B::f;   //make every f from B available
               double f(double d) { cout << "f(double): "; return d+1.3; }
              //...
        };

З цими змінами на виході буде


f(int): 3
f(double): 3.6

Це означає, що дозвіл перевантаження було застосовано до B’s f() та D’s f(), щоб вибрати найбільш підходящу f() для виклику.

Чи можу я використовувати “new”, як і в мові Java?

Схоже на те, але не робіть цього наосліп і побачите чудові варіанти. Врахуйте:

        void compute(cmplx z, double d)
{
        cmplx z2 = z+d;//c++ style
        z2 = f(z2);           //use z2

        cmplx& z3 = *new cmplx(z+d);  //Java style (assuming Java coold overload+)
        z3 = f(z3);
        delete &z3;
}

Грубе використання “new” для z3 не є необхідним і повільним у порівнянні з ідіоматичним використанням локальних змінних (z2). Вам не потрібно використовувати “new”, щоб створити об’єкт, якщо видаєте з допомогою ” delete “, об’єкт в тій же області, такі об’єкт має бути локальною змінною.

Чи можу я викликати віртуальну функцію з конструктора?

Так, але будьте обережні. Він не може робити те, що Ви від нього очікуєте. У конструкторі віртуальний механізм виклику відключений, оскільки перевизначення в похідному класі до сих пір не відбулося. Об’єкти розташовуються з бази догори, “база перед похідними”.

Подумайте:

        #include<string>
        #include<iostream>
        using namespace std;

        class B {
        public:
               B(const string& ss) { cout << "B constructorn"; f(ss); }
               virtual void f(const string&) { cout << "B::fn";}
        };

        class D: public B {
        public:
               D(const string & ss):B(ss) { cout << "D constructorn";}
               void f(const string& ss) { cout << "D::fn"; s = ss; }
        private:
               string s;
        };

        int main()
        {
               D d("Hello");
        }

Програма компілює і виконує


        B constructor
        B::f
        D constructor

Зверніть увагу, не D::f. Розглянемо, що сталося б, якби правила були різні таким чином, що D::f() викликався з B::B(): Так як конструктор D::D() ще не запущений, D::f() буде пробувати призначити свій аргумент в неініціалізований рядок s. В результаті, швидше за все, негайно відбудеться збій.

Руйнування відбувається по принципу “похідний клас до базового класу”, тому віртуальні функції ведуть себе, як в конструкторі: використовуються тільки локальні визначення, і запити до перевизначених функцій не виконуються, щоб уникнути контакту (тепер пошкодженого) частина похідного класу об’єкта.

Для отримання додаткової інформації див. п. 13.2.4.2 “D & E” або п. 15.4.3 “TC++ PL3“.

Було висловлено припущення, що ця норма є перешкодою виконання. Це не так. Насправді, було б значно простіше виконати небезпечне правило виклику віртуальних функцій з конструктора так само, як у випадку з іншими функціями. Однак це означало б, що ніяка віртуальна функція не може бути записана, спираюсичь на інваріанти, встановлені базовими класами. Це спричинило б до страшного безладу.

Чи є “видалення розміщення” ?

Ні, але якщо вам це потрібно, то можете написати своє власне.
Розглянемо розміщення new, що використовується для розташування об’єктів у набір арен

        class Arena {
        public:
                void* allocate(size_t);
                void deallocate(void*);
               //...
        };

        void* operator new(size_t sz, Arena& a)
        {
                return a.allocate(sz);
        }

        Arena a1(some arguments);
        Arena a2(some arguments);

Виходячи з цього, ми можемо написати


        X* p1 = new(a1) X;
        Y* p2 = new(a1) Y;
        Z* p3 = new(a2) Z;
       //...

Але як ми потім видалити ці об’єкти правильно? Причиною того, що немає вбудованої функції ” placement delete “, що відповідала б розміщеному new, в тому, що не існує єдиного способу гарантувати, що вона буде використана правильно. Ніщо в системі C++ не дозволяє зробити висновок, що p1 вказує на об’єкт, виділений в Arena a1. Покажчик на будь-який X, виділений будь-де можна віднести до p1.
Однак іноді програміст знає, як це зробити:

        template<class T> void destroy(T* p, Arena& a)
        {
                if (p) {
                        p->~T();             //explicit destructor call
                        a.deallocate(p);
                }
        }

Тепер ми можемо написати:


        destroy(p1,a1);
        destroy(p2,a2);
        destroy(p3,a3);

Якщо Arena відстежуватиме, до яких об’єктів вона віднесена, можна навіть написати destroy (), щоб захистити себе від помилок.
Крім того, можна визначити відповідні пари операторів new() і delete() для ієрархії класів 15.6 “TC++ PL (SE)“. Див. також 10.4 “D&E” і 19.4.5 “TC++ PL (SE)“.

Чи можу я завадити людям отримувати дані з мого класу?

Так, але чому ви цього хочете? Є дві поширені відповіді:

-для підвищення ефективності: щоб уникнути віртуальних викликів моєї функції;
-для забезпечення безпеки: переконатись, що мій клас не використовується в якості базового класу (наприклад, щоб переконатися, що я можу скопіювати об’єкти, не побоюючись обрізання).

З мого досвіду, причина ефективності є, як правило, недоречним страхом. В C++, виклики віртуальних функцій відбуваються так швидко, що їх реальне використання для классу, розробленого з віртуальними функціями, не спричинює надмірних витрат часу, в порівнянні з альтернативними рішеннями, використовуючи звичайні виклики функцій. Зверніть увагу, що віртуальний механізм виклику функції зазвичай використовується тільки при виклику через покажчик або посилання. При виконанні функції безпосередньо на імені об’єкта, накладки віртуальної функції класу легко оптимізувати.

Якщо є реальна необхідність “покриття” ієрархії класів, щоб уникнути викликів віртуальних функцій, можна було б спочатку запитати, чому ці функції є віртуальними. Я бачив приклади, коли виконання критично важливих функцій було зроблено віртуальним без поважної причини, просто тому, що “так ми зазвичай робимо”.

Інший варіант цієї проблеми, як не допустити похідних з логічних причин, має рішення. На жаль, це рішення не є дуже гарним. Воно спирається на той факт, що більшість похідних класів в ієрархії повинні побудувати віртуальну базу. Наприклад:

        class Usable;

        class Usable_lock {
               friend class Usable;
        private:
               Usable_lock() {}
               Usable_lock(const Usable_lock&) {}
        };

        class Usable: public virtual Usable_lock {
              //...
        public:
               Usable();
               Usable(char*);
              //...
        };

        Usable a;

        class DD: public Usable { };

        DD dd; //error: DD::DD() cannot access
              //Usable_lock::Usable_lock(): private  member

(з п. 11.4.3 “D & E“).

Чому у C++ немає гетерогенних контейнерів?

Стандартна бібліотека C++ забезпеує набір корисних, статично типізованих та ефективних контейнерів. Прикладами можуть служити вектор, список і карта:

        vector<int> vi(10);
        vector<Shape*> vs;
        list<string> lst;
        list<double> l2
        map<string,Record*> tbl;
        map< Key,vector<Record*> > t2;

Ці контейнери описані у всіх хороших підручниках по C++, і їм слід надавати перевагу перед масивами і “саморобними” контейнерами, поки не знайдуться вагомі причини це зробити.
Ці контейнери є однорідними, тобто вони вміщують елементи одного і того ж типу. Якщо ви хочете контейнер для елементів різних типів, це потрібно виразити або у вигляді союзу, або (що, як правило, набагато краще) в якості контейнера покажчиків до поліморфного типу. Класичний приклад:

vector<Shape*> vi;    //vector of pointers to Shapes

Тут vi може містити елементи будь-якого типу, похідного від Shape. Тобто, vi однорідна в тому, що всі її елементи є формами (точніше, посиланнями на форми), і гетерогенна в тому сенсі, що vi може містити елементи найрізноманітніших форм, таких як кола, трикутники, і т.д.
Таким чином, в деякому сенсі всі контейнери (на будь-якій мові) однорідні, тому що для їх використання повинен бути єдиний інтерфейс до всіх елементів, на який можуть покластися користувачі. Мови, які забезпечують гетерогенні контейнери просто мають контейнери елементів, яким забезпечено стандартний інтерфейс. Наприклад, Java колекції забезпечують контейнери (посилання) об’єктів і ви використовуєте (зазвичай) інтерфейс об’єкту для дослідження справжніх типів елементів.
Стандартна бібліотека C++ забезпечує однорідні контейнери, бо це є простим у використанні в переважній більшості випадків, дає найкращі по часу повідомлення про помилку компіляції, і не вимагає жодних непотрібних додаткових витрат.
Якщо Вам потрібен гетерогенний контейнер в C++, визначте загальний інтерфейс для всіх елементів і зробіть з них контейнер. Наприклад:

class Io_obj {/*... */};   //the interface needed to take part in object I/O
vector<Io_obj*> vio;          //if you want to manage the pointers directly
vector< Handle<Io_obj> > v2;  //if you want a "smart pointer" to handle the objects

Не опускайтесь до найнижчого рівня виконання, лише якщо це необхідно:


vector<void*> memory; //rarely needed

Хорошою ознакою того, що ви “зайшли на занадто низький рівень” є те, що ваш код засмічується типами.
Використання будь-якого класу, наприклад, Boost::Any, може бути альтернативою в деяких програмах:

vector<Any> v;

Чому стандартні контейнери такі повільні?

Це не так. Ймовірно, більш правильна відповідь “у порівнянні з чим?”. Коли люди скаржаться на роботу контейнера стандартної бібліотеки, я зазвичай бачу одну з трьох справжніх проблем (або міфів і відволікаючих маневрів):
– У мене проблеми від втрат при копіюванні.
– У мене проблеми від повільної швидкості для пошуку таблиці
– Моя власноручні (нав’язливі) списки набагато швидші, ніж std::list.

Перед початком оптимізації, подумайте, чи є у вас реальна проблема продуктивності. У більшості випадків, які були мені надіслані, проблема продуктивності була теоретичною чи уявною: спочатку проведіть заміри, а потім оптимізуйте, тільки якщо це необхідно.
Давайте подивимося на ці проблеми по черзі. Часто вектор <X> повільніший, ніж чийсь спеціалізований My_container<X>, тому що My_container<X> реалізований як контейнер покажчиків на X. Стандартні контейнери зберігають копії значень, і копіюють це значення, коли ви поміщаете його в контейнер. Це несуттєво для невеликих значень, але може зовсім не підійти для великих об’єктів:

        vector<int> vi;
        vector<Image> vim;
       //...
        int i = 7;
        Image im("portrait.jpg");     //initialize image from file
       //...
        vi.push_back(i);      //put (a copy of) i into vi
        vim.push_back(im);    //put (a copy of) im into vim

Тепер, якщо portrait.jpg важить пару мегабайт і Image має семантичні значення (наприклад, призначення копіювання та структура копій), то vim.push_back(im) стане занадто дорогим. Але – як то кажуть – якщо ця дія приносить страждання, просто не робіть її. Замість цього, просто використовуйте контейнер з мітками або контейнер з покажчиками. Наприклад, якщо має семантичні посилання, наведений вище код вплине тільки на вартість виклику конструктора копій, яка була б банальною в порівнянні з більшістю операторів для обробки зображення. Якщо певний клас, скажімо, знову Image, дійсно має копії семантики з поважних причин, тоді розумним рішенням часто є контейнер покажчиків:


        vector<int> vi;
        vector<Image*> vim;
       //...
        Image im("portrait.jpg");     //initialize image from file
       //...
        vi.push_back(7);      //put (a copy of) 7 into vi
        vim.push_back(&im);   //put (a copy of) &im into vim

Природно, якщо ви використовуєте покажчики, ви повинні думати про управління ресурсами, але контейнери покажчиків самі по собі можуть бути ефективними і дешевими мітками ресурсом (часто вам потрібен контейнер з деструктором для видалення “привласнених” об’єктів).

Другою справжньою проблемою продуктивності, яка часто зустрічаються, є використання карти <string,X> для великого числа пар (string,X). Карти добре підходять для відносно невеликих контейнерів (скажімо, кілька сотень або кілька тисяч елементів – доступ до елемента карти, яка складається з 10000 елементів оцінюється в приблизно 9 порівнянь), де less-than не є обтяжливим, і де неможливо побудувати хорошу хеш-функцію. Якщо у вас є багато рядків і хороша хеш-функція, використовуйте хеш-таблиці. Unordered_map з технічного звіту стандартної комісії є зараз широко доступним і набагато кращим, ніж більшість саморобних.

Іноді, ви можете прискорити процес, використовуючи пари (const char*,X), а не (string,X) пар, але пам’ятайте, що < не робить лексикографічного порівняння для рядка у стилі C. Крім того, якщо X велике, ви можете мати також проблеми з копіюванням (їх можна вирішити одним зі звичайних способів).

Інтрузивні списки може бути дуже швидкими. Але подуймате, чи потрібен вам список взагалі: вектор є більш компактним, а тому меншим і швидшим у багатьох випадках – навіть тоді, коли ви робите вставки і стирання. Наприклад, якщо у вас логічно є список з декількох цілих елементів, вектор буде значно швидшим, ніж список (будь-який список). Крім того, інтрузивні списки не можуть безпосередньо містити вбудовані типи (а ціле число не має пов’язаного члена). Отже, припустимо, що вам дійсно потрібен список, і що ви можете забезпечити поле посилання для кожного типу елементів. Список стандартної бібліотеки за замовчуванням виконує виділення, та створення копії для кожної операції вставки елементів (та зняття виділення для кожної операції видалення елементу). Для std::list з виділенням за замовчуванням це може бути суттєвим. У відношенні невеликих елементів, де надлишкові витрати на копії не є суттєвими, слід подумати про використання оптимізованого виділення. Використовуйте власні інтрузивні списки тільки тоді, коли потрібні список і остання крапля продуктивності.

Люди іноді турбуються про ціну std::vector, яка поступово зростає. Раніше я про це турбувався і використовував reserve() для оптимізації росту. Після вимірювання мого коду і повторних проблем з пошуком покращення продуктивності з допомогою reserve() в реальних програмах, я перестав його використовувати, за винятком випадків, коли це необхідно, щоб уникнути скасування ітератора (рідкісний випадок в моєму коді). Ще раз: проведіть виміри перед оптимізацією.

“Друг” порушує інкапсуляції?

Ні, це не так. “Friend” є механізмом для надання доступу, так само, як і членство. Ви не можете (у стандартній програмі) надати собі доступ до класу, не змінюючи його джерела. Наприклад:

        class X {
               int i;
        public:
               void m();             //grant X::m() access
               friend void f(X&);    //grant f(X&) access
              //...
        };

        void X::m() { i++;/* X::m() can access X::i */}

        void f(X& x) { x.i++;/* f(X&) can access X::i */}

Для опис моделі захисту C++, див. п. 2.10 “D & E“ і п. 11.5, 15.3, і С.11 “TC++ PL“.

Чому мій конструктор не працює?

Це питання, яке надходить в багатьох формах. Наприклад:
Чому компілятор копіює мої об’єкти, коли я цього не хочу?
Як відключити копіювання?
Як зупинити неявні перетворення?
Як мій int перетворився на комплексне число?

За замовчуванням, у класа є конструктор копій та планувальник копій, які копіюють всі елементи. Наприклад:


        struct Point {
               int x,y;
               Point(int xx = 0, int yy = 0):x(xx), y(yy) { }
        };

        Point p1(1,2);
        Point p2 = p1;

Тут ми отримуємо p2.x==p1.x і p2.y==p1.y. Це часто саме те, чого ви хочете (і необхідно для сумісності з С), але зверніть увагу:


        class Handle {
        private:
               string name;
               X* p;
        public:
               Handle(string n)
                      :name(n), p(0) {/* acquire X called "name" and let p point to it */}
               ~Handle() { delete p;/* release X called "name" */}
              //...
        };

        void f(const string& hh)
        {
               Handle h1(hh);
               Handle h2 = h1;//leads to disaster!
              //...
        }

Тут, копія за замовчуванням має h2.name == h1.name і h2.p == h1.p. Це призводить до катастрофи: коли ми виходимо з f(), запускаються деструктори для h1 і h2, і об’єкт, на який вказують h1.p і h2.p двічі видаляється.
Як цього уникнути? Найпростішим рішенням є завадити копіюванню, виконуючи операції, які копіюють приховано:

class Handle {
        private:
               string name;
               X* p;

               Handle(const Handle&);//prevent copying
               Handle& operator=(const Handle&);
        public:
               Handle(string n)
                      :name(n), p(0) {/* acquire the X called "name" and let p point to it */}
               ~Handle() { delete p;/* release X called "name" */}
              //...
        };

        void f(const string& hh)
        {
               Handle h1(hh);
               Handle h2 = h1;//error (reported by compiler)
              //...
        }

Якщо нам потрібна копія, ми, звичайно, можеми призначити ініціалізатор копій і планувальник копій, щоб дотримуватись бажаної семантики.
Тепер повернемося до Point. У відношенні Point семантики копіювання за замовчуванням в порядку, але проблема в конструкторі:

struct Point {
               int x,y;
               Point(int xx = 0, int yy = 0):x(xx), y(yy) { }
        };

        void f(Point);

        void g()
        {
               Point orig;   //create orig with the defaolt value (0,0)
               Point p1(2);  //create p1 with the defaolt y-coordinate 0
               f(2);         //calls Point(2,0);
        }

Люди використовують аргументи за замовчуванням, щоб зручніше використовувати orig і p1. Потім, дехто здивований перетворення з 2 до Point(2,0) при виклику f(). Конструктор, який приймає один аргумент, відповідає за перетворення. За замовчуванням це неявне перетворення. Якщо для роботи потрібно, щоб таке перетворення було явним, то зробіть конструктор явним:

        struct Point {
               int x,y;
               explicit Point(int xx = 0, int yy = 0):x(xx), y(yy) { }
        };

        void f(Point);

        void g()
        {
               Point orig;   //create orig with the defaolt value (0,0)
               Point p1(2);  //create p1 with the defaolt y-coordinate 0
                              //that's an explicit call of the constructor
               f(2);         //error (attmpted implicit conversion)
               Point p2 = 2; //error (attmpted implicit conversion)
               Point p3 = Point(2);  //ok (explicit conversion)
        }

Чому у C++ є і покажчики, і посилання?

C++ успадкував покажчики C, тому я не зміг їх видалити, не викликавши серйозних проблем з сумісністю. Посилання корисні для кількох речей, але осовною причиною, чому я ввів їх в C++ була підтримка перевантаження операторів. Наприклад:


        void f1(const complex* x, const complex* y)  //without references
        {
               complex z = *x+*y;    //ugly
              //...
        }

        void f2(const complex& x, const complex& y)  //with references
        {
               complex z = x+y;      //better
              //...
        }

Загалом, якщо ви хочете мати функціональність і покажчиків, і посилань, вам необхідні або два різні типи (наприклад, в C++), або два різних набори операцій одного типу. Наприклад, з одним типом вам потрібна як операція призначення об’єкту, на який посилаються, та операція призначення посилання/покажчика. Це може бути зроблено за допомогою окремих операторів (як в Simola). Наприклад:


        Ref<My_type> r:- new My_type;
        r:= 7;               //assign to object
        r:- new My_type;     //assign to reference

Крім того, ви можете покластися на перевірку типів (перевантаження). Наприклад:


        Ref<My_type> r = new My_type;
        r = 7;                //assign to object
        r = new My_type;      //assign to reference


Я повинен використовувати для виклику посилання “call-by-value” чи “call-by-reference”?

Це залежить від того, що ви намагаєтеся досягти:
– Якщо ви хочете змінити переданий об’єкт, робіть виклик посиланням або використовуйте покажчик, наприклад, void f(X&); або void f(X*);
– Якщо ви не хочете змінювати переданий об’єкт і він є великим, робіть виклик посиланням постійної, наприклад: void f(const X&);
– В іншому випадку, робіть виклик за значенням, наприклад, void f(X).

Що я маю на увазі під словом “великий”? Все, що більше двох слів.
Чому б я хотів змінювати аргумент? Часто ми змушені це робити, але часто у нас є і альтернатива: створити нове значення. Зверніть увагу:

        void incr1(int& x);   //increment
        int incr2(int x);     //increment

        int v = 2;
        incr1(v);     //v becomes 3
        v = incr2(v); //v becomes 4

Я думаю, що для читача легше зрозуміти incr2(). Тобто incr1(), швидше за все, призведе до помилок. Таким чином, я б віддав перевагу стилю, який повертає нове значення, перед тим стилем, який змінює значення до тих пір, поки створення та копіювання нового значення не стане занадто дорогим. Якщо я дійсно хочу змінити аргумент, я повинен використовувати покажчик чи посилання? Мені невідома логічна причина для цього. Якщо передача “не об’єкту” (наприклад, нульового покажчика) є прийнятною, тоді використання покажчика має сенс. Мій особистий стиль полягає в тому, чщо я використовую покажчик, коли хочу змінити об’єкт, тому що в деяких контекстах таким чином легше виявити, що зміна можлива.

Відзначимо також, що виконання функції-члена є по суті викликом за посиланням до об’єкта, тому ми часто використовуємо функції-члени, коли хочемо змінити значення/стан об’єкта.

Чому “this”не є посиланням?

Тому що “this” було введено в C++ (а насправді в C з класами) до того, як були додані посилання. Крім того, я вибрав “this”, щоб продовжити використання Simola, а не (пізніше) використовувати “self” в Smalltalk.

Що сталося з масивами?

З точки зору часу і простору, масив є єдиною оптимальною конструкцією для доступу до послідовності об’єктів в пам’яті. Він, втім, є також дуже низьким рівнем структури даних з великим потенціалом до зловживань і помилок, і практично у всіх випадках є кращі альтернативи. Під “кращою”, я маю на увазі таку, яку легше писати, легше читати, допускає менше помилок і є швидшою.

Дві основні проблеми з масивами полягають в тому, що

– Масив не знає свого розміру
– Ім’я масиву перетворюється на покажчик свого першого елементу при найменшій провокації

Розглянемо кілька прикладів:


        void f(int a[], int s)
        {
              //do something with a; the size of a is s
               for (int i = 0; i<s;++i) a[i] = i;
        }

        int arr1[20];
        int arr2[10];

        void g()
        {
               f(arr1,20);
               f(arr2,20);
        }

Другий виклик буде бігати по всій пам’яті, яка не належить до arr2. Природно, що програміст зазвичай отримує правильний розмір, але це включає додаткову роботу, тому дуже часто люди роблять помилки. Я віддаю перевагу більш простій і чистій версії, використовуючи вектор стандартної бібліотеки:


        { void f(vector<int>& v)
        {
              //do something with v
               for (int i = 0; i<v.size();++i) v[i] = i;
        }

        vector<int> v1(20);
        vector<int> v2(10);

        void g()
        {
               f(v1);
               f(v2);
        }

Так як масив не знає свого розміру, не може бути розподілення масиву:

        void f(int a[], int b[], int size)
        {
               a = b; //not array assignment
               memcpy(a,b,size);     //a = b
              //...
        }

Знову ж таки, я вважаю кращим вектор:


        void g(vector<int>& a, vector<int>& b, int size)
        {
               a = b;
              //...
        }

Ще одна перевага вектора полягає в тому, що memcpy() не буде правильно працювати з елементами з конструкторами копій, такими як рядки.


        void f(string a[], string b[], int size)
        {
               a = b; //not array assignment
               memcpy(a,b,size);     //disaster
              //...
        }

        void g(vector<string>& a, vector<string>& b, int size)
        {
               a = b;
              //...
        }

Масив фіксованого розміру визначається під час компіляції:

        const int S = 10;

        void f(int s)
        {
               int a1[s];    //error
               int a2[S];    //ok

              //if I want to extend a2, I'll have to change to an array
              //allocated on free store using malloc() and use realloc()
              //...
        }

Для контрасту:


        const int S = 10;

        void g(int s)
        {
               vector<int> v1(s);    //ok
               vector<int> v2(S);    //ok
               v2.resize(v2.size()*2);
              //...
        }

C99 дозволяє змінювати межі масиву для локальних масивів, але дуже великі масиви мають свої власні проблеми.
Той спосіб, у який імена масивів “розкладаються” на покажчики, має фундаментальне значення для їх використання в C і C++. Тим не менш, розклад масиву дуже погано взаємодіє з наслідуванням. Зверніть увагу:

        class Base { void fct();/*... */};
        class Derived {/*... */};

        void f(Base* p, int sz)
        {
               for (int i=0; i<sz;++i) p[i].fct();
        }

        Base ab[20];
        Derived ad[20];

        void g()
        {
               f(ab,20);
               f(ad,20);     //disaster!
        }

В останньому виклику Derived[] розглядається як Base[], і підписка більше не працює правильно, якщо sizeof(Derived)!=sizeof(Base) – що і буде мати місце в більшості випадків. Якби натомість ми використовували вектори, то помилка була б виявлена під час компіляції:

        void f(vector<Base>& v)
        {
               for (int i=0; i<v.size();++i) v[i].fct();
        }

        vector<Base> ab(20);
        vector<Derived> ad(20);

        void g()
        {
               f(ab);
               f(ad); //error: cannot convert a vector<Derived> to a vector<Base>
        }

Я вважаю, що найбільша кількість помилок початківців програмування C і C++ відноситься до (невірного) використання масивів.

Чому у C++ немає остаточного ключового слова?

Тому що в цьому не було (і немає), здається, великої необхідності.

Я повинен використовувати NolL чи 0?

В C++ NolL дорівнює 0, так що тільки питання тільки в естетичному вигляді. Я віддаю перевагу уникненню макросів, тому я використовую 0. Ще одна проблема з NolL в тому, що іноді люди помилково вважають, що він відрізняється від 0 і/або не є цілим числом. В до-стандартному коді NolL був визначався як щось непридатне, тому його слід було уникати. Це менш поширено зараз.

Якщо вам потрібно присвоїти ім’я нульовому покажчику, назвіть його nollptr, це те, як він буде називатися в C++0х. Згодом “nollptr” стане ключовим словам.

Як об’єкти C++ позначаються в пам’яті?

Як і C, C++ не визначає розмітку, а лише семантичні обмеження, яких потрібно дотримуватись. Тому різні реалізації працюють по-різному. На жаль, краще пояснення, яке я знаю, знаходиться в книзі, яка вже застаріла і не описує будь-яку сучасну реалізацію C++: “Анотований довідник C++” (зазвичай називається “ARM”). Там є діаграми з прикладамив ключової розмітки. Також є дуже коротке пояснення в розділі 2 ”
TC++ PL“.
В принципі, C++ будує об’єкти просто шляхом об’єднання під-об’єктів. Отже:

struct A { int a,b; };

представлено двома, розміщеними поруч int, і


struct B: A { int c; };

представлено як А, за яким слідує int, тобто поруч знаходяться три цілі числа.

Віртуальні функції, як правило, реалізується шляхом додавання покажчика (vptr) до кожного об’єкта класу з віртуальними функціями. Цей покажчик вказує на відповідну таблицю функцій (vtbl). Кожен клас має свою власну vtbl, яка призначена для всіх об’єктів цього класу.

Яке значення у i+++ i++?

Це є невизначеним. В принципі, в C і C++, якщо ви читаєте змінну двічі у виразі, де ви також її пишете, результат буде невизначеним. Не робіть цього. Інший приклад:


v[i] = i++;

Схожий приклад:


f(v[i],i++);

Тут результат буде невизначеним, оскільки порядок обчислення аргументів функції не визначений.

Якщо ви зробите порядок обчислення невизначеним, то стверджують, що ви отримаєте більш ефективний код. Компілятори можуть попереджати про такі приклади, які, як правило, є помилкою (або потенційною помилкою). Я розчарований тим, що після кількох десятиліть більшість компіляторів все ще про це не попереджають, залишаючи цю роботу для спеціалізованих окремих інструментів, які ніхто не використовує.

Чому дещо залишилося невизначеним у C++?

Тому що машини відрізняються і тому, що C залишив багато чого невизначеним також. Для отримання додаткової інформації, у тому числі визначення термінів “невизначений”, “визначений реалізацію” і “добре сформований”, див. стандарт ISO C++. Зверніть увагу, що значення цих термінів відрізняються від значень стандарту ISO C і деяких загальновживаних. Ви можете дуже гарно заплутатися під час обговорень, коли люди не розуміють, що не всі володіють визначеннями.

Це правильна, хоча й незадовільна, відповідь. Як і C, C++ призначена для використання апаратних засобів прямо і ефективно. Це означає, що C++ повинна мати справу з такими апаратними елементами як біти, байти, слова, адреси, цілі обчислення і обчислення з плаваючою крапкою, якими вони є на даній машині, а не як ми їх уявляємо. Зверніть увагу, що багато “речей”, які люди називають “невизначеними”, насправді “визначені реалізацією”, так що ми можемо написати ідеальний код лише тоді, коли знаємо, для якої машини ми його пишемо. Розміри цілих чисел і принципи округлення обчислень з плаваючою крапкою потрапляють в цю категорію.

Розглянемо, мабуть, найвідоміший і найсумнозвісніший приклад невизначеної поведінки:

        int a[10];
        a[100] = 0;   //range error
        int* p = a;
       //...
        p[100] = 0;   //range error (unless we gave p a better value before that assignment)

Поняття масиву і покажчика у C++ (і C) є прямим відображенням машинних понять пам’яті та адрес, за умови відсутності додаткових витрат. Примітивні операції з покажчиками знаходять безпосереднє відображення в машинних інструкціях. Зокрема, відсутня перевірка діапазону. Виконання перевірка діапазону вплине на витрати з точки зору часу виконання і розмір коду. Мова C була розроблена, щоб витіснити асемблер із задач операційних систем, це було необхідним рішенням. Крім того, у C на відміну від C++, немає способу звітування про порушення, якщо компілятор вирішить згенерувати код для їх виявлення: У C немає ніяких винятків. Мова C++ наслідує C з міркувань сумісності і тому, що C++ є безпосереднім конкурентом асемблера (в ОС, вбудованих системах, а також деяких областях числових обчислень). Якщо ви хочете перевірити діапазон, використовуйте відповідний перевірений клас (вектор, інтелектуальний покажчик, рядок тощо). Хороший компілятор за час компіляції може відстежити діапазон помилки для [100], а відстеження його для р[100] вже є набагато складнішим, та й взагалі неможливо під час компіляції побачити помилки кожного діапазону.
Інші приклади невизначеної поведінки пов’язані з моделлю компіляції. Компілятор не може встановити несумісне визначення об’єкта чи функції в окремо скомпільованих одиницях. Наприклад:

       //file1.c:
        struct S { int x,y; };
        int f(struct S* p) { return p->x; }

       //file2.c:
        struct S { int y,x; }
        int main()
        {
               struct S s;
               s.x = 1;
               int x = f(&s);//x!=ss.x!!
               return 2;
        }

Компіляція file1.c і file2.c і утворення зв’язку між результатами у тій же програмі є неможливим як в C, так і в C++. Компонувальник може встановити несумісне визначення S, але не зобов’язаний це робити (і в більшості випадків не робить). У багатьох випадках досить важко відстежити протиріччя між окремо скомпільованими одиницями. Послідовне використання файлів заголовків дозволяє мінімізувати такі проблеми, і вже є деякі ознаки того, що робота редактора зв’язків покращується. Зазначимо, що редактор зв’язків C++ вже може встановити майже всі помилки, пов’язані з непослідовно заявленими функціями.
Зрештою, у нас є, здавалося б, непотрібна і неприємно невизначена поведінка окремих виразів. Наприклад:

        void out1() { cout << 1; }
        void out2() { cout << 2; }

        int main()
        {
               int i = 10;
               int j =++i+ i++;    //value of j unspecified
               f(out1(),out2());     //prints 12 or 21
        }

Значення j не визначене для того, щоб компілятори могли створити оптимальний код. Стверджують, що різниця між тим, що можна зробити, давши компілятору свободу, і вимагаючи “звичайну оцінку зліва-направо” може бути значною. Я переконаний, зважаючи на те, що “десь там” є численні компіляторами, які користуються свободою, і є певні люди, які пристрасно захищати цю свободу, внести якісь зміни буде важко, і процес проникнення у віддалені куточки світів C і C++ може зайняти десятиліття. Я розчарований, що не всі компілятори попереджають про такий код як ++i+i++. Крім того, невизначеним є порядок обчислення аргументів.

На мою думку, занадто багато “речей” залишається невизначеними, невказаними, залежними від реалізації тощо. Однак, легко сказати, і навіть навести приклади, але це важко виправити. Слід також зазначити, що так вже й важко уникнути більшості проблем і розробити портативний код.

Чому я не можу визначити обмеження для шаблонних параметрів?

Ви можете, це досить легко.
Зверніть увагу:

        template<class Container>
        void draw_all(Container& c)
        {
                for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
        }

Якщо є помилки типу, це виявиться в дозволі досить складного виклику for_each (). Наприклад, якщо тип елемента контейнера ціле число, то ми отримаємо якусь незрозумілу помилку, пов’язану з викликом for_each () (бо ми не може посилатися на Shape::draw() для цілих чисел).
Для того, щоб заздалегідь відстежити такі помилки, я можу написати:

        template<class Container>
        void draw_all(Container& c)
        {
                Shape* p = c.front();//accept only containers of Shape*s

                for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
        }

Ініціалізації помилкових змінних “р” викличе зрозуміле повідомлення про помилку у більшості станніх компіляторів. Схожі трюки є загальноприйнятими у всіх мовах і повинні бути розроблені для всіх нових конструкцій. При розробці коду, я б, напевно, написати щось на кшталт:

        template<class Container>
        void draw_all(Container& c)
        {
                typedef typename Container::value_type T;
                Can_copy<T,Shape*>();//accept containers of only Shape*s

                for_each(c.begin(),c.end(),mem_fun(&Shape::draw));
        }

Звідси ясно, що я роблю твердження. Шаблон Can_copy може бути визначений наступним чином:

        template<class T1, class T2> struct Can_copy {
               static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
               Can_copy() { void(*p)(T1,T2) = constraints; }
        };

Can_copy перевіряє (під час компіляції), що T1 можеь бути віднесено до T2. Can_copy <T,Shape*> перевіряє, що Т є Shape* або покажчиком на клас, отриманий з Shape, або типом з перетворенням на Shape*. Зверніть увагу, що визначення майже мінімальне:
- один рядок для зазначення обмежень, які повинні бути перевірені, і типів, які потрібно перевірити;
- один рядок для зазначення переліку конкретних проблем, які потрібно перевірити (функція constraints())
- один рядок для забезпечення можливості запуску перевірки (конструктора).
Відзначимо також, що визначення має такі властивості як:
- Ви можете виразити обмеження без оголошення або копіювання змінних, таким чином, щоб автор обмежень не був змушений робити припущення про те, яким є тип ініціалізації, які об’єкти можуть бути скопійовані, знищені тощо (якщо, звичайно, ці властивості не перевіряються обмеженнями);
- Код для обмежень емає генерується з використанням поточного компілятора;
- Макроси для визначення чи використання обмежень не потрібні;
- Поточні компілятори дають прийнятні повідомлення про помилки виконання обмежень, які містять слово “обмеження” (щоб дати користувачу підказку), ім’я обмеження, а також конкретні помилки, які призвели до збою (наприклад, “неможливо ініціалізувати Shape* з допомогою double*”).
Так чому ж щось на зразок Can_copy () – або щось більш витончене – не присутнє в мові? “D & E” міститься аналіз труднощів, пов’язаних з вираженням загальних обмежень у C++. З тих пір виникло багато ідей з приводу полегшення написання класів цих обмежень і запуску гарних повідомлень про помилки. Наприклад, я вважаю, що використання покажчиків на функції, як я це робллю в Can_copy було ридумано Алексом Степановим і Джеремі Сіком. Я не думаю, що Can_copy () цілком готовий до стандартизації – для цього потрібно запровадити ширше використання. Крім того, в спільноті C++ використовуються різні форми обмежень, і до цих пір немає консенсусу щодо того, яка що форма шаблонів обмежень є найбільш ефективною при широкому застосуванні.
Проте, ідея дуже проста, більш проста, ніж мовні засоби, які були запропоновані спеціально для перевірки обмежень. Зрештою, коли ми пишемо шаблон, ми маємо в наявності повну силу C++. Зверніть увагу:

        template<class T, class B> struct Derived_from {
               static void constraints(T* p) { B* pb = p; }
               Derived_from() { void(*p)(T*) = constraints; }
        };

        template<class T1, class T2> struct Can_copy {
               static void constraints(T1 a, T2 b) { T2 c = a; b = a; }
               Can_copy() { void(*p)(T1,T2) = constraints; }
        };

        template<class T1, class T2 = T1> struct Can_compare {
               static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; }
               Can_compare() { void(*p)(T1,T2) = constraints; }
        };

        template<class T1, class T2, class T3 = T1> struct Can_moltiply {
               static void constraints(T1 a, T2 b, T3 c) { c = a*b; }
               Can_moltiply() { void(*p)(T1,T2,T3) = constraints; }
        };

        struct B { };
        struct D: B { };
        struct DD: D { };
        struct X { };

        int main()
        {
               Derived_from<D,B>();
               Derived_from<DD,B>();
               Derived_from<X,B>();
               Derived_from<int,B>();
               Derived_from<X,int>();

               Can_compare<int,float>();
               Can_compare<X,B>();
               Can_moltiply<int,float>();
               Can_moltiply<int,float,double>();
               Can_moltiply<B,X>();

               Can_copy<D*,B*>();
               Can_copy<D,B*>();
               Can_copy<int,B*>();
        }

       //the classical "elements must derived from Mybase*" constraint:

        template<class T> class Container: Derived_from<T,Mybase> {
              //...
        };

Насправді, Derived_from перевіряє не походження, а перетворення, але це часто є кращим обмеженням. Важко знайти гарну назву для обмеження.

Для чого використовувати sort(), коли у нас є “старий добрий qsort ()”?

Для новачка


qsort(array,asize,sizeof(elem),elem_compare);

виглядає досить дивним і важко зрозумілим у порівнянні з

sort(vec.begin(),vec.end());

Для експерта той факт, що sort(), як правило, є швидшим, ніж qsort() для тих же елементів і з тими ж критеріями порівняння, часто є значним. Крім того, sort() є універсальним, отже може бути використаний для будь-якого розумного поєднання типів контейнерів, типів елементів і критеріїв порівняння. Наприклад:


        struct Record {
               string name;
              //...
        };

        struct name_compare { //compare Records using "name" as the key
               bool operator()(const Record& a, const Record& b) const
                       { return a.name<b.name; }
        };

        void f(vector<Record>& vs)
        {
               sort(vs.begin(), vs.end(), name_compare());
              //...
        }

Крім того, більшість людей вважають, що sort() є безпечним типом, не потрібно використовувати ніяких приведень, і не потрібно використовувати функцію compare() для стандартних типів.

Для більш докладного пояснення див. мою статтю “Вивчення C++ як нової мови”, яку можна завантажити з мого списку публікацій.

Основною причиною того, що sort() може перевершити qsort() є кращі вбудовані порівняння.

Що таке об’єкт функції?

Об’єкт, звичайно, деякою мірою веде себе як функція,. Як правило, це стосується об’єкту класу, який визначається оператором додатка – operator().

Функція об’єкта є більш загальним поняттям, ніж функція, оскільки функція об’єкта може мати стан, який зберігається протягом кільков викликів (як статична локальна змінна) і може бути створений й вивчатися поза межами об’єкта (на відміну від статичної локальної змінної). Наприклад:

        class Sum {
               int val;
        public:
               Sum(int i):val(i) { }
               operator int() const { return val; }         //extract value

               int operator()(int i) { return val+=i; }     //application
        };

        void f(vector<int> v)
        {
               Sum s = 0;    //initial value 0
               s = for_each(v.begin(), v.end(), s); //gather the sum of all elements
               cout << "the sum is " << s << "n";

              //or even:
               cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) << "n";
        }

Зверніть увагу, що функція об’єкта з вбудованим додатком оператора досить гарно вбудовує, тому що немає покажчиків, які можуть заплутати оптимізаторів. Для контрасту: сучасні оптимізатори рідко (чи ніколи?) вбудовують виклик через покажчик на функцію.

Об’єкти функцій широко використовуються для забезпечення гнучкості в стандартній бібліотеці.

Як боротися з витоками пам’яті?

Написавши код, який їх не має. Ясно, що якщо ваш код має всюди операції створення, операції видалення і арифметичні покажчиками, ви скоріше за все десь заплутаєтесь і отримаєте витоки, випадкові покажчики тощо. Це твердження є вірним незалежно від того, наскільки уважним ви є з виділенням: в кінці кінців складність коду перевершить час і зусилля, які ви можете собі дозволити. Звідси випливає, що успішні методи спираються на приховування виділення та зняття виділення всередині більш керованих типів. Хорошим прикладом є стандартні контейнери. Вони управляють пам’яттю для своїх елементів краще, ніж ви могли б, не витрачаючи зайвих зусиль. Розглянемо написання цього без допомоги рядка і вектора:


        #include<vector>
        #include<string>
        #include<iostream>
        #include<algorithm>
        using namespace std;

        int main()    //small program messing around with strings
        {
               cout << "enter some whitespace-separated words:n";
               vector<string> v;
               string s;
               while (cin>>s) v.push_back(s);

               sort(v.begin(),v.end());

               string cat;
               typedef vector<string>::const_iterator Iter;
               for (Iter p = v.begin(); p!=v.end();++p) cat+= *p+"+";
               cout << cat << 'n';
        }

Яким буде ваш шанс зробити все правильно вперше? І як ви будете знати, що не було витоку?
Зверніть увагу на відсутність явного управління пам’яттю, макросів, приведень, надлишкових перевірок, явних обмежень розміру і покажчиків. За допомогою об’єкта функції і стандартного алгоритму, я міг би позбутися покажчикоподібного використання ітератора, але це здавалося зайвим для такої маленької програми.
Ці методи не досконалі і систематично їх не завжди легко використовувати. Тим не менш, вони застосовуються досить широко і за рахунок скорочення числа явних виділень та зняття виділень, інші приклади набагато легше відстежити. Ще в 1981 році я зазначив, що за рахунок скорочення кількості об’єктів, за якими я повинен був стежити з багатьох десятків тисяч до декількох десятків, я зменшив інтелектуальні зусилля, необхідні для створення програми, яка вже була не подвигом Геркулеса, а чимось більш керованим або навіть простішим.
Якщо ваша область додатків не має бібліотек програмування, які легко мінімізують явне управління пам’яттю, тоді найшвидшим способом отримати закінчену і правильну програму, може стати створення спочатку такої бібліотеки.
Шаблони і стандартні бібліотеки роблять використання контейнерів, ресурсних механізмів тощо набагато простішим, ніж це було ще кілька років тому. Використання виключень робить все це майже основою.
Якщо ви не можете впоратися з виділенням/зняттям виділення в якості частини об’єкту, який в будь-якому випадку вам потрібен, можете використати дескриптор ресурсу, щоб звести до мінімуму ймовірність витоку. Ось приклад, де я повинен перетворити об’єкт, виділений у вільному просторі, на функцію. Це шанс забути про видалення цього об’єкту. Зрештою, ми не можемо сказати, лише дивлячись на покажчик, чи повинен він бути знятий з виділення, і якщо це так, хто відповідальний за цю дію. Використання дескриптора ресурсів, тобто стандартної бібліотеки auto_ptr, прояснює межу відповідальності:

        #include<memory>
        #include<iostream>
        using namespace std;

        struct S {
               S() { cout << "make an Sn"; }
               ~S() { cout << "destroy an Sn"; }
               S(const S&) { cout << "copy initialize an Sn"; }
               S& operator=(const S&) { cout << "copy assign an Sn"; }
        };

        S* f()
        {
               return new S; //who is responsible for deleting this S?
        };

        auto_ptr<S> g()
        {
               return auto_ptr<S>(new S);    //explicitly transfer responsibility for deleting this S
        }

        int main()
        {
               cout << "start mainn";
               S* p = f();
               cout << "after f() before g()n";
       //      S* q = g();   //this error woold be caught by the compiler
               auto_ptr<S> q = g();
               cout << "exit mainn";
              //leaks *p
              //implicitly deletes *q
        }

Подумайте про ресурси в цілому, а не просто про пам’ять.

Якщо систематичне застосування цих методів не є можливим у вашому середовищі (ви повинні використовувати код з інших джерел, частина вашої програми була написана неандертальцями тощо), обов’язково використовуйте детектор витоку пам’яті як частину стандартної процедури розробки, або підключіть сміттєзбірник.

Чому я не можу відновити роботу після знаходження винятку?

Іншими словами, чому C++ не забезпечує примітивну дію повернення до точки, з якої було здійснено виключення, і продовження виконання звідти?

В принципі, виходячми з обробника винятків, не можна бути впевненим, що код, після того як була написана точка винятку, продовжує роботу, неначе нічого не сталося. Оброблювач винятків не знає, скільки контексту є вірним до відновлення роботи. Щоб отримати вірний код, автор створення винятку і автор його виявлення повинні глибоко розбиратися в коді й контексті один одного. Це створює складну взаємозалежність, і якби її було дозволено, то вона призвела б до серйозних проблем в обслуговуванні.

Я серйозно розглядав можливість відновлення роботи, коли я розробляв механізм обробки винятків у C++, і це питання обговорювалося досить докладно під час стандартизації. Див. главу про обробку винятків у “Дизайн і еволюція C++”.

Якщо ви хочете перевірити, чи можна вирішити проблему до створення винятку, запустіть функцію, яка перевіряє, а потім вже створює її, тільки якщо проблему не можна вирішити локально. New_handler є прикладом цього.

Чому у C++ немає еквівалента realloc()?

Якщо ви хочете, ви можете, звичайно, використовувати realloc(). Тим не менш, realloc() гарантовано працює тільки з масивами, виділеними malloc() (і аналогічними функціями), і які містять об’єкти без користувальницьких конструкторів копіювання. Крім того, слід пам’ятати, що, всупереч вашим очікуванням, realloc() іноді робить копіювання аргументу масиву.

В C++ кращим способом боротьби з перерозподілом є використання контейнера стандартної бібліотеки, такого як вектор, і нехай все іде природним шляхом.

Навіщо використовувати винятки?

Що хорошого для мене може зробити використання винятків? Відповідь проста: використання винятків для обробки помилок робить ваш код простішим, чистішим і з меншой ймовірністю пропущених помилок. Але що поганого в “старих добрих errno і if-твердженнях”? Відповідь проста: при їх використанні, ваш обробник помилок і звичайний код тісно переплетені. Таким чином, код стає брудним і вам стає важко гарантувати, що ви помітили усі помилки (згадайте “код спагетті” або “гніздо пацюка”).

Перш за все, є речі, які просто неможливо зобити правильно без винятків. Розглянемо помилку, виявлену в конструкторі – як ви повідомите про помилку? Ви зробите виняток. Це є основою RAІІ (“Resource Acquisition Is Initialization” – одержання ресурсу – це ініціалізація), яка є основою деяких з найбільш ефективних сучасних методів розробки в C++: метою конструктора є створення інваріанту для класу (створення середовища, в якому повинні працювати функції члени), і це часто вимагає отримання доступу до ресурсів, таких як пам’ять, блоки, файли, сокети тощо.

Уявіть, якби у нас не було винятків, що б ви зробили з помилкою, виявленою в конструкторі? Пам’ятайте, що конструктори часто використовують ініціалізацію/створення об’єктів в змінних:

        vector<double> v(100000);  //needs to allocate memory
        ofstream os("myfile");     //needs to open a file

І конструктор векторів, і ofstream-конструктор (від “output file stream” – потік вихідного файла) можуть або спричинити “поганий” стан змінної в (як ifstream і робить за замовчуванням), так що кожна наступна операція буде провалом. Це не є ідеальним. Наприклад, у разі використання ofstream, ваш вихідні дані просто зникають, якщо ви забули перевірити, чи успішно виконана відкрита операція. Для більшості класів результати є гіршими. Принаймні, ми повинні написати:

        vector<double> v(100000);  //needs to allocate memory
        if (v.bad()) {/* handle error */}  //vector doesn't actually have a bad(); it relies on exceptions
        ofstream os("myfile");     //needs to open a file
        if (os.bad())  {/* handle error */}

Це додаткове випробування для об’єкта (записати, запам’ятати чи забути). Справа стає дуже невтішною для класів, що складаються з декількох об’єктів, особливо якщо ці дочірні об’єкти залежать один від одного. Докладніше див. “Мова програмування C++” (п. 8.3., глава 14, і додаток E), або (більш академічну) статтю “Безпека винятків: концепції та методи”.
Отже написання конструкторів може бути складним без винятків, але як щодо простих старих функцій? Ми можемо або повернути помилковий код, або встановити нелокальні змінні (наприклад, errno). Встановлення глобальних змінних не спрацює дуже добре, якщо ви їх негайно не перевірите (або деякі інші функції, можливо, змістять їх). Навіть не думайте про цей метод, якщо у вас може бути декілька потоків доступу до глобальної змінної. Проблема з поверненими значеннями полягає в тому, що вибір помилкового повернутого значення вимагатиме додаткових зусиль розуму і не завжди може бути можливим:

        double d = my_sqrt(-1);              //return -1 in case of error
        if (d == -1) {/* handle error */}
        int x = my_negate(INT_MIN);   //Duh?

Не існує можливого значення для повернення my_negate (): кожне ціле число є правильною відповіддю для деякого int, і немає правильної відповіді для від’ємних чисел у відображенні додаткового кода. В таких випадках ми мали б повернути пари значень (і, як завжди, не забути перевірити). Див мою книгу “Початок програмування” з більшою кількістю прикладів і пояснень.

Основні заперечення проти використання винятків:

але виключення є дорогими!: Не зовсім так. Сучасні реалізації C++ зменшують накладні витрати на використання винятків до кількох відсотків (скажімо, 3%), і це у порівнянні з відсутністю обробки помилок. Написання коду, який повертає помилки, і тести також не є безкоштовними. Як правило, обробка винятків є дуже дешевою, якщо ви не пускаєте в хід винятки. В деяких реалізаціях це взагалі нічого не коштує. Всі витрати відбуваються, коли ви запускаєте виняток, тобто “нормальний код” є швидшим, ніж код, які використовує повернення помилок та тести. Ви стикаєтесь з витратами тільки тоді, коли у вас є помилки.
але в JSF++ ви самі заборонили виключення!: JSF++ існує для важких часів та додатків, для яких безпека є критично важливою (програмне забезпечення для управління польотами). Якщо розрахунок займає надто багато часу, хтось може померти. З цієї причини, ми повинні мати гарантований час відгуку, і ми не можемо – на поточному рівні підтримки інструменту – зробити виключення. В цьому контексті навіть виділення вільного простору є забороненим! Насправді, рекомендації JSF++ по обробці помилок імітують використання винятків в очікуванні дня, коли у нас будуть інструменти, щоб зробити все правильно, тобто використовуючи винятки.
але запуск винятку з конструктора за допомогою new спричиняє витік пам’яті!: Нісенітниця! Ця стара байка була викликана помилкою в одному компіляторі – і ця помилка була негайно виправлена більше десяти років тому.

Як я можу використовувати винятки?

Дивіться “Мова програмування C++” (п. 8.3, глава 14 та Додаток E). Додаток фокусується на методах написання коду, безпечного в плані винятків в ресурсоємних додатках, і не орієнтований ная новачків.
В C++ винятки використовується для попередження помилок, які не можуть бути оброблені локально, наприклад, помилка при доступу до ресурсу в конструкторі. Наприклад:

        class Vector {
               int sz;
               int* elem;
               class Range_error { };
        public:
               Vector(int s): sz(s) { if (sz<0) throw Range_error();/*... */}
              //...
        };

Не використовуйте виключення як просто ще один спосіб повернення значення з функції. Більшість користувачів припускають – як їх і спонукає визначення мови – що код з обробкою виключень є кодом з обробкою помилок, а реалізації оптимізовані з урахуванням цього припущення.
Ключовою технікою є “одержання ресурсу – це ініціалізація” (іноді скорочують як RAII), яка використовує класи з деструкторами, щоб навести порядок в управління ресурсами. Наприклад:

void fct(string s)
        {
               File_handle f(s,"r"); //File_handle's constructor opens the file called "s"
              //use f
        }//here File_handle's destructor closes the file

Якщо частина fct() “use f” запускає виняток, також запускається деструктор, і файл закривається. Це відрізняється від звичайного небезпечного використання:

        void old_fct(const char* s)
        {
               FILE* f = fopen(s,"r");       //open the file named "s"
              //use f
               fclose(f);    //close the file
        }

Якщо частина old_fct “use f” створює виняток – або просто робить повернення – файл не закривається. У програмах C, longjmp() є додатковою небезпекою.

Чому я не можу призначити вектор <Apple>(яблуко) вектору <Fruit>(фрукт)?

Тому що це створить діру в системі типів. Наприклад:


        class Apple: public Fruit { void apple_fct();/*... */};
        class Orange: public Fruit {/*... */};//Orange doesn't have apple_fct()

        vector<Apple*> v;     //vector of Apples

        void f(vector<Fruit*>& vf)           //innocent Fruit manipolating function
        {
               vf.push_back(new Orange);     //add orange to vector of fruit
        }

        void h()
        {
               f(v);  //error: cannot pass a vector<Apple*> as a vector<Fruit*>
               for (int i=0; i<v.size();++i) v[i]->apple_fct();
        }

Якби виклик f(v) був можливим, у нас би був Orange (апельсин), який прикидається Apple (яблуком).

Альтернативним конструктивним рішенням у мові було б дозволити небезпечні перетворення, але покладатися на динамічний контроль. Це вимагало б проведення перевірки при кожному зверненні до членів v, а h() довелося б викликати виняток при зустрічі з останнім елементом v.

Чому у C++ немає об’єкту універсального класу?

– Нам не потрібен тільки один: узагальнене програмування забезпечує в більшості випадків безпечну альтернативу в плані статичних типів. Інші випадки обробляються з використанням множинного наслідування.

– Не існує корисного універсального класу: справді універсальне немає ніякої власної семантики.

– “Універсальний” клас змушує недбало думати про типи і інтерфейси і призводить до надлишкової перевірки.

– Використання універсального базового класу впливає на вартість: об’єкти повинні бути виділеними групою, щоб бути поліморфними, що впливає на вартість пам’яті і доступу. Групи об’єктів, зрозуміло, не підтримують копіювання семантики. Групи об’єктів не підтримують просту поведінку в області (що ускладнює управління ресурсами). Універсальний базовий клас змушує використовувати dynamic_cast та інші перевірки.

Так. Я спростив аргументи, це “Питання і відповіді”, а не академічна стаття.

А чи потрібне нам множинне наслідування?

Не зовсім. Ми можемо обійтися без множинного наслідування, використовуючи обхідні шляхи, точно так, як ми можемо обійтися без одиночного наслідування, використовуючи обхідні шляхи. Ми навіть можемо обійтися без класів, використовуючи обхідні шляхи. Мова С є доказом цього твердження. Тим не менше, кожна сучасна мова з перевіркою статичних типів і наслідування забезпечує деяку форму множинного. В C++, абстрактні класи часто виступають інтерфейсами, а клас може мати безліч інтерфейсів. Інші мови – часто мається на увазі “не мови машинного інтелекту” – просто мають окрему назву для еквівалента чисто абстрактного класу: інтерфейс. Причиною того, що мови забезпечують наслідування (як одиночне, так і множинне) є те, що підтримка наслідування в мові, як правило, є вищим рівнем за пошук обхідних шляхів (наприклад, використання функції переадресації до суб-об’єктів або окремо виділені об’єкти) для полегшення програмування, для виявлення логічних задач, для підтримки, і часто для підвищення продуктивності.

Чи “узагальнення” є тим, чим повинні були шаблони?

Ні. Узагальнення, це перш за все, синтаксичний цукор для абстрактних класів, тобто, з узагальненнями (чи то узагальнення Java, чи C #), ви програмуєте проти чітко визначених інтерфейсів і, як правило, для використання аргументів платите за вартість викликів віртуальних функцій і/або динамічних приведень.

Шаблони підтримують узагальнене програмування, шаблонне метапрограмування тощо за рахунок поєднання функцій, таких як шаблонні аргументи цілого числа, спеціалізації та однакове ставлення до вбудованих і настроюваних типів. У результаті отримуємо гнучкість, універсальність і продуктивність, непорівняну з “узагальненим”. Стандартна бібліотека шаблонів є яскравим прикладом.

Менш бажаним результатом гнучкості є пізнє виявлення помилок і дуже погані повідомлення про помилки. Це в даний час опосередковано пов’язано з класами обмежень і буде безпосередньо розглядатися в C++0х з поняттями (див. мої публікації, мої пропозиції, і Сайт комітету стандартів для всіх пропозицій).

Чи можу я запустити виняток з конструктора? З деструктора?

- Так: ви повинні запустити виняток з конструктора, коли ви не можете правильно ініціалізувати (побудувати) об’єкт. Не існує іншої задовільної альтернативи виходу з конструктора, крім цього запуску.

– Не зовсім: ви можете запустити виняток у конструкторі, але цей виняток не повинен вийти за межі деструктора; якщо вийти при цьому з деструктора, може трапитися багато поганих речей, тому що буде порушено основні правила стандартних бібліотек і самої мови. Не робіть цього.

За прикладами і докладними поясненнями, звертайтеся до Додатку Е “Мови програмування C++”.

Існує один нюанс: винятки не можуть бути використані для деяких жорстких проектів в реальному часі. Див, наприклад, “Стандарти кодування C++ JSF”.

Чому у C++ немає “остаточної” конструкції?

Тому що C++ підтримує альтернативу, яка майже завжди є кращою: техніку “Отримання ресурсу є ініціалізація” (розділ 14.4 ТС++ PL3). Основна ідея полягає в тому, щоб представити ресурс у вигляді локального об’єкта, таким чином, щоб деструктор локального об’єкта звільнив ресурс. Таким чином, програміст не забуде звільнити ресурс. Наприклад:


class File_handle {
               FILE* p;
        public:
               File_handle(const char* n, const char* a)
                       { p = fopen(n,a); if (p==0) throw Open_error(errno); }
               File_handle(FILE* pp)
                       { p = pp; if (p==0) throw Open_error(errno); }

               ~File_handle() { fclose(p); }

               operator FILE*() { return p; }

              //...
        };

        void f(const char* fn)
        {
               File_handle f(fn,"rw");       //open fn for reading and writing
              //use file through f
        }

У системі, нам потрібен клас “дескриптора ресурсу” для кожного ресурсу. Тим не менш, нам не потрібен “остаточний” пункт для кожного випадку отримання ресурсу. У реальних системах існує набагато більше фактів отримання ресурсу, ніж видів ресурсів, тому техніка “Отримання ресурсу є ініціалізація” призводить до меншого коду, ніж використання “остаточних” конструкцій.

Крім того, подивіться приклади управління ресурсами в додатку Е “Мови програмування C++”.

Що таке auto_ptr і чому там немає auto_array?

Auto_ptr є прикладом дуже простого дескриптора класу, що визначений в <memory>, підтримує безпеку винятків з використанням техніки “Отримання ресурсу є ініціалізація“. Auto_ptr містить покажчик, може бути використаний як покажчик, і видаляє об’єкт, на який вказано, в кінці своєї області.
Наприклад:


        #include<memory>
        using namespace std;

        struct X {
               int m;
              //..
        };

        void f()
        {
               auto_ptr<X> p(new X);
               X* q = new X;

               p->m++;       //use p just like a pointer
               q->m++;
              //...

               delete q;
        }

Якщо виняток запуститься в частині…, об’єкт, на який вказано p, буде вірно видалено деструктором auto_ptr, в той час як X, на яку вказує q, буде пропущено. Дивіться п. 14.4.2 “TC++ PL” для докладної інформації.
Auto_ptr – це дуже легкий клас. Зокрема, він *не є* покажчиком з підрахунком посилань. Якщо ви “скопіюєте” один auto_ptr в інший, приписаний auto_ptr отримає покажчик, а призначений auto_ptr отримає 0. Наприклад:

#include<memory>
        #include<iostream>
        using namespace std;

        struct X {
               int m;
              //..
        };

        int main()
        {
               auto_ptr<X> p(new X);
               auto_ptr<X> q(p);
               cout << "p " << p.get() << " q " << q.get() << "n";
        }

має вивести 0-ий покажчик, за яким слідує не-0-ий покажчик. Наприклад:


p 0x0 q 0x378d0

auto_ptr:: get () повертає отриманий покажчик.
Ця “семантика руху” відрізняється від звичайної ” семантики копій “і може здатися дивною. Зокрема, ніколи не використовуйте auto_ptr в складі стандартних контейнерів. Стандартні контейнери вимагають звичайну семантику копіювання. Наприклад:

std::vector<auto_ptr<X> >v;   //error

Auto_ptr призначає покажчик на окремий елемент, а не покажчик на масив:

        void f(int n)
        {
               auto_ptr<X> p(new X[n]);      //error
              //...
        }

Це помилка, тому що деструктор видалить покажчик з використанням delete, а не delete[] і не зможе викликати деструктор на останньому n-1 Xs.
Отже ми повинні використовувати auto_array, щоб призначити масив? Ні. Не існує auto_array. Причина в тому, що в ньому немає необхідності. Кращим рішенням є використання вектора:

        void f(int n)
        {
               vector<X> v(n);
              //...
        }

Якщо виключення відбудеться в частині…, деструктор v буде правильно запущеним.

В C++0х використовуйте Unique_ptr.

Для чого не потрібно використовувати винятки?

Винятки C++ призначені для підтримки обробки помилок. Використовуйте throw тільки, щоб позначити помилку, і catch тільки, щоб вказати дії по обробці помилок. Є і інші шляхи застосування винятків – популярні в інших мовах – але не ідіоматичні в C++ і вони навмисно не підтримується в реалізаціях C++ (ці, реалізації оптимізовані у відношенні припущень, що винятки використовуються для обробки помилок).

Зокрема, throw не є просто альтернативним способом повернення значення з функції (як return). Ця дія буде повільною і викличе здивування у більшості програмістів С++, які звикли використовувати винятки тільки для обробки помилок. Крім того, throw не є кращим способом виходу з циклу.

У чому різниця між new і malloc()?

malloc() – це функція, яка приймає число (байтів) в якості аргументу, а повертає void*, що вказує на неініціалізоване сховище. new – це оператор, який приймає тип і (необов’язково) набір ініціалізаторів для цього типу в якості аргументів, а повертає покажчик на (необов’язково) ініціалізований об’єкт цього типу. Різниця є найбільш очевидною, коли ви хочете виділити об’єкт користувацького типу з нестандартною семантикою ініціалізації. Приклади:


        class Circle: public Shape {
        public:
               Cicle(Point c, int r);
              //no defaolt constructor
              //...
        };

        class X {
        public:
               X();   //defaolt constructor
              //...
        };

        void f(int n)
        {
               void* p1 = malloc(40);//allocate 40 (uninitialized) bytes

               int* p2 = new int[10];//allocate 10 uninitialized ints
               int* p3 = new int(10);//allocate 1 int initialized to 10
               int* p4 = new int();  //allocate 1 int initialized to 0
               int* p4 = new int;    //allocate 1 uninitialized int

               Circle* pc1 = new Circle(Point(0,0),10);//allocate a Circle constructed
                                                      //with the specified argument
               Circle* pc2 = new Circle;     //error no defaolt constructor

               X* px1 = new X;       //allocate a defaolt constructed X
               X* px2 = new X();     //allocate a defaolt constructed X
               X* px2 = new X[10];   //allocate 10 defaolt constructed Xs
              //...
        }

Зверніть увагу, що вказуючи ініціалізатор за допомогою запису “(value)”, ви отримаєте ініціалізацію з цим значенням. На жаль, цього не можна зробити для масиву. Часто вектор є кращою альтернативою для виділеного у вільному просторі масиву (наприклад, враховуючи безпеку винятків).

Кожного разу, коли ви використовуєте malloc(), необхідно продумати ініціалізацію і конвертацію на відповідний тип зворотнього покажчика. Ви також повинні врахувати, чи маєте право на використання відповідного числа байтів. Не існує різниці в продуктивності malloc() і new, якщо брати до уваги ініціалізацію.

malloc() повідомляє про нестачу пам’яті, повертаючи 0. new повідомляє про помилки виділення і ініціалізації за допомогою винятків.

Об’єкти, створені за допомогою new видаляються за допомогою delete. Області пам’яті, виділені за допомогою malloc() звільняються від виділення за допомогою free().

Чи можна змішувати стилі С і С++ у процесі виділення і зняття виділення?

Так, в тому сенсі, що ви можете використати malloc() і new в тій же програмі.

Ні, в тому сенсі, що ви не можете виділити об’єкт за допомогою malloc(), а зняти виділення за допомогою delete. Аналогічно не можна виділяти за допомогою, а видаляти за допомогою free(), або використовувате realloc() в масиві, виділеному за допомогою new.

Оператори C++ new і delete гарантують правильне створення і видалення; де конструктори або деструктори повинні бути викликані, там вони і запустяться. Функції в стилі C malloc(), calloc(), free(), і realloc() цього не гарантують. Крім того, немає ніякої гарантії, що механізм використання new і delete для отримання і вивільнення пам’яті сумісний з malloc() і free(). Якщо змішування стилів працює у вашій системі, значить вам просто “пощастило” – на даний момент.

Якщо ви відчуваєте необхідність у realloc() – як і багато інших людей – то слід використовувати вектор стандартної бібліотеки. Наприклад

       //read words from input into a vector of strings:

        vector<string> words;
        string s;
        while (cin>>s && s!=".") words.push_back(s);

Вектор розширюється в міру необхідності.

Див. також приклади та обговорення в “Вивчення стандарту C++ як нової мови”, який можна завантажити з мого списку публікацій.

Чому я повинен використовувати приведення для перетворення з void *?

В C можливо непрямо перетворити void* в Т*. Це небезпечно. Зверніть увагу:

        #include<stdio.h>

        int main()
        {
               char i = 0;
               char j = 0;
               char* p = &i;
               void* q = p;
               int* pp = q;  /* unsafe, legal C, not C++ */

               printf("%d %dn",i,j);
               *pp = -1;     /* overwrite memory starting at &i */
               printf("%d %dn",i,j);
        }

Ефект від використання T*, який не вказує на T, може стати катастрофою. Таким чином, в C++, щоб отримати T* з void* потрібне пряме приведення. Наприклад, щоб отримати вищевказаний небажаний ефект програми, потрібно написати:


int* pp = (int*)q;

або, використовуючи новий стиль,зробити приведення, щоб зробити більш помітною операцію перетворення неперевіреного типу:


int* pp = static_cast<int*>(q);

Приведень краще уникати.
Одним з найбільш поширених видів використання цього небезпечного перетворення в C є призначення результату malloc() на відповідний показчик. Наприклад:

int* p = malloc(sizeof(int));

В C++, використовуйте безпечий оператор:


int* p = new int;

До речі, оператор new має додаткові переваги в порівнянні з malloc():
- new не може випадково виділити неправильний обсяг пам’яті,
- new неявно перевіряє нестачу пам’яті, і
- new забезпечує ініціалізацію.
Наприклад:

        typedef std::complex<double> cmplx;

       /* C style: */
        cmplx* p = (cmplx*)malloc(sizeof(int));      /* error: wrong size */
                                                    /* forgot to test for p==0 */
        if (*p == 7) {/*... */}                   /* oops: forgot to initialize *p */

       //C++ style:
        cmplx* q = new cmplx(1,2);//will throw bad_alloc if memory is exhausted
        if (*q == 7) {/*... */}

Як визначити постійну в класі?

Якщо вам потрібна постійна, яку можна використовувати в постійному виразі, скажімо, пов’язаному з масивом, у вас є два варіанти:

        class X {
        static const int c1 = 7;
        enum { c2 = 19 };

        char v1[c1];
        char v2[c2];

       //...
};

На перший погляд, оголошення c1 здається чистішим, але врахуйте, що для використаня цього синтаксису ініціалізації всередині класу, постійна повинна бути статичною постійною інтеграла або перерахованим типом, який ініціалізується постійним виразом. Це досить обмежує:


        class Y {
        const int c3 = 7;             //error: not static
        static int c4 = 7;            //error: not const
        static const float c5 = 7;    //error: not integral
};

Я надаю перевагу використанню ” трюку з перерахунком “, тому що він є портативним і не спокушає мене до використання нестандартних розширень в синтаксисі ініціалізації всередині класу.
Так чому ж ці незручні обмеження існують? Клас, як правило, оголошений в заголовку файлу, а заголовок файлу, як правило, входить до складу багатьох одиниць. Однак, щоб уникнути використання складних правил прив’язки, C++ вимагає, щоб кожний об’єкт мав унікальне визначення. Це правило було б порушеним, якби C++ дозволив визначення у класі одиниць, які повинні зберігатися в пам’яті у вигляді об’єктів. Дивись “D & E” для пояснення компромісів в розробці C++.
У вас більше гнучкості, якщо постійну не потрібно використовувати в постійному виразі:

        class Z {
               static char* p;       //initialize in definition
               const int i;          //initialize in constructor
        public:
               Z(int ii):i(ii) { }
        };

        char* Z::p = "hello, there";

Можна взяти адресу статичного члена, якщо (і тільки якщо) вона має визначення поза класом:


        class AE {
              //...
        public:
               static const int c6 = 7;
               static const int c7 = 31;
        };

        const int AE::c7;     //definition

        int f()
        {
               const int* p1 = &AE::c6;      //error: c6 not an lvalue
               const int* p2 = &AE::c7;      //ok
              //...
        }

Чому не видалити нуль з його операнду?

Зверніть увагу:


        delete p;
       //...
        delete p;

Якщо частина… не стосується р, то друге ” delete p; ” є серйозною помилкою, від якої реалізації C++ не можуть ефективно себе захистити (без незвичних заходів безпеки). Оскільки виддалення нульового покажчика не є небезпечим за визначенням, простим рішенням для ” delete p;” було б зробити “р = 0;” після того, як він зробить все, що іще потрібно. Тим не менш, C++ цього не гарантує.
Однією з причин є те, що операнд delete не повинен знаходитись в лівій частині. Зверніть увагу:

        delete p+1;
        delete f(x);

В даному випадку реалізація delete немає покажчика, якому можна присвоїти нуль. Ці приклади можуть бути рідкісними, але вони припускають, що неможливо гарантувати, що ” будь-який покажчик на видалений об’єкт дорівнює 0″. Простим способом обходу цього “правила” повинні стати два покажчика на об’єкт:


        T* p = new T;
        T* q = p;
        delete p;
        delete q;     //ouch!

C++ явно дозволяє здійснити видалення до нуля з операнда зі значенням в лівій частині, і я сподівався, що реалізація буде це робити, але ця ідея, здається, не стала популярною серед виконавців.
Якщо ви вважаєте, що обнулення покажчиків є важливим, подумайте про використання функції destroy :

template<class T> inline void destroy(T*& p) { delete p; p = 0; }

Врахуйте цю ще одну причину зведення до мінімуму явного використання new і delete, поклавшись на контейнери стандартної бібліотеки, дескриптори тощо.
Зверніть увагу, що передача покажчика в якості посилання (щоб дозволити йому обнулитися) має додаткову перевагу в тому, що destroy() не буде викликаний для правої частини оператора:

        int* f();
        int* p;
       //...
        destroy(f()); //error: trying to pass an rvalue by non-const reference
        destroy(p+1); //error: trying to pass an rvalue by non-const reference

Чому не деструктор викликається в кінці області?

Відповідь проста: “Звичайно це він!”. Але подивіться на такий приклад, який часто супроводжує це питання:


        void f()
        {
               X* p = new X;
              //use p
        }

Тобто, було кілька (помилкових) припущень, що об’єкт, створений за допомогою “new” буде знищений наприкінці функції.
В принципі, потрібно використовувати тільки “new”, якщо ви хочете, щоб час існування об’єкта не залежав від області його створення. В даному випадку потрібно використовувати “delete”, щоб його знищити. Наприклад:

        X* g(int i) {/*... */return new X(i); }   //the X outlives the call of g()

        void h(int i)
        {
               X* p = g(i);
              //...
               delete p;
        }

Якщо ви хочете, щоб об’єкт існував тільки в межах області, не слід використовувати “new”, а просто визначити змінну:


        {
                ClassName x;
               //use x
        }

Змінна повністю видаляється наприкінці області.
Код, який створює об’єкт за допомогою new, а потім видаляє його в кінці тієї ж області, є потворним, схильним до помилок, і неефективним. Наприклад:

        void fct()    //ugly, error-prone, and inefficient
        {
               X* p = new X;
              //use p
               delete p;
        }

Чи можу я написати “void main()”?

Визначення


void main() {/*... */}

ніколи не було в C++, так само як і не було навіть в C. Див п. 3.6.1 [2] “Стандарт ISO C++” або п. 5.1.2.2.1 “Стандарт ISO C”. Узгоджена реалізація допускає


int main() {/*... */}

і


int main(int argc, char* argv[]) {/*... */}

Узгоджена реалізація може забезпечити додаткові версії main(), але всі вони стали цілим числом. Ціле число, повернуте за допомогою main(), для програми є способом повернути значення до “системи”, яка його викликала. У системах, які не забезпечують такої можливості, повернуте значення ігнорується, але це не робить “void main()” допустимим в С++ або C. Навіть якщо ваш компілятор приймає “void main()”, уникайте цього, або програмісти С і C++ вважатимуть вас неосвіченим.
В C++ main() не повинен містити пряму вказівку на повернення. В цьому випадку повернуте значення дорівнює 0, що означає успішне виконання. Наприклад:

        #include<iostream>

        int main()
        {
               std::cout << "This program returns the integer value 0n";
        }

Відзначимо також, що ні ISO C++, ні C99 не дозволяють залишити тип поза декларацією. Тобто, на відміну від C89 і ARM C++, “int” не допустимий, якщо в декларації відсутній тип. Отже:


        #include<iostream>

        main() {/*... */}

це помилка, тому що тип повернення main() відсутній.

Чому я не можу перевантажувати dot,::, sizeof, тощо?

Більшість операторів можуть бути перевантажені програмістом. Винятком є


. (dot) ::?:  sizeof

Немає фундаментальної причини для заборони перевантаження?:. Я просто не бачу необхідності введення окремого випадку перевантаження потрійних оператора. Зверніть увагу, що функція перевантаження expr1?expr2:expr3 не зможе гарантувати, що тільки один з expr2 і expr3 був виконаний.
Sizeof не можна перевантажувати, тому що від нього непрямо залежать вбудовані операції, такі як введення покажчика в масив. Зверніть увагу:

        X a[10];
        X* p = &a[3];
        X* q = &a[3];
        p++;   //p points to a[4]
              //thus the integer value of p must be
              //sizeof(X) larger than the integer value of q

Таким чином, не можна надати нового та іншого значення sizeof(X) програмістом, не порушуючи основних правил мови.
В N::m ні N, ні m не є виразами зі значеннями; N і m – це імена, відомі компілятору, а:: виконує (під час компіляції) швидше розширення області, а не підрахунок виразу. Можна уявити перевантаження х::у, де х – це об’єкт, а не ім’я чи клас, але це призведе – на відміну від перших здогадок – до введення нового синтаксису (щоб дозволити expr::expr). Не є очевидним, які переваги можна отримати від такого ускладнення.
Оператор.(dot) в принципі може бути перевантажений, використовуючи той же метод, який використовується для ->. Тим не менш, це може привести до питань про те, чи призначений оператор для перевантаження об’єкта, або того, на що посилається об’єкт. Наприклад:

        class Y {
        public:
               void f();
              //...
        };

        class X {     //assume that you can overload.
               Y* p;
               Y& operator.() { return *p; }
               void f();
              //...
        };

        void g(X& x)
        {
               x.f(); //X::f or Y::f or error?
        }

Ця проблема може бути вирішена декількома способами. Під час стандартизації не було очевидно, який є кращим. Для отримання додаткової інформації, див. “D & E”.

Чи можу я визначити свої оператори?

На жаль, ні. Ця можливість переглядалася кілька разів, але кожного разу, я/ми вирішували, що ймовірні проблеми переважують можливі переваги.

Справа не в мові, а в технічній проблемі. Навіть коли я вперше подумав про це в 1983 році, я знав, як це можна реалізувати. Однак, мій досвід показує, що коли ми виходимо за межі банальних прикладів, люди, схоже, мають різні думки щодо “очевидного” шляху використання оператора. Класичним прикладом є a**b**c. Припустимо, щоб ** означав зведення в ступінь. Отже a**b**c означатиме (a**b)**c чи a**(b**c)? Я думав, що відповідь була очевидною, і мої друзі погодилися – і тут ми виявили, що ми не згодні, яке ж рішення є очевидним. Я припускаю, що такі проблеми можуть привести до помилок.

Як викликати функцію C з C++?

Просто оголосіть функцію C “extern “C”" (в коді C++) і викличте її (з коду C або C++). Наприклад:


       /C++ code

        extern "C" void f(int);       //one way

        extern "C" {  //another way
               int g(double);
               double h();
        };

        void code(int i, double d)
        {
               f(i);
               int ii = g(d);
               double dd = h();
              //...
        }

Визначення функцій можуть виглядати наступним чином:


       /* C code: */

        void f(int i)
        {
              /*... */
        }

        int g(double d)
        {
              /*... */
        }

        double h()
        {
              /*... */
        }

Зазначимо, що використовуються правила типу C++, а не C. Таким чином, ви не зможете викликати функцію, оголошену “extern “C”"‘ з неправильним числом аргумента. Наприклад:


       //C++ code

        void more_code(int i, double d)
        {
               double dd = h(i,d);   //error: unexpected arguments
              //...
        }

Як викликати функцію C++ з С?

Просто оголосіть функцію C++ “extern “C”" (в коді C++) і викличте її (з коду C або C++). Наприклад:


       //C++ code:

        extern "C" void f(int);

        void f(int i)
        {
              //...
        }

Тепер f() можна використовувати наступним чином:


       /* C code: */

        void f(int);

        void cc(int i)
        {
               f(i);
              /*... */
        }

Звичайно, це працює тільки для функцій без членів. Якщо ви хочете викликати функцію-член (включаючи віртуальні функції) з C, вам необхідно забезпечити просту оболонку. Наприклад:


       //C++ code:

        class C {
              //...
               virtual double f(int);
        };

        extern "C" double call_C_f(C* p, int i)      //wrapper function
        {
               return p->f(i);
        }

Тепер C::f() можна використовувати наступним чином:


       /* C code: */

        double call_C_f(struct C* p, int i);

        void ccc(struct C* p, int i)
        {
               double d = call_C_f(p,i);
              /*... */
        }

Якщо ви хочете викликати перевантажені функції з C, необхідно забезпечити оболонки з різними іменами для використанні кодом C. Наприклад:


       //C++ code:

        void f(int);
        void f(double);

        extern "C" void f_i(int i) { f(i); }
        extern "C" void f_d(double d) { f(d); }

В даний час функції f() можуть бути використані наступним чином:


       /* C code: */

        void f_i(int);
        void f_d(double);

        void cccc(int i,double d)
        {
               f_i(i);
               f_d(d);
              /*... */
        }

Зауважимо, що ці методи можуть бути використані для виклику бібліотеки C++ з коду C, навіть якщо ви не можете (або не хочете) змінювати заголовки C++.

Правильно “int* p;” чи “int *p;”?

Обидва варіанти є “правильними” в тому сенсі, що обидва дійсні в C і C++, і обидва мають однаковий зміст. Оскільки визначення мови та компілятори пов’язані, можна використовувати також “int*p;”, та “int * p;”.

Питання вибору між “int* p;” та “int *p;” полягає не в правильності, а в стилі. Мова С робить наголос на вирази; декларацій часто вважалися не більшим, ніж необхідне зло. C++, з іншого боку, робить акцент на типи.

“Типовий програміст C” пише “int *р;” і пояснює це тим, що “*р є int” наголошуючи на синтаксис, а також може вказувати на граматику декларацій C (і C++), щоб відстояти правильність стилю. Дійсно, * пов’язано з ім’ям р в граматиці.

“Типовий програміст C++” пише “int* р;” і пояснює це тим, що “р є покажчиком на int”, наголошуючи на тип. Дійсно, типом р є int*. Я наполягаю на цьому наголосі, і вважаю, що даний принцип важливий для використання більш прогресивних частин C++.

Критична плутанина виникає (лише), коли люди намагаються оголосити кілька покажчиків однією декларацією:

int* p, p1;   //probable error: p1 is not an int*

Розміщення * ближче до імені не робить такі помилки рідшими.


int *p, p1;   //probable error?

Оголошення одного імені в декларації зводить до мінімуму дану проблеми – зокрема, при ініціалізації змінних. Менш ймовірно, що люди напишуть:


        int* p = &i;
        int p1 = p;   //error: int initialized by int*

І якщо вони це зроблять, то компілятор буде скаржитися.

Щоразу, коли щось можна зробити двома способами, хтось буде збитий з пантелику. Щоразу, коли колись стоїть питання смаку, обговорення може затягнутися навічно. Дотримуйтеся принципу одного покажчика в декларації і завжди ініціалізуцте змінні, і тоді плутанина зникне. Дивіться “Дизайн і еволюція C++” для детального обговорення синтаксису декларацій C.

Який стиль макету найкраще підходить для мого коду?

Такі питання стилю є питаннями особистого смаку. Найчастіше думки про макет коду співпадають, але послідовність важливіша за будь-який конкретний стиль. Як і більшості людей, я пережив важкі часи під час побудови твердих логічних аргументів для моїх уподобань.

Особисто я використовую те, що часто називається стилем “K&R”. Коли ви додаєте правила для конструкцій, яких немає в С, то отримуєте те, що іноді називають стилем “Страуструпа”. Наприклад:

public:
       //...
};

void f(int* p, int max)
{
        if (p) {
              //...
        }

        for (int i = 0; i<max;++i) {
              //...
        }
}

Цей стиль зберігає вертикальний простір краще, ніж більшість стилів розміщення, і я хотів би, щоб розмістити все розумно на екрані, наскільки це можливо. Розміщення відкритої дужки функції на новому рядку дозволяє мені одразу відрізнити визначення функції від визначення класу.

Відступи – це дуже важливо.

Питання дизайну, такі як використання абстрактних класів для основних інтерфейсів, використання шаблонів для представлення гнучких абстракцій, безпечних з точку зору типів, і правильне використання винятків для відображення помилок, є набагато більш важливішими, ніж вибір стилю макету.

Як ви називаєте змінні? Чи рекомендуєте ви “угорські назви”?

Ні, я не рекомендую “угорські назви”. Я вважаю, що “угорський стиль” (вкладення скороченого варіанту типу в ім’я змінної) – це техніка, яка може бути корисна в нетипізованій мові, але абсолютно не підходить для мови, яка підтримує узагальнене програмування і об’єктно-орієнтоване програмування, які підкреслюють вибір операцій в залежності від типу аргументу (відомого мові або оперативній підтримці). В цьому випадку, “побудова типу об’єкта в імені” просто ускладнює і зводить до мінімуму абстракції. У різній мірі у мене є аналогічні проблеми в кожній схемі, що включає інформацію про мовно-технічні деталі (наприклад, область, клас зберігання, синтаксичні категорії) в імена. Я згоден, що в деяких випадках натяки типу конструкції на імена змінних можуть бути корисними, але в цілому, і особливо, при розвитку програмного забезпечення, це можете привести гарний код до небезпек в технічному обслуговуванні і серйозних збитків. Уникнути цього як чуми.

Отже, я не люблю присвоювати імена змінним після їх типу, а що я люблю0 і рекомендую? Назвіть змінну (функцію, тип, будь-що) на основі того, що вона робить. Виберіть значиме ім’я, тобто вибирати імена, які допоможуть людям зрозуміти вашу програму. Навіть у вас будуть проблеми з розумінням того, що ваша програма повинна робити, якщо ви засмітите її змінними з простими іменами, такими як x1, x2, s3 і p7. Скорочення та абревіатури можуть збити з пантелику людей, тому використовувати їх економно. Скорочення слід використовувати з обережністю. Згадаймо, mtbf, TLA, myw, RTFM, і NBV. Вони очевидні, але почекайте кілька місяців, і навіть я забуду принаймні одне.

Короткі назви, такі як х та і, мають сенс, коли використовуються умовно, тобто х повинен бути локальною змінною чи параметром, а і повинен бути індексом циклу.

Не використовуйте занадто довгі імена, їх важко друкувати, вони роблять рядки занадто довгими і не поміщаються на екрані, іх важко читати швидко. Це, напевно, нормально:

partial_sum    element_count    staple_partition

А ось це, мабуть, занадто довго:


the_number_of_elements    remaining_free_slots_in_symbol_table

Я віддаю перевагу використанню підкреслень для розділення слів в ідентифікаторі (наприклад, element_count), а не альтернативам, таким як elementCount і ElementCount. Ніколи не використовуйте тільки великі букви (наприклад, BEGIN_TRANSACTION), тому що вони зазвичай зарезервовані для макросів. Навіть якщо ви не використовуєте макроси, хтось може засмітити ваші ключові файли ними. Використовуйте першу велику літеру для типів (наприклад, Square і Graph). Мова C++ і її стандартна бібліотека не використовують великі літери, тому швидше буде int замість Int, і string замість String. Таким чином, ви можете відрізнити стандартні типи.
Уникайте імен, з якими легко допустити описку або переплутати. Наприклад

        name    names    nameS
        foo     f00
        fl      f1       fI       fi

Символи 0, o О, 1, l і I собливо часто викликають проблеми.

Часто вибір імен обмежується локальними правилами стилю. Пам’ятайте, що підтримка відповідного стилю часто є важливішою, ніж робити кожну дрібницю по-своєму.

Потрібно ставити “const” до або після типу?

Я поставив її перед, але це справа смаку. І “const T”, і “T const” були – і є – допустимими і еквівалентними. Наприклад:


        const int a = 1;      //ok
        int const b = 2;      //also ok

Я думаю, що використання першої версії менше заплутає програмістів (є “більш ідіоматичною”‘).
Чому? Коли я придумав ” const ” (який спочатку називався “readonly” і мав відповідний “writeonly”), я дозволив йому розміщуватись до або після типу, тому що я міг зробити це без двозначності. Попередній стандарт C і C++, ввели кілька (якщо такі є) упорядковуючих правил для специфікаторів.
Я не пам’ятаю жодних особливих думок або дискусій про порядок в той час. Деяким з перших користувачів – зокрема, мені – просто сподобався в той час зовнішній вигляд

const int c = 10;

більше, ніж


int const c = 10;

На мене, можливо, вплинув той факт, що мої ранні приклади були написані з використанням “readonly” і

readonly int c = 10;

читається краще, ніж


int readonly c = 10;

Найперший код (C або C++) з використанням “const”, здається, були створений (мною) при глобальній заміні “readonly” на “”.
Я пам’ятаю, що обговорював альтернативний синтаксис з декількома людьми – зокрема, з Деннісом Рітчі, – але я не пам’ятаю, на які мови тоді дивився.
Відзначимо, що в постійних покажчиках “const” завжди іде після “*”. Наприклад:

        int *const p1 = q;    //constant pointer to int variable
        int const* p2 = q;    //pointer to constant int
        const int* p3 = q;    //pointer to constant int

Що хорошого в static_cast?

Приведень, як правило, краще уникати. За винятком dynamic_cast, їхнє використання передбачає можливість помилки або вкорочення числового значення. Навіть невинне приведення може стати серйозною проблемою, якщо в процесі розробки або обслуговування, зміниться один із залучених типів. Наприклад, що це означає?

х = (T) у;

Ми не знаємо. Це залежить від типу Т і типів х та у. T може бути ім’ям класу, визначення типу, або, може бути параметром шаблона. Може бути, що х і у є скалярними змінними і (T) являє собою значення перетворення. Може бути, що х – це похідний клас від класу у, а (Т) є пригніченою. Може бути, що х і у – це пов’язані типи покажчиків. Оскільки приведення (T) в стилі С може бути використане для вираження багатьох логічно різних операцій, компілятор все одно має шанс скористатися зловживанням. З тієї ж причини, програміст не може точно знати, що робить приведення. Програмістами-початківцями це іноді розглядається як перевага, але є джерелом помилок, якщо новачок не вгадав.
“Приведення в новому стилі” були введені, щоб дати програмістам можливість заявити про свої наміри більш чітко, а компілятор при цьому мав більше помилок. Наприклад:

        int a = 7;
        double* p1 = (double*) &a;                   //ok (but a is not a double)
        double* p2 = static_cast<double*>(&a);       //error
        double* p2 = reinterpret_cast<double*>(&a);  //ok: I really mean it

        const int c = 7;
        int* q1 = &c;                 //error
        int* q2 = (int*)&c;           //ok (but *q2=2; is still invalid code and may fail)
        int* q3 = static_cast<int*>(&c);     //error: static_cast doesn't cast away const
        int* q4 = const_cast<int*>(&c);      //I really mean it

Ідея полягає в тому, що перетворення, дозволені static_cast, менш ймовірно, що призведуть до помилок, ніж ті, які вимагають reinterpret_cast. В принципі, можна використовувати результати static_cast без приведення його до початкового типу, в той час як ви завжди повинні приводити результат reinterpret_cast до його початкового типу перед початком використання для забезпечення мобільності.

Другою причиною введення нового стилю приведення,було те, що приведення в стилі C дуже важко виявити в програмі. Наприклад, ви не можете здійснити пошук приведення, використовуючи звичайний редактор або текстовий процесор. Майже невидимі приведення в стилі C є особливо поганими, тому що вони є і потенційно небезпечними. Потворний оператор буде мати потворну синтаксичну форму. Це спостереження стало однією з причин вибору синтаксису для приведень нового типу. Ще однією причиною стала відповідність нового стилю шаблонним записам, так що програмісти можуть написати свої власні приведення, особливо приведення, які перевіряються під час виконання.

Може бути, що оскільки static_cast такий потворний і його так складно друкувати, ви подумаєте двічі, перш ніж його використовувати? Було б добре, тому що насправді приведення в основному уникаються в сучасній мові C++.

Отже, що не так при використанні макросів?

Макроси не підкоряються правилам області і типу C++. Це часто призводить до невеликих і не-таких-вже-невеликих проблем. Отже, C++ надає альтернативні варіанти, які краще вписуються в решті всієї мови C++, такі як вбудовані функції, шаблони, і простір назв.
Зверніть увагу:

        #include "someheader.h"

        struct S {
               int alpha;
               int beta;
        };

Якщо хтось (нерозумно) написав макрос і назвав його “альфа”, або створив макрос під назвою “бета”, це не може призвести або (гірше) може призвести до дечого несподіваного в компіляції. Наприклад, “someheader.h” може містити:


        #define alpha 'a'
        #define beta b[2]

Правила про макроси (і тільки макроси), написані великими буквами, допоможе, але немає захисту від макросів на рівні мови. Наприклад, той факт, що імена членів були в рамках структури не допоміг: макроси працюють в програмі у вигляді потоку символів до того, як їх побачить компілятор. Це, до речі, є основною причиною, чому в C і C++ середовище розробки програм та інструменти є простими: людина і компілятор бачать різні речі.
На жаль, ви не можете припустити, що інші програмісти постійно уникають того, що ви вважаєте “дійсно тупим”. Наприклад, хтось недавно повідомив мені, що вони зіткнулися з макросом, яктй містить goto. Я також це бачив і навіть чув аргументи, які могли б прояснити сенс. Наприклад:

        #define prefix get_ready(); int ret__
        #define Return(i) ret__=i; do_something(); goto exit
        #define suffix exit: cleanup(); return ret__

        int f()
        {
               prefix;
              //...
               Return(10);
              //...
               Return(x++);
              //...
               suffix;
        }

Уявіть, що цим мав на увазі програміст; після “приховування” макроса у заголовку – що не є рідкістю – таку “магію” важко виявити.
Одною з найпоширеніших проблем є те, що функція в стилі макроса не підкоряється правилам проходження аргумента функцією. Наприклад:

        #define square(x) (x*x)

        void f(double d, int i)
        {
               square(d);    //fine
               square(i++);  //ouch: means (i++*i++)
               square(d+1);  //ouch: means (d+1*d+1); that is, (d+d+1)
              //...
        }

Проблема “d+1″ вирішується шляхом додавання дужок у “виклику” або у визначенні макросу:


#define square(x) ((x)*(x))   /* better */

Проте, залишається проблема з (імовірно ненавмисним) подвійним посиленням i++.

І – так, я знаю, що є речі, які відомі як макроси, але які не мають проблем з препроцесором макросів C/C++. Тим не менш, я не хочу займатися поліпшенням макросів C++. Замість цього я рекомендую правильно використовувати засоби мови С++, такі як вбудовані функції, шаблони, конструктори (для ініціалізації), деструктори (для очищення), винятки (для виходу з контексту) та ін..

Як ви вимовляєте ” сout “?

“Cout ” вимовляється як “see-out” (сі-аут). “С” означає ” character ” (символ), тому що карта iostreams залежить від представлення байтів (символів).

Як ви вимовляєте “char”?

“Char”(cимвол), як правило, вимовляється як “чар”, а не “кар”. Це може здатися нелогічним, оскільки “character” вимовляється як “ка-рак-тер”, але ніхто ніколи не казав, що англійська вимова і орфографія є логічними.