C++ без new і delete

Привіт, хабравчане!

Мене звати Михайло Матросів, я технічний менеджер в компанії Align Technology. Сьогодні я попрацюю капітаном і трохи розповім про основи сучасного С++.

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

Мені б хотілося поговорити про ці засади і почну зі своєї улюбленої теми. Будемо говорити про операторів
new
та
delete
. А точніше про їх відсутність. Я розповім, як писати надійний і сучасний код на С++ без використання операторів
new
та
delete
.

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

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

Зображення взято з сайту behappy.me

Основна думка: намагайтеся прибрати виклики
new
та
delete
з клієнтського коду. Вони потрібні тільки у виняткових випадках, і ці випадки вимагають виняткової уваги. Наприклад, це створення власного контейнера або менеджера пам'яті.

Ми поступимо таким чином:
  • Зробимо невелике теоретичне введення.
  • Розглянемо кілька сценаріїв і покажемо, чому
    • слід уникати оператора
      delete
      ;

    • слід уникати оператора
      new
      на прикладі
      make
      -функцій;
    • слід уникати оператора
      new
      на інших прикладах.
  • Підсумуємо висновки.
Таким чином ми поступово прийдемо до розуміння, чому потрібно відмовитися від використання
new
та
delete
в клієнтському коді і поховати їх в надрах STL і boost.

Навіщо взагалі потрібні
new
та
delete
?
Операції
new
та
delete
в С++ потрібні для створення і видалення динамічних об'єктів.

Основна особливість динамічних об'єктів в тому, що часом їх життя потрібно керувати вручну. Протилежність їм з цієї точки зору складають автоматичні об'єкти, час життя яких управляє компілятор. Існують ще статичні та thread-local об'єкти, але вони нам в рамках даної статті не цікаві. См. storage duration.

Автоматичні об'єкти видаляються неявно у відповідності з чіткими правилами, які реалізовані в компіляторі. Локальні змінні функції видаляються, коли потік управління покидає область видимості, в якому вони оголошені. Члени класу видаляються після виконання деструктора цього класу.

А ось для динамічних об'єктів таких правил немає. Їх потрібно завжди видаляти явно (явне видалення може бути приховано в надрах утилітарних класів і функцій). Ось невелика ілюстрація для кращого розуміння:
struct A
{
std::string str; // Автоматичний об'єкт, неявно видаляється в деструкторе A (який згенерований 
// автоматично). Сам рядковий буфер - динамічний об'єкт (*), буде явно 
// вилучений у деструкторе std::string, який буде неявно викликаний в деструкторе A. 
// (*) Якщо рядок не дуже коротка, тоді спрацює Small String Optimization і динамічний
// буфер взагалі не буде виділено.
};

void foo()
{
std::vector < int> v; // Автоматичний об'єкт, неявно видаляється при виході з функції.
v.push_back(10); // Вміст вектора - динамічний об'єкт (масив), буде видалено деструкторе
// вектора, який буде неявно викликаний при виході з функції.

A a; // Автоматичний об'єкт класу А, неявно видаляється при виході з функції.

A* pa = new A; // Вказівник pa - автоматичний об'єкт, неявно видаляється при виході з функції,
// але він вказує на динамічний об'єкт класу А, який потрібно видалити в явному вигляді.
delete pa; // Явне видалення динамічного об'єкта.

auto upa = // Розумний покажчик upa - автоматичний об'єкт, неявно видаляється при виході з функції, 
std::make_unique<A>(); // але він вказує на динамічний об'єкт класу А, який буде явно видалений
// в деструкторе розумного покажчика.
}

Зазвичай динамічні об'єкти знаходяться в купі, хоча в загальному випадку це не так. Автоматичні об'єкти можуть перебувати як на стеку, так і в купі. У прикладі вище автоматичний об'єкт
upa->str
знаходиться в купі, оскільки він — частина динамічного об'єкта
*upa
. Тобто властивості динамічний/автоматичний визначають час життя, але не життя об'єкта.

Властивість динамічний/автоматичний належить саме об'єкта, а не типу, т. к. об'єкти одного і того ж типу можуть бути як динамічні, так і автоматичними*). У прикладі вище об'єкти
a
та
*pa
обидва мають тип А, але перший є автоматичним, а другий — динамічним.

Динамічні об'єкти в С++ створюються за допомогою
new
, а видаляється з допомогою
delete
. Ось звідси і всі проблеми: ніхто не говорив, що ці конструкції слід використовувати безпосередньо! Це низькорівневі виклики, вони як би під капотом. І не треба лізти під капот без необхідності.

Про те, навіщо взагалі може знадобитися динамічні об'єкти, ми поговоримо трохи пізніше.


* Існують техніки, щоб обмежити властивість динамічний/автоматичний на рівні типу. Наприклад, закриті конструктори.

проблема
new
та
delete
?
З самого моменту свого винаходу оператори
new
та
delete
використовуються невиправдано часто. Найбільші проблеми відносяться до оператора
delete
:
  • Можна взагалі забути викликати
    delete
    (витік пам'яті, memory leak).
  • Можна забути викликати
    delete
    у випадку виключення або дострокового повернення з функції (теж витік пам'яті).
  • Можна викликати
    delete
    двічі (подвійне видалення, double delete).
  • Можна викликати не ту форму оператора:
    delete
    замість
    delete[]
    або навпаки (невизначений поведінка, undefined behavior).
  • Можна використовувати об'єкт після виклику
    delete
    (dangling pointer).
Всі ці ситуації приводять у кращому випадку до падінь програми, а в гіршому до витоків пам'яті та назальним демонам.

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

Тепер перейдемо до сценаріїв використання new і delete. Нагадаю, що ми розглянемо кілька сценаріїв і планомірно покажемо, що в більшості з них код стане краще, якщо відмовитися від використання new і delete.

Почнемо з простого — з динамічних масивів.

Динамічні масиви
Динамічний масив — це масив з елементами, виділеними в динамічній пам'яті. Він необхідний у випадку, якщо розмір невідомий на етапі компіляції, або якщо розмір досить великий, і ми не хочемо виділяти масив на стеку, розмір якого зазвичай сильно обмежений.

Для виділення динамічних масивів С++ на низькому рівні надає векторну форму операторів
new
та
delete
:
new[]
та
delete[]
. В якості прикладу розглянемо деяку функцію, яка працює з зовнішнім буфером:
void DoWork(int* buffer, size_t bufSize);

Подібні функції часто зустрічаються в бібліотеках з API на чистому*. Нижче наведено приклад того, як може виглядати використовує її код. Це поганий код, т. к. він в явному вигляді використовує
delete
, а пов'язані з ним проблеми ми вже описали вище.
void Call(size_t n)
{
int* p = new int[n];
DoWork(p, n);
delete[] p; // Погано!
}

Тут все просто і більшості відомо, що для подібних цілей в С++ слід використовувати стандартний контейнер
std::vector
**. Він сам виділить пам'ять в конструкторі і звільнить її в деструкторе. До того ж, він ще може змінювати свій розмір під час життя, але для нас це зараз не має значення. З використанням вектора код буде виглядати так:
void Call(size_t n)
{
std::vector < int> v(n); // Краще.
DoWork(v.data(), v.size());
}

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

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


* На С++ подібний інтерфейс слід було б реалізувати з використанням типу
span<int>
. Він надає уніфікований STL-сумісний інтерфейс для доступу до безперервним послідовностям елементів, при цьому ніяк не впливаючи на їх час життя (невладеющая семантика).

** Бо цю статтю читають програмісти на С++, я майже впевнений, що хтось подумає: «Ха!
std::vector
зберігає в собі цілих три (!) покажчика, коли старий добрий
int*
— це за визначенням всього один покажчик. Наявна перевитрата пам'яті і декількох машинних інструкцій на їх ініціалізацію! Це неприйнятно!». Майерс відмінно прокоментував це властивість програмістів на С++ в своїй доповіді Why C++ Sails When the Vasa Опустив. Якщо для вас це справді проблема, то можу порекомендувати
std::unique_ptr<int[]>
, а в майбутньому стандарт може подарувати нам
dynarray
.

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

Коли динамічний об'єкт створений, хто-то повинен його видалити, і умовно типи об'єктів можна розділити на дві групи: ті, які ніяк не обізнані у процесі свого видалення, і ті, які щось підозрюють. Будемо говорити, що перші мають стандартну модель управління пам'яттю, а другі — нестандартну.

До типів зі стандартною моделлю управління пам'яттю відносяться всі стандартні типи**, включаючи контейнери. У самому справі, контейнер управляє пам'яттю, яку він виділив сам. Йому немає ніякого діла до того, хто його створив і як він буде видалений.

До типів з нестандартною моделлю управління пам'яттю можна віднести, наприклад, об'єкти Qt. Тут у кожного об'єкта є батько, який відповідальний за його видалення. І об'єкт про це знає, т. к. він успадковується від класу
QObject
. Сюди ж відносяться типи з лічильником посилань, наприклад, розраховані на роботу з
boost::intrusive_ptr
.

Іншими словами, тип зі стандартною моделлю управління пам'яттю не надає ніяких додаткових механізмів для управління своїм часом життя. Цим цілком і повністю повинна займатися спеціальна сторона. А ось тип з нестандартною моделлю такі механізми надає. Наприклад,
QObject
має методи
setParent()
та
children()
і містить у собі список дітей, а тип
boost::intrusive_ptr
спирається на функції
intrusive_ptr_add_ref
та
intrusive_ptr_release
і містить у собі лічильник посилань.

Якщо тип об'єкта має стандартну модель управління пам'яттю, то будемо для стислості говорити, що це об'єкт зі стандартним управлінням пам'яттю. Аналогічно, якщо тип об'єкта має нестандартну модель управління пам'яттю, то будемо говорити, що це об'єкт з нестандартним управлінням пам'яттю.

Далі розглянемо об'єкти обох моделей. Забігаючи вперед, варто сказати, що для об'єктів зі стандартним управлінням пам'яттю однозначно не варто використовувати
new
та
delete
в клієнтському коді, а для об'єктів з нестандартним — залежить від конкретної моделі.


* Деякі винятки: ідіома
pimpl;
дуже великий об'єкт (наприклад, буфер пам'яті).

** Виняток становить
std::locale::facet
(див. далі).

Динамічні об'єкти зі стандартним управлінням пам'яттю
Такі найчастіше зустрічаються на практиці. І саме їх слід намагатися використовувати в сучасному С++, тому як з ними працюють стандартні підходи, використовувані зокрема в розумних покажчиках.

Власне, розумні покажчики, так, це відповідь. Саме їм слід віддати управління часом життя динамічних об'єктів. Їх в З++ цілих два:
std::shared_ptr
та
std::unique_ptr
. Не будемо тут виділяти
std::weak_ptr
, т. к. це просто помічник для
std::shared_ptr
в певних сценаріях використання.

Що стосується
std::auto_ptr
, він був офіційно виключений з С++ починаючи з С++17. Спочивай з миром!

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

Тобто замість ось такого:
std::unique_ptr<Cookie> cookie(new файлів Cookie dough, sugar, cinnamon));

слід писати ось так:
auto cookie = std::make_unique<Cookie>(dough, sugar, cinnamon);

Переваги
make
-функцій над явним створенням розумних покажчиків чудово описані Гербом Саттером в його GotW #89 і Скоттом Майерсом в його Effective Modern C++ Item 21. Не буду повторюватися, лише наведу тут короткий список тез:
  • Для обох
    make
    функції:
    • Безпека з точки зору винятків.
    • Немає дублювання імені типу.
  • std::make_shared
    :
    • Виграш в продуктивності, оскільки контрольний блок виділяється поряд із самим об'єктом, що зменшує кількість звернень до менеджера пам'яті і збільшує локальність даних. Оптимізація We Know Where You Live.
У make-функцій є і ряд обмежень, докладно описаних у тих же джерелах:
  • Для обох
    make
    функції:
    • не Можна передати свій
      deleter
      . Це цілком логічно, оскільки всередині себе
      make
      -функції визначення використовують стандартний
      new
      .
    • не Можна використовувати
      braced initializer
      , а також всі інші тонкощі, пов'язані з perfect forwarding (див. Effective Modern C++, Item 30).
  • std::make_shared
    :
    • Потенційний перевитрата пам'яті для великих об'єктів при довгоіснуючих слабких посиланнях (
      std::weak_pointer
      ).
    • Проблеми з операторами
      new
      та
      delete
      переопределенными на рівні класу.
    • Потенційне помилкове поділ (false sharing) між об'єктом і контрольним блоком (див. питання на StackOverflow).
На практиці зазначені обмеження зустрічаються рідко і не применшують переваг. Виходить, що розумні покажчики приховали від нас виклик
delete
, а
make
-функції приховали від нас виклик
new
. В результаті ми отримали більш надійний код, в якому немає ні
new
ні
delete
.


До речі, пристрій
make
-функцій серйозно розкриває у своїх доповідях Стефан Лававей (a.k.a. STL). Наведу тут красномовний слайд з його доповіді don't Help the Compiler:

Динамічні об'єкти з нестандартним управлінням пам'яттю
Крім стандартного підходу до управління пам'яттю через розумні покажчики зустрічаються і інші моделі. Наприклад, підрахунок кількості посилань (reference counting) і відносини батько-дитина (parent to child relationship).

Далі розглянемо кілька прикладів з різною моделлю пам'яті і спробуємо зробити наше життя легше за рахунок позбавлення від
new
та
delete
. Десь у нас це вийде, а де-то немає.

Динамічні об'єкти з підрахунком посилань


Дуже часто зустрічається прийом, використовуваний у багатьох бібліотеках. Розглянемо в якості прикладу бібліотеку OpenSceneGraph. Це відкритий багатоплатформовий 3D-движок, написаний на С++ і OpenGL.

Велика частина класів в ньому успадковується від класу
osg::Referenced
, який здійснює всередині себе підрахунок посилань.
Метод ref()
збільшує лічильник, метод
unref()
зменшує лічильник і видаляє об'єкт, коли лічильник опускається до нуля.

У комплекті також йде розумний покажчик
osg::ref_ptr<T>
, який викликає метод
T::ref()
для зберігається об'єкта в своєму конструкторі і метод
T::unref()
в деструкторе. Такий же підхід використовується в
boost::intrusive_ptr
, тільки там замість методів
ref()
та
unref()
виступають зовнішні функції.

Розглянемо фрагмент коду, який наведено в офіційному керівництві OpenSceneGraph 3.0: beginner's guide:
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
// ... 
osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
// ... 
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices.get());
geom->setNormalArray(normals.get());
// ... 

Дуже знайомі конструкції виду
osg::ref_ptr<T> p = new T
. Абсолютно аналогічно тому, як функції
std::make_unique
та
std::make_shared
служать для створення класів
std::unique_ptr
та
std::shared_ptr
, ми можемо написати функцію
osg::make_ref
для створення класу
osg::ref_ptr
. Робиться це дуже просто, за аналогією з функцією
std::make_unique
:
namespace osg
{
template < typename T, typename... Args>
osg::ref_ptr<T> make_ref(Args&&... args)
{
return new T(std::forward<Args>(args)...);
}
}

Перепишемо цей фрагмент коду озброївшись нашою новою функцією:
auto vertices = osg::make_ref<osg::Vec3Array>(); 
// ... 
auto normals = osg::make_ref<osg::Vec3Array>(); 
// ... 
auto geom = osg::make_ref<osg::Geometry>();
geom->setVertexArray(vertices.get());
geom->setNormalArray(normals.get());
// ... 

Зміни тривіальні і легко можуть бути виконані автоматично. Таким нехитрим способом ми отримуємо безпеку з точки зору винятків*, відсутність дублювання імені типу і прекрасне відповідність стандартного стилю.

Виклик
delete
вже був захований в методі
osg::Referenced::unref()
, а тепер ми сховали і виклик
new
у функції
osg::make_ref
. Так що ніяких
new
та
delete
.



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

Динамічні об'єкти для немодальных діалогів в MFC


Розглянемо приклад, специфічний для бібліотеки MFC. Це обгортка з класів С++ над Windows API. Вона використовується для спрощення розробки GUI під Windows.

Цікавий прийом, яким Microsoft офіційно рекомендує користуватися для створення немодальных діалогів. Т. к. діалог немодальний, не зовсім ясно, хто відповідальний за його видалення. Пропонується йому видаляти себе самого в переопределенном метод
CDialog::PostNcDestroy()
. Цей метод викликається після обробки повідомлення
WM_NCDESTROY
— останнього повідомлення, одержуваного вікном у його життєвому циклі.

У прикладі нижче діалог створюється за допомогою натискання на кнопку в методі
CMainFrame::OnBnClickedCreate()
і видаляється в переопределенном метод
CMyDialog::PostNcDestroy()
.
void CMainFrame::OnBnClickedCreate()
{
auto* pDialog = new CMyDialog(this);
pDialog->ShowWindow(SW_SHOW);
}

class CMyDialog : public CDialog
{
public:
CMyDialog(CWnd* pParent)
{
Create(IDD_MY_DIALOG, pParent);
}

protected:
void PostNcDestroy() override
{
CDialog::PostNcDestroy();
delete this;
}
};

Тут у нас не захований ні виклик
new
, ні виклик
delete
. Способів вистрілити собі в ногу — маса. Крім звичайних проблем з покажчиками, можна забути змінити у своєму діалозі метод
PostNcDestroy()
, отримаємо витік пам'яті. При вигляді виклику
new
, може виникнути бажання самостійно викликати в певний момент
delete
, отримаємо подвійне видалення. Можна випадково створити об'єкт діалогу в автоматичній пам'яті, знову отримаємо подвійне видалення.

Спробуємо заховати виклики до
new
та
delete
проміжного класу
CModelessDialog
і фабрики
CreateModelessDialog
, які будуть відповідати в нашому додатку за немодальному діалоги:
class CModelessDialog : public CDialog
{
public:
CModelessDialog(UINT nIDTemplate, CWnd* pParent)
{
Create(nIDTemplate, pParent);
}

protected:
void PostNcDestroy() override
{
CDialog::PostNcDestroy();
delete this;
}
};

// Фабрика для створення модальних діалогів
template < class Derived, typename... Args>
Derived* CreateModelessDialog(Args&&... args)
{
// Замість static_assert в тілі функції, можна використовувати std::enable_if в її заголовку, що дозволить нам використовувати SFINAE. 
// Але оскільки навряд чи очікуються інші перевантаження цієї функції, розумним виглядає використати просте і наочне рішення.
static_assert(std::is_base_of<CModelessDialog, Derived>::value, 
"CreateModelessDialog should be called for descendants of CModelessDialog");
auto* pDialog = new Derived(std::forward<Args>(args)...);
pDialog->ShowWindow(SW_SHOW);
return pDialog;
}

Клас сам перевизначити метод
PostNcDestroy()
, в якому ми сховали
delete
, а для створення класів спадкоємців використовується фабрика, в якій ми сховали
new
. Створення та визначення класу спадкоємця тепер виглядає так:
void CMainFrame::OnBnClickedCreate()
{
CreateModelessDialog<CMyDialog>(this);
}

class CMyDialog : public CModelessDialog
{
public:
CMyDialog(CWnd* pParent) : CModelessDialog(IDD_MY_DIALOG, pParent) {}
};

Звичайно, подібним чином ми не вирішили всіх проблем. Наприклад, об'єкт все одно можна виділити на стеку і отримати подвійне видалення. Заборонити виділення об'єкта на стеку можна тільки шляхом модифікації самого класу об'єкта, наприклад додаванням закритого конструктора. Але ми ніяк не можемо цього зробити базового класу
CModelessDialog
. Можна, звичайно, взагалі приховати клас
CMyDialog
і зробити фабрику не шаблонно, а більш класичної, приймаючої деякий ідентифікатор класу. Але це все вже виходить за рамки статті.

Так чи інакше, ми спростили створення діалогу з клієнтського коду та написання нового класу діалогу. І при цьому ми прибрали з клієнтського коду виклики
new
та
delete
.


Динамічні об'єкти з відношенням батько-дитина


Зустрічаються досить часто, особливо в бібліотеках для розробки GUI. В якості прикладу розглянемо Qt — добре відому бібліотеку для розробки додатків і UI.

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

Відмінний приклад ситуації, коли позбутися
new
та
delete
так просто не вийде. Бібліотека проектувалася таким чином, що ці оператори можна і потрібно застосовувати в багатьох випадках. Я пропонував обгортку для створення об'єктів з ненульовим батьком, але ідея не пішла (див. обговорення в Qt mailing list).

Таким чином, мені невідомий хороший спосіб позбутися від new та delete в Qt.

Динамічні об'єкти
std::locale::facet


Для управління виведенням даних на потоки у С++ використовуються об'єкти
std::locale
. Локаль є набором фасетів (facet), які визначають спосіб виводу тих чи інших даних. Фасети мають свій лічильник посилань при копіюванні локалей не відбувається копіювання фасетів, копіюється лише вказівник і збільшується лічильник посилань.

Локаль сама відповідальна за видалення фасетів, коли лічильник посилань падає до нуля, але ось створювати фасети повинен користувач, використовуючи оператор new (див. секцію Notes в описі конструктора
std::locale)
:
std::locale default;
std::locale myLocale(default, new std::codecvt_utf8<wchar_t>);

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

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


Висновок
Отже, спочатку ми розглянули такі сценарії, як створення динамічних масивів і динамічних об'єктів зі стандартним управлінням пам'яттю. Замість
new
та
delete
ми використали стандартні контейнери і
make
-функції і отримали більш простий і надійний код.

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

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

Список рекомендацій:
  • Уникайте використання
    new
    та
    delete
    в коді. Сприймайте їх як низькорівневі операції ручного управління динамічною пам'яттю.
  • Використовуйте стандартні контейнери для динамічних структур даних.
  • Використовуйте
    make
    -функції для створення динамічних об'єктів, коли це можливо.
  • Створюйте обгортки для об'єктів з нестандартною моделлю пам'яті.
Від автора
Особисто мені доводилося стикатися з безліччю випадків витоків пам'яті та падінь з-за надмірного використання
new
та
delete
. Так, велика частина такого коду була написана багато років тому, але потім з ним починають працювати молоді програмісти й думають, що ось так і треба писати.

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

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

PS В процесі обговорення статті, у нас з колегами розгорівся цілий спір, як правильно: «Майерс» або «Мейерс». З одного боку, для російського слуху більш звично звучить «Мейерс», і ми самі начебто завжди говорили саме так. З іншого боку, на вікі використовується саме «Майерс». Якщо подивитися локалізовані книги, то там взагалі хто на що здатний: до цих двох варіантів додається ще й «Мейерс». На конференціях різні люди представляють його по-різному. В кінцевому підсумку нам вдалося з'ясувати, що сам себе він називає саме «Майерс», на чому і порішили.

Посилання
  1. Herb Sutter, ,GotW #89 Solution: Smart Pointers.
  2. Scott Meyers, ,Effective Modern C++, Item 21, p. 139.
  3. Stephan T. Lavavej, ,don't Help the Compiler.
  4. Bjarne Stroustrup, ,The C++ Programming Language, 11.2.1, p. 281.
  5. Five Popular Myths about C++., Part 2
  6. Mikhail Matrosov, ,C++ without new and delete.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.