Настанови з практичного програмування C++
Ця стаття є перекладом C++ Programming Practice Guidelines, поширюється з дозволу автора.
Вступ.
[ред.]Настанови з програмування на C++ часто змішують рекомендації до стилю, практичні поради з програмування і питання дизайну програми, що може заплутати. Цей документ фокусується переважно на добрих порадах з програмування. Щоб отримати настанови зі стилю програмування на C++, зверніться до відповідних настанов. За настановами з дизайну програми, зверніться до праць про шаблони програмування, об'єктно-орієнтований дизайн програм і т.д.
Рекомендації в цьому документі базовані на працях Скотта Мейерса [1] [2] [3] і інших джерелах.
Як влаштовані ці поради.
[ред.]Поради згруповані за темами і кожна порада пронумерована, що робить легшим посилання на неї при переглядах.
Поради влаштовані так:
n. Загальний опис поради.
Приклад, якщо можливо
Мотивація, походження і додаткова інформація.
Мотивація також важлива. Стандарти і поради щодо програмування мають властивість розпочинати "холівари", і важливо зазначити їхнє походження.
Ступінь важливості порад.
[ред.]Поради зґруповані за темами і кожна порада пронумерована для полегшення посилань на них.
В цих настановах слова треба, слід та може мають особливе значення. Треба означає, що поради треба дотримуватися, слід - наполеглива рекомендація, а може - загальна настанова.
Перехід з C на C++.
[ред.]1. Слід уникати #define
const double PI = 3.1415; // А не: #define PI 3.1415
#define - не частина мови C++, і оскільки C++ надає можливості, що роблять #define надмірним, треба віддати перевагу саме їм.
Замість констант #define слід використовувати змінні з модифікатором const, а ще краще - використовувати функції-члени, щоб отримати константи[1].
Замість безтипових макросів користуйтеся шаблонізованими функціями.
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. Слід завжди визначати конструктор копіювання і оператор присвоювання для класів з динамічно розподіленою пам'яттю[1], 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. Деструктору слід бути віртуальним тоді і тільки тоді, коли в класа є віртуальні методі[1].
Коли 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++.
27. Функції, які можна реалізувати за допомогою публічного інтерфейсу класа, не слід робити членами.
28. Публічним функціям-членам не треба повертати неконстантне посилання чи вказівник на дані члена[4].
Повернення неконстантного посилання на дані члена порушує інкапсуляцію класа.
29. Завжди треба явно позначати тип, що функція повертає.
int function() // А не: function()
{ // {
... // ...
} // }
Функції, для яких не вказано явно тип, що вони повертають, неявно отримує int в якості такого типу. Ніколи не треба на це покладатися.
Посилання
[ред.]- ↑ 1,0 1,1 1,2 1,3 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
- ↑ Programming in C++, Rules and Recommendations, M. Henricson / E. Nyquist, 1992