Настанови з практичного програмування 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. 1,0 1,1 1,2 1,3 Effective C++ Second Edition, Scott Meyers - Addison-Wesley 1998
  2. More Effective C++, Scott Meyers - Addison-Wesley, 1996
  3. How Non-Member Functions Improve Encapsulation, Scott Meyers - C/C++ Users Journal, February 2000
  4. Programming in C++, Rules and Recommendations, M. Henricson / E. Nyquist, 1992