raw, weak_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-х указателей это нормально, чтобы оставить их «сырые» или я должен использовать что-то более подходящее?

И во второй раз, если работодатель будет читать код, будет ли он строг в отношении того, какие указатели я использую или нет?

CheshireChild
источник
Эта тема кажется такой подходящей для SO. это было в 2008 году . А вот какой тип указателя мне использовать, когда? , Я уверен, что вы можете найти еще лучшие совпадения. Это было только первое, что я увидел
сэх
На мой взгляд, это граница, поскольку речь идет не только о концептуальном значении / намерении этих классов, но и о технических деталях их поведения и реализации. Поскольку принятый ответ склоняется к первому, я рад, что это будет «версия PSE» этого вопроса SO.
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;
};

Теперь, если мы удалим a 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, который должен поддерживать компиляторы до C ++ 11, имеет свою собственную модель сборки мусора: у многих QObjectесть механизм, где они будут уничтожены должным образом без необходимости пользователя для deleteних.

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

greyfade
источник
1
@Zilators: Обратите внимание на мой добавленный комментарий о Qt. Ответ на ваш вопрос о том, следует ли управлять всеми тремя указателями, зависит от того, будут ли объекты Qt вести себя хорошо.
Greyfade
2
"оба делают отдельное выделение для хранения указателя"? Нет, unique_ptr никогда не выделяет ничего лишнего, только shared_ptr должен выделять объект reference-count + allocator-object. "обе строки будут пропускать память"? нет, только может, даже не гарантия плохого поведения.
дедупликатор
1
@Deduplicator: Моя формулировка должна быть неясной: shared_ptrэто отдельный объект - отдельное выделение - от newобъекта ed. Они существуют в разных местах. make_sharedимеет возможность объединить их в одном месте, что, помимо прочего, улучшает локальность кэша.
Greyfade
2
@greyfade: Нонононо. shared_ptrэто объект. И чтобы управлять объектом, он должен выделить объект (счетчик ссылок (слабый + сильный) + разрушитель) -объект. make_sharedпозволяет выделить этот объект и управляемый объект как единое целое. unique_ptrне использует их, таким образом, нет никакого соответствующего преимущества, кроме того, чтобы удостовериться, что объект всегда принадлежит смарт-указателю. Кроме того, можно иметь a, shared_ptrкоторый владеет базовым объектом и представляет a nullptr, или который не является владельцем и представляет ненулевой указатель.
дедупликатор
1
Я посмотрел на него, и, похоже, возникла общая путаница в отношении того, что shared_ptrделает: 1. Он разделяет владение каким-либо объектом (представлен внутренним динамически размещаемым объектом, имеющим слабый и сильный счетчик ссылок, а также средство удаления). , 2. Содержит указатель. Эти две части независимы. make_uniqueи make_sharedоба убедитесь, что выделенный объект безопасно помещен в смарт-указатель. Кроме того, make_sharedпозволяет выделить объект владения и управляемый указатель вместе.
дедупликатор