Розумні вказівники

Розумний вказівник (англ. Smart pointer) — в програмуванні, це абстрактний тип даних, який імітує вказівник з допоміжними можливостями, такими як автоматичне керування пам'яттю або перевірку виходу за межі виділеної пам'яті.

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

Призначення

Необережне поводження з вказівниками, є основним джерелом програмних помилок[джерело?]. Розумні вказівники запобігають виникненню більшості ситуацій з витоками пам'яті роблячи процедуру звільнення пам'яті автоматичною. В загальному випадку, вони роблять автоматичною деструкцію об'єкта: об'єкт, що перебуває під контролем розумного вказівника, знищується автоматично коли останній (або єдиний) власник об'єкта був знищений, наприклад, тому що власник був локальною змінною, яка вийшла за рамки області видимості (блоку). Розумні вказівники також унеможливлюють ситуації з висячими вказівниками за допомогою відкладеної деструкції, коли об'єкт більше не використовується в програмі.

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

Розумні вказівники в C++

Огляд

C++ Core Guidelines та низка авторів радять замість простих вказівників використовувати «розумні» або шаблону RAII[1][2].

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

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

some_type* ambiguous_function(); // Що треба робити з результатом?

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

unique_ptr<some_type> obvious_function1();

Функція робить явним те, що її користувач відповідальний за звільнення пам'яті результату. Крім того, не станеться витоку пам'яті, навіть якщо програміст не зробить жодної дії. У версіях до появи стандарту C++11, unique_ptr можна замінити на auto_ptr.

auto_ptr

Шаблонний клас auto_ptr був першою реалізацією розумних вказівників у C++. Однак, він мав низку недоліків, зокрема, ним не можна користуватись в контейнерах стандартної бібліотеки шаблонів C++[3].

Конструктор копій і оператор присвоювання класу std::auto_ptr не виконують фактичне копіювання вказівника, який зберігає клас. Замість того, вони передають його значення, залишаючи первісний об'єкт std::auto_ptr порожнім. Це один із способів реалізації правила чіткого володіння, при якому лише один екземпляр std::auto_ptr може володіти вказівником в конкретний момент часу. Таким чином, std::auto_ptr не повинен використовуватись у коді, де необхідно мати семантику копіювання[4].

У версіях C++11 та C++14 цей клас був позначений як застарілий (англ. deprecated), а у версії C++17 його взагалі було прибрано зі стандартної бібліотеки[3].

На заміну, у C++11 було додано реалізації розумних вказівників unique_ptr та shared_ptr[3].

unique_ptr

C++11 забезпечує шаблонний клас std::unique_ptr, оголошений в файлі заголовку <memory>[5].

C++11 має підтримку семантики переміщення яка дозволяє явно здійснити переміщення значень у вигляді окремої операції, на відміну від операції копіювання. Також C++11 дозволяє явно захистити об'єкт від копіювання. Оскільки std::auto_ptr уже був створений із його семантикою копіювання, її не можна змінити без порушення зворотної сумісності коду. Для цих цілей, в C++11 запропонований новий тип вказівника std::unique_ptr.

Цей вказівник має свій конструктор копій, а оператор присвоювання явно не доступний; він не може бути скопійований. Він може бути переміщений із допомогою std::move, що дозволяє вказівнику unique_ptr передати у власність об'єкт іншому вказівнику.

std::unique_ptr<int> p1(new int(5));
std::unique_ptr<int> p2 = p1; //Помилка компіляції
std::unique_ptr<int> p3 = std::move(p1); //Передає об'єкт. p3 тепер є власником пам'яті, а p1 став не валідним.

p3.reset(); //Вивільняє пам'ять
p1.reset(); //Не робить нічого.

shared_ptr і weak_ptr

C++11 включає в себе класи shared_ptr і weak_ptr, які основані на аналогічних версіях, створених у бібліотеці Boost. TR1 запропонував їх як стандарт, але в C++11 до них додали додаткового функціоналу в порівнянні з Boost.

Клас std::shared_ptr підраховує посилання на об'єкт, власником якого він є. Кожна копія одного shared_ptr містить однаковий вказівник. Пам'ять за тим вказівником буде звільнена, лише коли всі екземпляри shared_ptr в програмі будуть видалені.

std::shared_ptr<int> p1(new int(5));
std::shared_ptr<int> p2 = p1; //Обидва вказують на одну область пам'яті.

p1.reset(); //Дані в пам'яті зберігаються, завдяки існуванню p2.
p2.reset(); //Вивільняє пам'ять, оскільки більше ніхто не посилається на неї.

Клас std::shared_ptr використовує підрахунок посилань, тому потенційною проблемою для таких вказівників є циклічні посилання. Аби уникнути появи таких циклів, для доступу до об'єктів можна користуватися класом std::weak_ptr. Збережений об'єкт буде видалений лише у випадку коли власниками об'єкту є екземпляри weak_ptr. Тобто, weak_ptr не гарантує, що об'єкт продовжить існувати, але він може здійснити запит до ресурсу.

std::shared_ptr<int> p1(new int(5));
std::weak_ptr<int> wp1 = p1; //p1 є власником об'єкта в пам'яті.
{
  std::shared_ptr<int> p2 = wp1.lock(); //тепер p1 і p2 є власниками пам'яті.
  if(p2) // оскільки p2 був визначений як слабкий вказівник, вам слід перевірити чи існує досі об'єкт в пам'яті!
  {
    //Дії з p2
  }
} //p2 видалений. Посилання на пам'ять міститься в p1.

p1.reset(); //Пам'ять буде видалена.

std::shared_ptr<int> p3 = wp1.lock(); //Об'єкт не існує, тому ми отримаємо порожній shared_ptr.
if(p3)
{
  //Не виконає цього коду.
}

Екземпляр shared_ptr містить контрольний блок, в якому зберігається лічильник посилань інших shared_ptr та лічильник посилань weak_ptr на цей спільний ресурс. Окрім того, контрольний блок може містити інформацію про функцію видалення спільного ресурсу. Якщо ресурс та shared_ptr створені окремо, то пам'ять буде виділено двічі: спочатку власне для самого ресурсу, а потім для контрольного блоку розумного вказівника. Тому зазвичай радять використовувати шаблонну функцію std::make_shared для створення таких розумних вказівників. Ця функція звертається до виділення пам'яті лише один раз, як наслідок і ресурс, і контрольний знаходитимусь в пам'яті поруч[6].

Стандартом C++17 була додана можливість роботи shared_ptr з масивами, а C++20 додав спеціалізацію make_shared для масивів, наприклад: make_shared<T[]>(N)[7].

Від початку shared_ptr надавав гарантію того, що операції з лічильниками атомарні і тому ресурс буде видалено тільки один раз. Жодних гарантій стосовно операцій з вказівником не було[8].

Стандартом C++20 також були додані часткові спеціалізації шаблонів atomic<shared_ptr<T>> та atomic<weak_ptr<T>> які гарантують атомарний доступ до спільного або слабкого вказівника. Раніше такі операції було можливо виконувати із використанням шаблонних функцій подібних до shared_ptr<T> atomic_load(const shared_ptr<T>*), які мали низку вад[7].

Примітки

  1. R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization). C++ Core Guidelines. August 3, 2020.
  2. Marc Gregoire (2021). Smart Pointers. Professional C++ (вид. 5-те). ISBN 978-1-119-69540-0.
  3. Marc Gregoire (2021). The Old and Removed auto_ptr. Professional C++ (вид. 5-те). ISBN 978-1-119-69540-0.
  4. CERT C++ Secure Coding Standard Архівовано 30 березня 2014 у Wayback Machine. 08. Memory Management (MEM)
  5. ISO 14882:2011 20.7.1
  6. Reiner Grimm (11 грудня 2016). std::shared_ptr. Modernes CPP.
  7. Thomas Köppe (2 березня 2020). Changes between C++17 and C++20 DIS.
  8. Reiner Grimm (27 лютого 2017). Atomic Smart Pointers. Modernes CPP.

Див. також

Література

This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.