Ця стаття є перекладом C++ Programming Practice Guidelines, поширюється з дозволу автора.
Вступ.
Настанови з програмування на C++ часто змішують рекомендації до стилю, практичні поради з програмування і питання дизайну програми, що може заплутати. Цей документ фокусується переважно на добрих порадах з програмування. Щоб отримати настанови зі стилю програмування на C++, зверніться до відповідних настанов. За настановами з дизайну програми, зверніться до праць про шаблони програмування, об'єктно-орієнтований дизайн програм і т.д.
Рекомендації в цьому документі базовані на працях Скотта Мейерса Effective C++ Second Edition, Scott Meyers - Addison-Wesley 1998 More Effective C++, Scott Meyers - Addison-Wesley, 1996 How Non-Member Functions Improve Encapsulation, Scott Meyers - C/C++ Users Journal, February 2000 і інших джерелах.
Як влаштовані ці поради.
Поради згруповані за темами і кожна порада пронумерована, що робить легшим посилання на неї при переглядах.
Поради влаштовані так:
n. Загальний опис поради. Приклад, якщо можливо Мотивація, походження і додаткова інформація.
Мотивація також важлива. Стандарти і поради щодо програмування мають властивість розпочинати "холівари", і важливо зазначити їхнє походження.
Ступінь важливості порад.
Поради зґруповані за темами і кожна порада пронумерована для полегшення посилань на них.
В цих настановах слова , та мають особливе значення. Треба означає, що поради треба дотримуватися, слід - наполеглива рекомендація, а може - загальна настанова.
Перехід з C на C++.
1. Слід уникати #define const double PI = 3.1415; // А не: #define PI 3.1415 #define - не частина мови C++, і оскільки C++ надає можливості, що роблять #define надмірним, треба віддати перевагу саме їм.
Замість констант #define слід використовувати змінні з модифікатором const, а ще краще - використовувати функції-члени, щоб отримати константи.
Замість безтипових макросів користуйтеся шаблонізованими функціями.
2. Слід надавати перевагу бібліотеці iostream перед stdio.h #include <iostream> // А не: #include <stdio.h> Бібліотека stdio (примітка перекладача: в C++11 - cstdio) замінена значно потужнішою біблотекою iostream, і при програмуванні на C++ слід надати перевагу останній.
3. Слід надавати перевагу перетворенням типів в стилі C++ перед перетвореннями в стилі C. static_cast<double> intValue; // А не: (double) intValue
4. Слід використовувати 0 замість NULL. NULL - частина стандартної бібліотеки C, застаріла в C++.
Примітка перекладача: з появою стандарту C++11 введене нове ключове слово - nullptr, яке і слід застосовувати замість NULL чи 0 в операціях з вказівниками.
5. слід надавати перевагу перед вказівниками. Вказівники слід використовувати тоді і тільки тоді, коли можлива передача 0 чи nullptr замість об'єкта.
Приклад: якщо як об'єкт моделюється особа, то її батьків слід робити посиланнями на особи (бо батьки є у всіх), а дітей слід робити вказівниками на особи.
Примітка перекладача: правило суперечливе, приклад взагалі недоречний: така структура буде або нескінчено великою (бо в батьків будуть свої батьки, а в тих свої і т.д.), або потребуватиме додаткові об'єкти в якості "невідомих батьків" - замість яких, щоб уникнути непорозумінь, значно краще використати nullptr.
6. Слід використовувати const всюди, де це є можливим. Ключове слово const допомагає документуванню.
Зокрема, функції-члени, які не впливають на стан об'єкта, мають бути оголошені як const. Тільки такі функції можуть застосовуватися до об'єктів з модифікатором const.
Примітка перекладача: в C++11 введені також посилання на rvalue. Поки ще зарано казати про загальноприйняті практики використання цієї можливості, але її також треба розглядати як альтернативу класичному const&.
7. Слід уникати передачі об'єктів за значенням. myMethod (const SomeClass &object) // А не: myMethod (SomeClass object) Для цього є дві підстави. Перша - швидкодія. Передача за значенням для об'єктів завжди викликає створення тимчасового об'єкта за допомогою конструктора копіювання і знищення наприкінці метода.
Друга причина в тому, що об'єкти, передані за значенням через змінну базового класу будуть ефективно поводитися як об'єкт базового класу, без розширеної інформації, визначеної в похідному класі.
8. Слід уникати невизначених списків аргументів (...).
Невизначені списки аргументів унеможливлюють сувору перевірку типів, що діє в C++. В більшості випадків невизначені списки аргументів можна замінити перевантаженням функцій і за допомогою типових аргументів.
9. Слід використовувати new та delete замість malloc, realloc та free.
C++ не потребує старих функцій роботи з пам'яттю malloc, realloc та free. Використання тільки одного набору методів роботи з пам'яттю покращує читаність коду. Крім того, це убезпечує звільнення пам'яті, оскільки небезпечно виконувати delete з об'єктом, стореним malloc-ом чи free - зі створеним за допомогою new.
Конструктори, деструктори і оператори присвоювання.
10. Слід завжди визначати конструктор копіювання і оператор присвоювання для класів з динамічно розподіленою пам'яттю, Item 11).
Якщо конструктор копіювання і оператор присвоювання не будуть визнчені явно, компілятор створить їх автоматично. Якщо клас використовує динамічно розподілену пам'ять, типово згенеровані конструктор копіювання і оператор присвоювання швидше за все не будуть поводитися, як очікували. Виняток - коли багато об'єктів одного класу справді використовують спільну область даних. В цьому випадку треба переконатися, що спільні дані не будуть звільнені, поки на них є посилання.
11. Оператором присвоювання завжди слід повертати посилання на *this. MyClass& MyClass::operator= (const MyClass& rhs) { ... return *this; } Це робиться, щоб забезпечити поведінку оператора присвоювання, аналогічну до присвоювання вбудованих типів.
Зокрема, такий вираз буде можливим і змістовним: MyClass a, b, c; a = b = c;
12. Оператору присвоювання слід завжди перевіряти, чи не відбувається присвоювання собі самому. MyClass& MyClass::operator= (const MyClass& rhs) { if (this != &rhs) { ...
return *this; }
MyClass& MyClass::operator= (const MyClass& rhs) { if (*this != rhs) { ...
return *this; }
Яку саме версію з наведених обрати, залежить від класу. Перша версія просто перевіряє, що rhs і this вказують на одне місце в пам'ті; цього, зазвичай, досить. Другий перевіряє рівність за допомогою оператора == (за умови, що він визначений).
13. Оператору присвоювання похідного класа треба явно виконувати присвоювання базового класу. Derived& Derived::operaor (const Derived& rhs) { if (this != &rhs) { Base::operator= (rhs);
... // Then do assignment of own stuff
return *this; } Присвоювання базового класу ніколи не буде автоматично викликане, як хтось міг подумати. Деякі компілятори не приймають наведену вище конструкцію, якщо оператор присвоювання базового класу згенерований автоматично. В такому разі робіть так: static_cast(*this) = rhs; це приведе this до базового класу і примусить виконати присвоювання для базового класу.
14. Слід надати ініціалізації перевагу перед присвоєнням в конструкторах.
Навіть хоча синтаксис ініціалізації стоїть під питанням, є переваги у його використанні перед присвоюванням в конструкторі.
15. Деструктору слід бути віртуальним тоді і тільки тоді, коли в класа є віртуальні методі.
Коли delete застосовується до похідного об'єкта, а деструктор базового класу не віртуальний, буде викликаний тільки деструктор базового класу.
З іншого боку, якщо в класу немає віртальних членів, їх взагалі не слід використовувати в базовому класі. В цих випадках немає сенсу оголошувати обидва деструктори віртуальними, бо це непотрібно і це збільшує кількість об'єктів класів з непотрібними таблицями віртуальних членів.
Оператори.
16. Слід реалізовувати обидва оператора == і !=, якщо потрібен один з них. bool C::operator!= (const C& lhs) { return !(this == lhs); }
В більшості випадків оператор != може бути виведений з оператора ==, як показано в прикладі. Але це не робиться автоматично компілятором, як можна було б очікувати.
17. Оператори &&, || та , ніколи не слід перевантажувати<ref="More Effective C++" />
Проблемам в користувацьких версіях цих операторів полягає в тому, що вони не відтворять поведінки стандартних версій для стандартних типів.
С++ використовує спрощене обчислення булівських виразів, тобто після визначення правдивості чи хибності виразу, обчислення виразу припиняється. Цю поведінку неможливо перенести на користувацькі оператори.
Примітка перекладача: про перевантажування оператора , не сказано, але очевидно, що це призведе до купи неймовірно складних змін в поведінці програми - наприклад, виклик функції з двома параметрами перетвориться на виклик функції з одним параметром.
Успадкування.
18. Відношення слід моделювати успадкуванням, слід моделювати вміщенням
class B : public A // B "є" A { ...
class B { ... private: A a_; // B "має" A }
19. Невіртуальні методи ніколи не треба перевизначати в підкласах.
Для цього є дві підстави. По-перше, якщо метод потребує перевизначення, то підклас не слід успідковувати від базового класу. Для нього не можна сказати, що він "є" базовим класом. Крім того, ще є технічна підстава. Невіртуальна функція зв'язується статично і посилання на базовий клас завжди викличе метод базового класу навіть якщо об'єкт успадковано від того класу.
20. Успадковані типові параметри ніколи не треба перевизначати.
З тих самих міркувань, що й попереднє правило.
21. Треба уникати приватного успадкування.
class C // А не: class C : private B { // { ... // ... private: // } B b_; }
В той час як публічне успадкування є відношенням "є", приватне успадкування взагалі нічого не моделює, і є виключно конструкцією для спільного використання кода з успадкованим класом. Це краще досягається вміщенням.
Треба також уникати захищеного успадкування.
22. Треба уникати перетворення до базового класу. derived = static_cast<DerivedClass*> base;// Не треба!
Потреба в перетворенні до базового класу виявляє помилку планування. Правильно записаний код на C++ ніколи не має галузитися за типом об'єктів ("якщо об'єкт A є типу такого-то зробити те, інакше зробити се"). Користуйтеся віртуальними функціями.
Виняткові ситуації.
23. Виняткові ситуації слід ловити за посиланням. try { ... } catch (Exception& exception) { ... }
Різне.
24. Робіть акуратне розрізнення між методами-членами, методами-не членами і методами-друзями.
Віртуальні методи треба робити членами Оператори << і >> ніколи не мають бути членами. Якщо їм потрібен доступ до непублічних членів класу, вони мають бути друзями. Якщо метод потребує перетворення типу найлівішого аргументу, такий метод треба робити нечленом. Якщо на додачу він потребує доступу до непублічних членів класу, його слід оголосити другом. Все інше слід робити членами.
25. Неявно згенеровані методи, які не слід використовувати, треба явно заборонити. class C { ...
private: C& operator= (const C& rhs); // Не визначайте } Оголосивши такі методи приватними і пропустивши їхнє визначення, спроби їх викликати будуть зупинені компілятором. Ось неявно генеровані компілятором методи, якщо їх не визначити явно:
Типовий конструктор (C::C()) Типовий конструктор (C::C (const C& rhs)) Деструктор (C::~C()) Оператор присвоювання (C& C::operator= (const C& rhs)) Оператор адреси (C* C::operator&()) Константний оператор адреси (const C* C::operator&() const;)
26. Слід надавати перевагу сінглетонам перед глобальними змінними. class C { public: static const C* getInstance()
if (!instance_) instance_ = new C; return instance_;
private: C(); static C *instance_; // Defined in the source file } Сінглетони вирішують проблему невизначеного порядку ініціалізації глобальних об'єктів, що може призвести до посилань на неініціалізовані об'єкти.
В цілому, немає жодної потреби у використанні глобальних об'єктів в C++.