Когда полезен std::weak_ptr?
Я начал изучать умные указатели C++11, и я не вижу никакого полезного использования std::weak_ptr
, Может кто-нибудь сказать мне, когда std::weak_ptr
полезно / необходимо?
15 ответов
Хорошим примером будет кеш.
Для объектов, к которым недавно обращались, вы хотите сохранить их в памяти, поэтому держите на них сильный указатель. Периодически вы сканируете кеш и решаете, к каким объектам не обращались в последнее время. Вам не нужно хранить их в памяти, поэтому вы избавляетесь от сильного указателя.
Но что, если этот объект используется и какой-то другой код содержит сильный указатель на него? Если кеш избавляется от своего единственного указателя на объект, он никогда не сможет найти его снова. Таким образом, кэш хранит слабый указатель на объекты, которые он должен найти, если они останутся в памяти.
Это именно то, что делает слабый указатель - он позволяет вам находить объект, если он все еще рядом, но не сохраняет его, если он больше не нужен.
std::weak_ptr
это очень хороший способ решить проблему с висящими указателями. Просто используя необработанные указатели, невозможно узнать, были ли ссылочные данные освобождены или нет. Вместо этого, позволяя std::shared_ptr
управлять данными и поставлять std::weak_ptr
для пользователей данных, пользователи могут проверить достоверность данных, позвонив expired()
или же lock()
,
Вы не могли бы сделать это с std::shared_ptr
один, потому что все std::shared_ptr
экземпляры разделяют право собственности на данные, которые не удаляются до всех экземпляров std::shared_ptr
удалены. Вот пример того, как проверить наличие висящего указателя, используя lock()
:
#include <iostream>
#include <memory>
int main()
{
// OLD, problem with dangling pointer
// PROBLEM: ref will point to undefined data!
int* ptr = new int(10);
int* ref = ptr;
delete ptr;
// NEW
// SOLUTION: check expired() or lock() to determine if pointer is valid
// empty definition
std::shared_ptr<int> sptr;
// takes ownership of pointer
sptr.reset(new int);
*sptr = 10;
// get pointer to data without taking ownership
std::weak_ptr<int> weak1 = sptr;
// deletes managed object, acquires new pointer
sptr.reset(new int);
*sptr = 5;
// get pointer to new data without taking ownership
std::weak_ptr<int> weak2 = sptr;
// weak1 is expired!
if(auto tmp = weak1.lock())
std::cout << *tmp << '\n';
else
std::cout << "weak1 is expired\n";
// weak2 points to new data (5)
if(auto tmp = weak2.lock())
std::cout << *tmp << '\n';
else
std::cout << "weak2 is expired\n";
}
Еще один ответ, надеюсь, проще. (для коллег по Google)
Предположим, у вас есть Team
а также Member
объекты.
Очевидно, это отношения: Team
объект будет иметь указатели на его Members
, И вполне вероятно, что члены также будут иметь обратный указатель на их Team
объект.
Тогда у вас есть цикл зависимости. Если вы используете shared_ptr
объекты больше не будут автоматически освобождаться, когда вы откажетесь от ссылки на них, потому что они ссылаются друг на друга циклически. Это утечка памяти.
Вы ломаете это, используя weak_ptr
, "Владелец" обычно использует shared_ptr
и "принадлежит" использовать weak_ptr
его родителю, и преобразовать его временно в shared_ptr
когда ему нужен доступ к его родителю.
Хранить слабый ptr:
weak_ptr<Parent> parentWeakPtr_ = parentSharedPtr; // automatic conversion to weak from shared
затем используйте его при необходимости
shared_ptr<Parent> tempParentSharedPtr = parentWeakPtr_.lock(); // on the stack, from the weak ptr
if( not tempParentSharedPtr ) {
// yes it may failed if parent was freed since we stored weak_ptr
} else {
// do stuff
}
// tempParentSharedPtr is released when it goes out of scope
Вот один пример, данный мне @jleahy: Предположим, у вас есть набор задач, выполняемых асинхронно и управляемых std::shared_ptr<Task>
, Вы можете периодически что-то делать с этими задачами, поэтому событие таймера может пройти через std::vector<std::weak_ptr<Task>>
и дать заданиям что-то делать. Однако одновременно задача может одновременно решить, что она больше не нужна, и умереть. Таким образом, таймер может проверить, является ли задача еще живой, создав общий указатель из слабого указателя и используя этот общий указатель, при условии, что он не равен нулю.
При использовании указателей важно понимать различные типы доступных указателей и когда имеет смысл использовать каждый из них. Существует четыре типа указателей в двух категориях:
- Сырые указатели:
- Сырой указатель [то есть
SomeClass* ptrToSomeClass = new SomeClass();
]
- Сырой указатель [то есть
- Умные указатели:
- Уникальные указатели [т.е.
std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() );
] - Общие указатели [т.е.
std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() );
] - Слабые указатели [т.е.
std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr );
]
- Уникальные указатели [т.е.
Необработанные указатели (иногда называемые "устаревшими указателями" или "указателями C") обеспечивают поведение указателей "без костей" и являются распространенным источником ошибок и утечек памяти. Необработанные указатели не предоставляют средств для отслеживания владения ресурсом, и разработчики должны вызывать "delete" вручную, чтобы убедиться, что они не создают утечку памяти. Это становится трудным, если ресурс используется совместно, так как бывает сложно узнать, указывают ли какие-либо объекты на ресурс. По этим причинам следует избегать необработанных указателей и использовать их только в критически важных для кода разделах кода с ограниченной областью действия.
Уникальные указатели являются базовым интеллектуальным указателем, который "владеет" базовым необработанным указателем на ресурс и отвечает за вызов удаления и освобождение выделенной памяти, как только объект, которому "принадлежит" уникальный указатель, выходит из области видимости. Название "уникальный" относится к тому факту, что только один объект может "владеть" уникальным указателем в данный момент времени. Владение может быть передано другому объекту с помощью команды перемещения, но уникальный указатель никогда не может быть скопирован или передан. По этим причинам уникальные указатели являются хорошей альтернативой необработанным указателям в том случае, если указатель нужен только одному объекту в определенный момент времени, и это освобождает разработчика от необходимости освобождать память в конце жизненного цикла объекта-владельца.
Совместно используемые указатели - это еще один тип интеллектуальных указателей, которые похожи на уникальные указатели, но позволяют многим объектам владеть общим указателем. Как и уникальный указатель, совместно используемые указатели отвечают за освобождение выделенной памяти, как только все объекты завершены, указывая на ресурс. Это достигается с помощью метода, называемого подсчетом ссылок. Каждый раз, когда новый объект становится владельцем общего указателя, счетчик ссылок увеличивается на единицу. Точно так же, когда объект выходит из области действия или перестает указывать на ресурс, счетчик ссылок уменьшается на единицу. Когда счетчик ссылок достигает нуля, выделенная память освобождается. По этим причинам общие указатели являются очень мощным типом интеллектуальных указателей, которые следует использовать всякий раз, когда несколько объектов должны указывать на один и тот же ресурс.
Наконец, слабые указатели - это еще один тип интеллектуальных указателей, которые вместо прямого указания на ресурс указывают на другой указатель (слабый или общий). Слабые указатели не могут получить доступ к объекту напрямую, но они могут определить, существует ли объект до сих пор или срок его действия истек. Слабый указатель может быть временно преобразован в интеллектуальный указатель для доступа к указанному объекту (при условии, что он все еще существует). Чтобы проиллюстрировать это, рассмотрим следующий пример:
- Вы заняты и у вас совпадающие собрания: Встреча А и Встреча Б
- Вы решили пойти на Собрание А, а ваш коллега - на Собрание Б
- Вы сообщаете своему коллеге, что если собрание B все еще продолжается после окончания собрания A, вы присоединитесь
- Могут разыграться следующие два сценария:
- Совещание A заканчивается, а собрание B все еще продолжается, поэтому вы присоединяетесь
- Совещание A заканчивается, и собрание B также закончилось, поэтому вы не присоединяетесь
В этом примере у вас слабый указатель на собрание B. Вы не являетесь "владельцем" собрания B, поэтому оно может закончиться без вас, и вы не будете знать, закончилось ли оно или нет, пока вы не проверите. Если это не закончилось, вы можете присоединиться и участвовать, в противном случае вы не можете. Это отличается от наличия общего указателя на собрание B, поскольку в этом случае вы будете "владельцем" как на собрании A, так и на собрании B (участвуя в обоих одновременно).
В этом примере показано, как работает слабый указатель, и это полезно, когда объект должен быть сторонним наблюдателем, но не хочет ответственности за владение. Это особенно полезно в сценарии, когда два объекта должны указывать друг на друга (круговая ссылка). При использовании общих указателей ни один объект не может быть освобожден, поскольку они все еще "сильно" указаны другим объектом. С помощью слабых указателей к объектам можно обращаться при необходимости и освобождать, когда они больше не нужны.
Они полезны с Boost.Asio, когда вам не гарантируется, что целевой объект все еще существует, когда вызывается асинхронный обработчик. Хитрость заключается в том, чтобы связать weak_ptr
в асинхронный объект-обработчик, используя std::bind
или лямбда захватывает.
void MyClass::startTimer()
{
std::weak_ptr<MyClass> weak = shared_from_this();
timer_.async_wait( [weak](const boost::system::error_code& ec)
{
auto self = weak.lock();
if (self)
{
self->handleTimeout();
}
else
{
std::cout << "Target object no longer exists!\n";
}
} );
}
Это вариант self = shared_from_this()
идиома, часто встречающаяся в примерах Boost.Asio, где ожидающий асинхронный обработчик не продлит срок жизни целевого объекта, но все еще безопасен, если целевой объект удален.
shared_ptr: содержит реальный объект.
слабый_птр: использует lock
для подключения к реальному владельцу или возвращает NULL в противном случае.
Грубо говоря, weak_ptr
Роль похожа на роль агентства недвижимости. Без агентов, чтобы получить дом в аренду, нам, возможно, придется проверять случайные дома в городе. Агенты следят за тем, чтобы мы посещали только те дома, которые еще доступны и доступны для аренды.
weak_ptr
Также хорошо проверить правильность удаления объекта, особенно в модульных тестах. Типичный вариант использования может выглядеть так:
std::weak_ptr<X> weak_x{ shared_x };
shared_x.reset();
BOOST_CHECK(weak_x.lock());
... //do something that should remove all other copies of shared_x and hence destroy x
BOOST_CHECK(!weak_x.lock());
Помимо других уже упомянутых действительных вариантов использования std::weak_ptr
это удивительный инструмент в многопоточной среде, потому что
- Он не владеет объектом и поэтому не может помешать удалению в другом потоке.
std::shared_ptr
в сочетании сstd::weak_ptr
безопасен от висящих указателей - в противоположностьstd::unique_ptr
в сочетании с необработанными указателямиstd::weak_ptr::lock()
является атомарной операцией (см. также О поточно-ориентированной безопасности weak_ptr)
Рассмотрим задачу по одновременной загрузке всех изображений каталога (~10.000) в память (например, в виде кэша миниатюр). Очевидно, что лучший способ сделать это - поток управления, который обрабатывает и управляет изображениями, и несколько рабочих потоков, которые загружают изображения. Теперь это простая задача. Вот очень упрощенная реализация (join()
etc опущен, потоки должны быть обработаны по-другому в реальной реализации и т.д.)
// a simplified class to hold the thumbnail and data
struct ImageData {
std::string path;
std::unique_ptr<YourFavoriteImageLibData> image;
};
// a simplified reader fn
void read( std::vector<std::shared_ptr<ImageData>> imagesToLoad ) {
for( auto& imageData : imagesToLoad )
imageData->image = YourFavoriteImageLib::load( imageData->path );
}
// a simplified manager
class Manager {
std::vector<std::shared_ptr<ImageData>> m_imageDatas;
std::vector<std::unique_ptr<std::thread>> m_threads;
public:
void load( const std::string& folderPath ) {
std::vector<std::string> imagePaths = readFolder( folderPath );
m_imageDatas = createImageDatas( imagePaths );
const unsigned numThreads = std::thread::hardware_concurrency();
std::vector<std::vector<std::shared_ptr<ImageData>>> splitDatas =
splitImageDatas( m_imageDatas, numThreads );
for( auto& dataRangeToLoad : splitDatas )
m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
}
};
Но это становится намного сложнее, если вы хотите прервать загрузку изображений, например, потому что пользователь выбрал другой каталог. Или даже если вы хотите уничтожить менеджера.
Вам потребуется связь между потоками и остановка всех потоков загрузчика, прежде чем вы сможете изменить m_imageDatas
поле. В противном случае загрузчики будут продолжать загрузку до тех пор, пока не будут выполнены все изображения, даже если они уже устарели. В упрощенном примере это не будет слишком сложно, но в реальной среде все может быть гораздо сложнее.
Потоки, вероятно, будут частью пула потоков, используемого несколькими менеджерами, некоторые из которых останавливаются, а некоторые нет и т. Д. Простой параметр imagesToLoad
будет заблокированной очередью, в которую эти менеджеры отправляют свои запросы на изображения из разных потоков управления, а читатели выкладывают запросы - в произвольном порядке - на другом конце. И поэтому общение становится сложным, медленным и подверженным ошибкам. Очень элегантный способ избежать какого-либо дополнительного общения в таких случаях - использовать std::shared_ptr
в сочетании с std::weak_ptr
,
// a simplified reader fn
void read( std::vector<std::weak_ptr<ImageData>> imagesToLoad ) {
for( auto& imageDataWeak : imagesToLoad ) {
std::shared_ptr<ImageData> imageData = imageDataWeak.lock();
if( !imageData )
continue;
imageData->image = YourFavoriteImageLib::load( imageData->path );
}
}
// a simplified manager
class Manager {
std::vector<std::shared_ptr<ImageData>> m_imageDatas;
std::vector<std::unique_ptr<std::thread>> m_threads;
public:
void load( const std::string& folderPath ) {
std::vector<std::string> imagePaths = readFolder( folderPath );
m_imageDatas = createImageDatas( imagePaths );
const unsigned numThreads = std::thread::hardware_concurrency();
std::vector<std::vector<std::weak_ptr<ImageData>>> splitDatas =
splitImageDatasToWeak( m_imageDatas, numThreads );
for( auto& dataRangeToLoad : splitDatas )
m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
}
};
Эта реализация почти так же проста, как и первая, не требует дополнительного взаимодействия потоков и может быть частью пула / очереди потоков в реальной реализации. Поскольку изображения с истекшим сроком пропускаются, а изображения с истекшим сроком действия обрабатываются, потоки никогда не должны были бы останавливаться во время нормальной работы. Вы всегда можете смело менять путь или уничтожать своих менеджеров, так как читатель fn проверяет, не истек ли срок действия указателя-владельца.
Я вижу много интересных ответов, объясняющих подсчет ссылок и т. Д., Но мне не хватает простого примера, демонстрирующего, как предотвратить утечку памяти с помощью
weak_ptr
. В первом примере я использую
shared_ptr
в классах с циклическими ссылками. Когда классы выходят за рамки, они НЕ уничтожаются.
#include<iostream>
#include<memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B>bptr;
A() {
cout << "A created" << endl;
}
~A() {
cout << "A destroyed" << endl;
}
};
class B
{
public:
shared_ptr<A>aptr;
B() {
cout << "B created" << endl;
}
~B() {
cout << "B destroyed" << endl;
}
};
int main()
{
{
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->bptr = b;
b->aptr = a;
}
// put breakpoint here
}
Если вы запустите фрагмент кода, вы увидите, как классы создаются, но не уничтожаются:
A created
B created
Теперь мы меняем
shared_ptr's
к
weak_ptr
:
class B;
class A
{
public:
weak_ptr<B>bptr;
A() {
cout << "A created" << endl;
}
~A() {
cout << "A destroyed" << endl;
}
};
class B
{
public:
weak_ptr<A>aptr;
B() {
cout << "B created" << endl;
}
~B() {
cout << "B destroyed" << endl;
}
};
int main()
{
{
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->bptr = b;
b->aptr = a;
}
// put breakpoint here
}
На этот раз при использовании
weak_ptr
мы видим правильное разрушение класса:
A created
B created
B destroyed
A destroyed
Я вижу std::weak_ptr<T>
в качестве ручки для std::shared_ptr<T>
: Это позволяет мне получить std::shared_ptr<T>
если он все еще существует, но он не продлит срок его службы. Есть несколько сценариев, когда такая точка зрения полезна:
// Some sort of image; very expensive to create.
std::shared_ptr< Texture > texture;
// A Widget should be able to quickly get a handle to a Texture. On the
// other hand, I don't want to keep Textures around just because a widget
// may need it.
struct Widget {
std::weak_ptr< Texture > texture_handle;
void render() {
if (auto texture = texture_handle.get(); texture) {
// do stuff with texture. Warning: `texture`
// is now extending the lifetime because it
// is a std::shared_ptr< Texture >.
} else {
// gracefully degrade; there's no texture.
}
}
};
Другим важным сценарием является разрыв циклов в структурах данных.
// Asking for trouble because a node owns the next node, and the next node owns
// the previous node: memory leak; no destructors automatically called.
struct Node {
std::shared_ptr< Node > next;
std::shared_ptr< Node > prev;
};
// Asking for trouble because a parent owns its children and children own their
// parents: memory leak; no destructors automatically called.
struct Node {
std::shared_ptr< Node > parent;
std::shared_ptr< Node > left_child;
std::shared_ptr< Node > right_child;
};
// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
std::shared_ptr< Node > next;
std::weak_ptr< Node > prev;
};
// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
std::weak_ptr< Node > parent;
std::shared_ptr< Node > left_child;
std::shared_ptr< Node > right_child;
};
Херб Саттер отлично говорит о том, как лучше всего использовать языковые функции (в данном случае умные указатели), чтобы обеспечить утечку свободы по умолчанию(то есть: все защелкивается на месте по конструкции; вряд ли это можно испортить). Это нужно смотреть.
http://en.cppreference.com/w/cpp/memory/weak_ptr std:: weak_ptr - это интеллектуальный указатель, содержащий несобственную ("слабую") ссылку на объект, управляемый std:: shared_ptr. Он должен быть преобразован в std:: shared_ptr для доступа к ссылочному объекту.
std:: weak_ptr моделирует временное владение: когда доступ к объекту требуется только в том случае, если он существует, и он может быть в любой момент удален кем-то другим, std:: weak_ptr используется для отслеживания объекта и преобразуется в std:: shared_ptr для временного владения. Если исходный std:: shared_ptr уничтожается в это время, время жизни объекта увеличивается до тех пор, пока не будет уничтожен временный std:: shared_ptr.
Кроме того, std:: weak_ptr используется для разрыва циклических ссылок на std:: shared_ptr.
Недостатком разделяемого указателя является то, что shared_pointer не может обработать зависимость родительско-дочернего цикла. Означает, если родительский класс использует объект дочернего класса, используя общий указатель, в том же файле, если дочерний класс использует объект родительского класса. Общий указатель не сможет уничтожить все объекты, даже общий указатель вообще не вызывает деструктор в сценарии зависимости цикла. в основном разделяемый указатель не поддерживает механизм подсчета ссылок.
Этот недостаток мы можем преодолеть с помощью weak_pointer.
Когда мы не хотим владеть объектом:
Пример:
class A
{
shared_ptr<int> sPtr1;
weak_ptr<int> wPtr1;
}
В приведенном выше классе wPtr1 не является владельцем ресурса, на который указывает wPtr1. Если ресурс удален, то срок действия wPtr1 истекает.
Чтобы избежать круговой зависимости:
shard_ptr<A> <----| shared_ptr<B> <------
^ | ^ |
| | | |
| | | |
| | | |
| | | |
class A | class B |
| | | |
| ------------ |
| |
-------------------------------------
Теперь, если мы сделаем shared_ptr класса B и A, use_count обоих указателей будет равен двум.
Когда shared_ptr выходит за пределы области действия, счетчик остается равным 1, и, следовательно, объекты A и B не удаляются.
class B;
class A
{
shared_ptr<B> sP1; // use weak_ptr instead to avoid CD
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void setShared(shared_ptr<B>& p)
{
sP1 = p;
}
};
class B
{
shared_ptr<A> sP1;
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
void setShared(shared_ptr<A>& p)
{
sP1 = p;
}
};
int main()
{
shared_ptr<A> aPtr(new A);
shared_ptr<B> bPtr(new B);
aPtr->setShared(bPtr);
bPtr->setShared(aPtr);
return 0;
}
выход:
A()
B()
Как видно из вывода, указатели A и B никогда не удаляются и, следовательно, происходит утечка памяти.
Чтобы избежать такой проблемы, просто используйте weak_ptr в классе A вместо shared_ptr, что имеет больше смысла.
Вдохновленный ответом @offirmo, я написал этот код, а затем запустил инструмент диагностики Visual Studio:
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
struct Member;
struct Team;
struct Member {
int x = 0;
Member(int xArg) {
x = xArg;
}
shared_ptr<Team> teamPointer;
};
struct Team {
vector<shared_ptr<Member>> members;
};
void foo() {
auto t1 = make_shared<Team>();
for (int i = 0; i < 1000000; i++) {
t1->members.push_back(make_shared<Member>(i));
t1->members.back()->teamPointer = t1;
}
}
int main() {
foo();
while (1);
return 0;
}
Когда указатель члена команды равен shared_ptr teamPointer, память не освобождается после выполнения foo(), т.е. она остается на уровне около 150 МБ.
Но если в диагностическом инструменте он изменится на weak_ptr teamPointer, вы увидите пик, а затем использование памяти вернется к примерно 2 МБ.