Чи хороша практика завжди використовувати розумні вказівники?


80

Я вважаю, що розумні вказівники набагато зручніші, ніж необроблені. То чи це гарна ідея завжди використовувати розумні вказівники? (Будь ласка, зверніть увагу, що я родом з Java, і тому мені не дуже подобається ідея явного управління пам’яттю. Отже, якщо з розумними вказівниками не виникають серйозні проблеми з продуктивністю, я хотів би дотримуватися їх)

Примітка: Хоча я походжу з мови Java, я досить добре розумію реалізацію розумних покажчиків та концепцій RAII. Тож ви можете сприймати ці знання як належне з мого боку, публікуючи відповідь. Я використовую статичне розподіл майже скрізь і використовую покажчики лише за необхідності. Моє запитання просто: чи можу я завжди використовувати розумні вказівники замість необроблених покажчиків ???


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

@Neil Так, я мав на увазі лише це.
Доні Борріс,

Я маю на увазі це без образи, але зрозуміло, що потрібно взяти гарну книгу і починати спочатку. Ваша термінологія неправильна, і я боюся, що ваш код дуже "не C ++".
GManNickG,

2
блін, тут немає нічого, крім солоного програміста на C ++
Бруно

Відповіді:


79

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

1. Коли не потрібно

Є дві ситуації, коли не слід використовувати розумні вказівники.

Перший - це абсолютно та сама ситуація, коли C++насправді не слід використовувати клас. IE: Межа DLL, якщо ви не пропонуєте вихідний код клієнту. Скажімо анекдотично.

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

void notowner(const std::string& name)
{
  Class* pointer(0);
  if (name == "cat")
    pointer = getCat();
  else if (name == "dog")
    pointer = getDog();

  if (pointer) doSomething(*pointer);
}

Цей приклад обмежений. Але покажчик семантично відрізняється від посилання тим, що може вказувати на недійсне розташування (нульовий покажчик). У цьому випадку цілком нормально не використовувати замість цього розумний вказівник, оскільки ви не хочете керувати життям об’єкта.

2. Розумні менеджери

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

Це суперечлива точка зору, але, переглянувши стільки прикладів недосконалого коду, я більше не ризикую. Отже, якщо ви пишете, newвам потрібен розумний менеджер для нещодавно виділеної пам’яті. І це вам потрібно прямо зараз.

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

Тепер справжня складність починається: який розумний менеджер?

3. Розумні покажчики

Звідти є різні розумні вказівники з різними характеристиками.

Пропуску, std::auto_ptrякого зазвичай слід уникати (його семантична копія вкручена).

  • scoped_ptr: немає накладних витрат, не можна скопіювати або перемістити.
  • unique_ptr: немає накладних витрат, не можна скопіювати, можна перемістити.
  • shared_ptr/ weak_ptr: деякі накладні витрати (підрахунок посилань), можна скопіювати.

Зазвичай намагаються використовувати або scoped_ptrабо unique_ptr. Якщо вам потрібно кілька власників, спробуйте змінити дизайн. Якщо ви не можете змінити дизайн і вам дійсно потрібні кілька власників, використовуйте a shared_ptr, але остерігайтеся циклів посилань, які слід порушити, використовуючи weak_ptrдесь посередині.

4. Розумні контейнери

Багато розумні вказівники не призначені для копіювання, тому їх використання з контейнерами STL дещо скомпрометовано.

Замість того, щоб вдаватися до shared_ptrта його накладних витрат, використовуйте розумні контейнери з контейнера Boost Pointer . Вони імітують інтерфейс класичних контейнерів STL, але зберігають власні вказівники.

5. Прокат свого

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

Написати розумного менеджера за наявності винятків досить складно. Зазвичай ви не можете припустити, що пам’ять доступна ( newможе вийти з ладу) або що вона Copy Constructorмає no throwгарантію.

Може бути прийнятним, дещо, ігнорувати std::bad_allocвиняток і нав'язувати, що Copy Constructors ряду помічників не виходять з ладу ... зрештою, це те, що boost::shared_ptrробить Dпараметр шаблону видалення .

Але я б не рекомендував це, особливо для початківців. Це складне питання, і ви навряд чи зараз помітите помилки.

6. Приклади

// For the sake of short code, avoid in real code ;)
using namespace boost;

// Example classes
//   Yes, clone returns a raw pointer...
// it puts the burden on the caller as for how to wrap it
//   It is to obey the `Cloneable` concept as described in 
// the Boost Pointer Container library linked above
struct Cloneable
{
  virtual ~Cloneable() {}
  virtual Cloneable* clone() const = 0;
};

struct Derived: Cloneable
{
  virtual Derived* clone() const { new Derived(*this); }
};

void scoped()
{
  scoped_ptr<Cloneable> c(new Derived);
} // memory freed here

// illustration of the moved semantics
unique_ptr<Cloneable> unique()
{
  return unique_ptr<Cloneable>(new Derived);
}

void shared()
{
  shared_ptr<Cloneable> n1(new Derived);
  weak_ptr<Cloneable> w = n1;

  {
    shared_ptr<Cloneable> n2 = n1;          // copy

    n1.reset();

    assert(n1.get() == 0);
    assert(n2.get() != 0);
    assert(!w.expired() && w.get() != 0);
  } // n2 goes out of scope, the memory is released

  assert(w.expired()); // no object any longer
}

void container()
{
  ptr_vector<Cloneable> vec;
  vec.push_back(new Derived);
  vec.push_back(new Derived);

  vec.push_back(
    vec.front().clone()         // Interesting semantic, it is dereferenced!
  );
} // when vec goes out of scope, it clears up everything ;)

4
Чудова відповідь! :) Я думаю, містер Баттерворт міг би чомусь навчитися з цього.
Доні Борріс

2
Мені особисто подобаються відповіді Ніла (загалом, і ця зокрема), я просто думав, що предмет вимагає більш глибоких пояснень, враховуючи, наскільки складним є управління пам'яттю і наскільки "відносно" нові бібліотеки (я думаю тут Pointer Container , від 2007 р.).
Matthieu M.

Що ви маєте на увазі під "розумними менеджерами"? Цей розділ відповіді для мене не має сенсу. Крім того, семантика std::auto_ptrвідмінна від того, що очікує більшість людей, але вона має сенс і призводить до того, щоб уникнути проблем із дизайном у коді, кажучи, що "взагалі уникати цього" не має сенсу.
Frunsi,

1
@frunsi: якщо думка "загалом уникати цього" є дурницею, тобі потрібно швидко потрапити до комітету стандарту ISO і пояснити їм, чому. Вони збираються зробити жахливу помилку, припинивши роботу auto_ptrв C ++ 0x, рекомендуючи використовувати unique_ptrзамість ;-). Чесно кажучи, unique_ptrпокладається на заміну конструкції переїзду / переуступки auto_ptr, якщо ви хочете отримати сувору передачу права власності на C ++ 03, у вас немає великого вибору.
Стів Джессоп,

@ Стів: Добре! Схоже, unique_ptr - це шлях, коли доступна семантика переміщення. До цього часу auto_ptr все ще корисний, але його буде видалено через 100 років або близько того;) Якщо ми всі почнемо писати код C ++ 0x наступного року (я сподіваюся на це), нам слід уникати auto_ptr зараз, але я підозрюю, так .. :) Жартую, ти маєш рацію, цього зараз слід уникати.
Frunsi,

18

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

Але для відповіді на ваше запитання вам слід віддати перевагу розумним покажчикам як першому наближенню до рішення, але, можливо, будьте готові відмовитись від них, коли це необхідно. Ніколи не слід використовувати покажчики (або будь-які) або динамічне розподіл, коли цього можна уникнути. Наприклад:

string * s1 = new string( "foo" );      // bad
string s2( "bar" );    // good

Редагувати: Щоб відповісти на ваше додаткове запитання "Чи можу я завжди використовувати розумні вказівники замість необроблених вказівників ??? Тоді ні, ви не можете. Якщо (наприклад) вам потрібно застосувати власну версію оператора new, вам доведеться змусити його повернути покажчик, а не розумний вказівник.


10
Зовсім безрезультатна відповідь. Я би хотів, щоб у мене було достатньо представників, щоб підтримати це.
Доні Борріс,

8
@Dony Якість відповіді часто відображає якість запитання.

12
@Dony Я, як правило, прихиляюсь до неправильних, а не просто до безпомилкових відповідей. Зрештою, може бути важко точно знати, чому допитуваному потрібно навчитися, щоб стати просвітленим.
Філіп Поттер,

4
На жаль, відповіді Ніла часто надходять зі стороною поблажливості. Йому слід перестати відповідати на питання, які його розчаровують, оскільки світ не такий розумний або досвідчений, як він.

3
@Neil: у своїх коментарях ви взяли кілька ударів на рівні компетенції ОП. Не зрозумійте мене неправильно. Я все за це. Думаю, той, хто не читав мовний стандартний фронтальний і принаймні принаймні 5 разів, потрапляє у "світ проблем, коли програмує на C ++".

13

Зазвичай ви не повинні використовувати покажчики (розумні чи інші), якщо вони вам не потрібні. Краще робіть локальні змінні, члени класу, векторні елементи та подібні елементи звичайними об'єктами, а не вказівниками на об'єкти. (Оскільки ви походите з Java, у вас, мабуть, спокуса розподілити все за допомогоюnew , що не рекомендується.)

Такий підхід (" RAII ") позбавляє вас більшої частини часу турбуватися про вказівники.

Коли вам доводиться використовувати покажчики, це залежить від ситуації та того, чому саме вам потрібні вказівники, але, як правило, можна використовувати розумні вказівники. Можливо, не завжди (жирним шрифтом) є найкращим варіантом, але це залежить від конкретної ситуації.


4
Отже, які це ситуації, від яких "це залежить"? Чому це ніхто не розповідає?
П Швед

1
Я можу подумати про кілька ситуацій: вимоги до продуктивності можуть зробити спільні вказівники непридатними (тоді boost::scoped_ptrце все ще можна було б використовувати, але, можливо, ви тоді не хочете брати залежність від підсилення?) - або вам потрібно взаємодіяти з API C , у цьому випадку сирі вказівники є більш послідовними. Якщо вам потрібно виконати ітерацію масиву, ваші ітератори, ймовірно, також будуть необробленими вказівниками.
jalf

1
Якщо ви створюєте об'єкт, який може пережити екземпляр його створення, ви, очевидно, не можете просто вбудувати його в інший об'єкт.
Бен Войгт,

1
@Ben Voigt: у загальному випадку ваш приклад не дійсний. Ви можете повертати auto_ptr, unique_ptrабо shared_ptrтак само , як ви б передати сирої покажчик з вашої сфери передачі права власності. scoped_ptrє єдиним розумним покажчиком набору, який не зможе передати / поділити право власності
Девід Родрігес - dribeas

1
@Ben: спільні покажчики boost мають механізм вирішення цього, див. Це запитання, яке я задав: stackoverflow.com/questions/1403465/… . В основному, у вас може бути спільний вказівник, який містить посилання (в тому числі підраховуючий сенс) на об'єкт, але при посиланні повертає інший вказівник (у цьому випадку це член об'єкта, на який посилається).
Еван Теран,

9

Час добре НЕ щоб використовувати розумні вказівники, знаходиться на межі інтерфейсу DLL. Ви не знаєте, чи будуть інші виконувані файли створені з тим самим компілятором / бібліотеками. Правила викликів DLL у вашій системі не визначають, як виглядають стандартні класи або класи TR1, включаючи розумні вказівники.

Якщо ви хочете представити право власності на пуантета у виконуваному файлі чи бібліотеці, то розумні вказівники в середньому найкращий спосіб це зробити. Тож добре хотіти завжди використовувати їх, а не сирі. Чи можна насправді завжди ними користуватися - це вже інша справа.

Для конкретного прикладу, коли цього не робити, - припустимо, ви пишете представлення загального графа, з вершинами, представленими об'єктами, і ребрами, представленими покажчиками між об'єктами. Звичайні розумні вказівники вам не допоможуть: графіки можуть бути циклічними, і жоден конкретний вузол не може нести відповідальність за управління пам’яттю інших вузлів, тому загальних та слабких вказівників недостатньо. Ви можете, наприклад, помістити все у вектор і використовувати індекси замість покажчиків, або помістити все в деке і використовувати необроблені покажчики. Ви можете використовувати, shared_ptrякщо хочете, але це не додасть нічого, крім накладних витрат. Або ви можете шукати GC для розмітки.

Більш маргінальний випадок: я волію бачити, як функції беруть параметр за покажчиком або посиланням, і обіцяю не зберігати вказівник чи посилання на нього , а не беруть a shared_ptrі залишають вас гадати, чи можуть вони зберегти посилання після повернення, можливо, якщо ви модифікуєте референс і коли-небудь знову щось зламаєте і т. д. Не зберігання посилань - це те, що часто не документується явно, це просто само собою зрозуміло. Можливо, не повинно, але це так. Розумні вказівники мають на увазі щось про право власності, а помилково натякають на те, що може заплутати. Тож якщо ваша функція приймає a shared_ptr, обов’язково задокументуйте, чи може вона зберегти посилання чи ні.


6

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


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

Якщо вам потрібно видалити об'єкт, фактичного покарання за продуктивність за деякими розумними вказівниками (зокрема scoped_ptr) не буде
Девід Родрігес - dribeas

@David: Це хороший момент ... хоча я думав (можливо, через незнання), що в цій ситуації все одно буде один додатковий тест. Мені доведеться кинути трохи зібрань і навчитись самому.
Mark Wilkins

2
shared_ptrмають накладні витрати як при розподілі спільно використовуваного інформаційного блоку, так і при проведенні звільнення, базуючись на цій спільній інформації. Але при розумних покажчиках з єдиною власністю деструктору не потрібно виконувати жодного тесту: просто видаліть внутрішній вказівник. Приклади: libstdc ++:, ~auto_ptr() { delete _M_ptr; }boost 1.37: ~scoped_ptr() { checked_delete(ptr); }де checked_deleteперевірка часу компіляції на повноту типу та один виклик delete, який, швидше за все, буде вбудований.
Девід Родрігес - dribeas

@David: Класно - дякую, що поділилися цим. Я чомусь навчився.
Mark Wilkins

4

Загалом, ні, ви не завжди можете використовувати розумні вказівники. Наприклад, коли ви використовуєте інші фреймворки, які не використовують розумний вказівник (наприклад, Qt), вам також доведеться використовувати необроблені вказівники.


2

Якщо ви обробляєте ресурс, ви завжди повинні використовувати техніки RAII, при цьому у випадку з пам'яттю означає використання тієї чи іншої форми розумного вказівника (примітка: розумний, а не shared_ptr , виберіть розумний вказівник, який найбільш підходить для вашого конкретного випадку використання ). Це єдиний спосіб уникнути витоків за наявності винятків.

Все ще існують випадки, коли необхідні необроблені вказівники, коли управління ресурсами не здійснюється через вказівник. Зокрема, це єдиний спосіб отримати посилання, яке можна скинути. Подумайте про те, щоб зберегти посилання на об’єкт, тривалість життя якого не може бути явно оброблена (атрибут члена, об’єкт у стеку). Але це дуже конкретний випадок, який я бачив лише один раз у реальному коді. У більшості випадків використання a shared_ptrє кращим підходом до спільного використання об’єкта.


2

Я розглядаю розумні вказівники: ВЕЛИКИЙ, коли важко зрозуміти, коли може відбутися вивільнення (скажімо, всередині блоку try / catch або всередині функції, яка викликає функцію (або навіть конструктор!), Яка може вивести вас із поточної функції) , або додавання кращого управління пам’яттю до функції, яка повертається скрізь у коді. Або розміщення покажчиків у контейнерах.

Однак розумні покажчики мають вартість, яку ви, можливо, не захочете платити за всю програму. Якщо керування пам'яттю легко зробити вручну ("Хм, я знаю, що коли ця функція закінчується, мені потрібно видалити ці три покажчики, і я знаю, що ця функція працюватиме до кінця"), то навіщо витрачати цикли, які робить комп'ютер це?


3
"Хм, я знаю, що коли ця функція закінчується, мені потрібно видалити ці три покажчики, і я знаю, що ця функція буде виконуватися до кінця" - ось що auto_ptrдля, або scoped_ptr. Вони рідко створюють вимірювані накладні витрати, а тим часом вони полегшують правильне отримання коду. Наприклад, якщо придбання другого з цих трьох покажчиків викликає виняток, ви звільняєте перший? Скільки коду потрібно написати для цього, порівняно з використанням розумних покажчиків? Як часто ви справді купуєте ресурси, які вам потрібно звільнити, але де ваше придбання не може провалитися?
Steve Jessop

Це ще один хороший приклад перебування всередині функції, яка може вивести вас з поточної функції, і хороший момент :)
RyanWilcox,

1

Так, АЛЕ я провів кілька проектів без використання розумного вказівника чи будь-яких покажчиків. Це хороша практика - використовувати контейнери, такі як deque, list, map тощо. Я також використовую посилання, коли це можливо. Замість того, щоб передати вказівник, я передаю посилання або посилання const і майже завжди нелогічно видаляти / звільняти посилання, тому у мене ніколи не виникає проблем (зазвичай я створюю їх у стеці, написавши{ Class class; func(class, ref2, ref3); }


0

Це є. Розумна вказівка ​​є одним із наріжних каменів старої екосистеми какао (дотик). Я вірю, що це продовжує впливати на нове.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.