raw, slab_ptr, unique_ptr, shared_ptr і т. д. ... Як їх правильно вибрати?


33

У C ++ є багато покажчиків, але якщо чесно, через 5 років або близько того в програмуванні на C ++ (конкретно з Qt Framework), я використовую лише старий необроблений покажчик:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Я знаю, що існує багато інших "розумних" покажчиків:

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

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

Наприклад, у мене є такий заголовок класу:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Це, очевидно, не вичерпно, але для кожного з цих 3 покажчиків це нормально, щоб залишити їх "сирими" чи я повинен використовувати щось більш відповідне?

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


Ця тема видається настільки підходящою для ТА. це було у 2008 році . І ось Який тип покажчика я використовую, коли? . Я впевнений, що ви можете знайти ще кращі матчі. Це були тільки перші я побачив
— sehe

imo це є кордоном, оскільки йдеться стільки про концептуальне значення / наміри цих класів, скільки про технічні деталі їх поведінки та реалізації. Оскільки прийнята відповідь схиляється до колишньої, я задоволений тим, що це буде "версія PSE" цього питання "ТАК".
— Ixrec

Відповіді:


70

"Сирий" вказівник не управляється. Тобто наступний рядок:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... буде протікати пам'ять, якщо супровід deleteне буде виконаний у відповідний час.

auto_ptr

З метою мінімізації цих випадків std::auto_ptr<>було введено. Зважаючи на обмеження рівня C ++ до стандарту 2011 року, проте все ще дуже легко auto_ptrпросочити пам'ять. Однак достатньо для обмежених випадків, таких як цей:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Один з його найслабших випадків використання - у контейнерах. Це тому, що якщо auto_ptr<>зроблена копія і стара копія не ретельно скидається, контейнер може видалити покажчик і втратити дані.

unique_ptr

В якості заміни C ++ 11 представив std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Таке unique_ptr<>заповіт буде правильно очищено, навіть якщо воно передане між функціями. Це робиться шляхом семантичного представлення "власності" на вказівник - "власник" очищає його. Це робить його ідеальним для використання в контейнерах:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

На відміну від цього auto_ptr<>, unique_ptr<>тут добре поводиться, і коли vectorрозміри не змінюються, жоден із об’єктів не буде випадково видалений під час vectorкопіювання його зберігання.

shared_ptr і weak_ptr

unique_ptr<>Це корисно, напевно, але є випадки, коли ви хочете, щоб дві частини вашої кодової бази могли посилатися на один і той же об'єкт і копіювати вказівник навколо, при цьому все ще гарантується належне очищення. Наприклад, дерево може виглядати приблизно так, коли використовується std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

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

Це працює, тому що кожен shared_ptr<>тримає на себе не лише вказівник на об’єкт, але і кількість посилань усіх shared_ptr<>об'єктів, які посилаються на один і той же покажчик. Коли буде створено нове, кількість рахунків збільшується. Коли хтось знищений, кількість знижується. Коли кількість досягне нуля, вказівник deleted.

Отже, це вводить проблему: подвійно пов'язані структури закінчуються циркулярними посиланнями. Скажімо, ми хочемо додати parentпокажчик до нашого дерева Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Тепер, якщо ми видалимо а Node, на нього є циклічне посилання. Це ніколи не буде deleted, тому що його посилання ніколи не буде дорівнює нулю.

Щоб вирішити цю проблему, ви використовуєте std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Тепер все буде працювати правильно, і видалення вузла не залишить застряглих посилань на батьківський вузол. Однак це робить ходьбу по дереву дещо складнішою:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

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

make_shared і make_unique

Зараз є деякі незначні проблеми, shared_ptr<>і їх unique_ptr<>слід вирішити. Наступні два рядки мають проблему:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Якщо thrower()викидає виняток, обидва рядки просочать пам'ять. Більше того, shared_ptr<>відлік відліку є далеко від об'єкта, на який він вказує, і це може означати повторне виділення). Це зазвичай не бажано.

C ++ 11 забезпечує, std::make_shared<>()а C ++ 14 забезпечує std::make_unique<>()вирішення цієї проблеми:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Зараз в обох випадках, навіть якщо thrower()викине виняток, не буде витоку пам'яті. Як бонус, make_shared<>()має можливість створити свою кількість відліку в тому ж просторі пам'яті, що і керований об’єкт, який може бути швидшим і може заощадити кілька байт пам'яті, надаючи при цьому гарантію безпеки винятку!

Примітки про Qt

Слід зазначити, однак, що Qt, який повинен підтримувати компілятори pre-C ++ 11, має власну модель збору сміття: у багатьох QObjectє механізм, де вони будуть належним чином знищені, не потребуючи в deleteних користувача.

Я не знаю, як QObjectбуде поводитися s, коли керуються C ++ 11 керованими покажчиками, тому не можу сказати, що shared_ptr<QDialog>це гарна ідея. У мене недостатньо досвіду роботи з Qt, щоб сказати напевно, але я вважаю, що Qt5 було скориговано для цього випадку використання.


1
@ Zilators: Будь ласка, зверніть увагу на мій доданий коментар щодо Qt Відповідь на ваше запитання про те, чи слід керувати всіма трьома вказівниками, залежить від того, чи добре поводяться об'єкти Qt.
— greyfade

2
"обидва роблять окреме виділення для утримання покажчика"? Ні, unique_ptr ніколи не виділяє нічого зайвого, лише shared_ptr повинен виділяти опорний підрахунок + алокатор-об'єкт. "обидва рядки просочать пам'ять"? ні, тільки може, навіть не гарантія поганої поведінки.
— Дедуплікатор

1
@Deduplicator: Моє формулювання повинно бути незрозумілим: shared_ptrце окремий об'єкт - окремий розподіл - від newоб'єкта ed. Вони існують у різних місцях. make_sharedмає можливість розміщувати їх у одному місці, що покращує кеш-пам'ять, серед іншого.
— greyfade

2
@greyfade: Nononono. shared_ptrє об’єктом. А для управління об'єктом він повинен виділити (облік посилань (слабкий + сильний) + руйнівник) -об'єкт. make_sharedдозволяє виділити це та керований об'єкт як один елемент. unique_ptrне використовує ці, тому немає відповідної переваги, окрім того, щоб переконатися, що об'єктом завжди належить смарт-покажчик. В бік може бути те, shared_ptrщо є власником основного об'єкта і являє собою nullptr, або яке не є власником і являє собою ненульовий вказівник.
— Дедуплікатор

1
Я подивився на це, і, здається, виникає загальна плутанина щодо того, що shared_ptrробить: 1. Він поділяє право власності на якийсь об'єкт (представлений внутрішнім динамічно виділеним об'єктом, що має слабкий і сильний відліку, а також делетером) . 2. Він містить вказівник. Ці дві частини незалежні. make_uniqueі make_sharedобидва переконайтеся, що виділений об’єкт надійно розміщений у смарт-покажчику. Крім того, make_sharedдозволяє розподілити об'єкт власності та керований покажчик разом.
— Дедуплікатор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.