Pull to refresh

Comments 26

Информация полезная (чтобы не городить костыли и подпорки) и интересная.
Единственное — у вас опечатка в заголовке.
Я сначала даже подумал, что это действительно какой-то секретный новый share_ptr :)
Спасибо за замечание :) поправлено
И позволю себе процитировать слова Stephan T. Lavavej:
This constructor is so secret, not even STL maintainers know about it...

Этот конструктор настолько секретный, что даже сопровождающие STL не знают о нём...

:)

Спасибо за перевод.
Спасибо за цитату. Не смог удержаться, чтобы не утащить ее в эпиграф :)
Возможно, у меня маловато опыта использования shared_ptr, но мне кажется что пример в разделе «Совместное использование подобъектов» выглядит немного странно. Если в библиотеку нужно передавать поле, на которое организована ссылка через shared_ptr и при этом раньше этого поля «умирает» объект, в котором оно содержится — с логикой приложения что-то категорически не так и нужно фиксить это, а не использовать сомнительные костыли.

И ещё вопрос — насколько нормальной практикой является оборачивание поля объекта в shared_ptr в стиле того, как это в том же примере сделано? Сам несколько раз порывался делать так, но не решился — показалось, что как-то это неправильно.
" с логикой приложения что-то категорически не так и нужно фиксить это, а не использовать сомнительные костыли." —

В реальной жизни «левая» логика часто может быть:
1. В сторонней библиотеке, не доступной для изменений.
2. В нашем коде, который по сотне причин не доступен для изменений. Например: время изменения (долгое), цена изменения (большая), сертификация/утверждение измененной версии, левая нога начальника, с которой он встал сегодня, и т.д.
Поддержу semenyakinVS

В примере «Совместное использование объектов» есть 2 варианта (и вообще — их всего 2) когда функция store_for_later(std::shared_ptr) использует экземпляр типа Y после «смерти» экземпляра типа X:
1) асинхронный вызов.
2) сохранение в другой глобальной структуре, к которой могут обращаться иные функции.

Если мы говорим про первый вариант, то данное использование вообще не верно, т.к. объект явно разделяемый, а объектов синхронизации нет => архитектурная ошибка.
Если мы говорим про второй вариант, а в статье судя по названию функции он как раз и имелся в виду, то мы САМИ ОБЯЗАНЫ использовать сию функцию с особой осторожностью и САМИ ОБЕСПЕЧИТЬ время жизни объекта типа X достаточное для работы всех библиотечных функций, которые зависят от поля типа Y. Иначе у нас ситуация один в один с «висячей ссылкой», а это => архитектурная ошибка.
Если мы говорим про первый вариант, то данное использование вообще не верно, т.к. объект явно разделяемый, а объектов синхронизации нет => архитектурная ошибка.
Почему же? Если первый поток забывает про объект сразу после совершения асинхронного вызова — то объект не является разделяемым между потоками, он между ними передается.

Кстати, кто вообще сказал, что нет объектов синхронизации? Нам никто не мешает использовать этот подход и синхронизацию одновременно. Просто статья не про синхронизацию была.

… Иначе у нас ситуация один в один с «висячей ссылкой», а это => архитектурная ошибка.
Почему висячая ссылка является архитектурной ошибкой, если она «висит» правильно?

PS вообще говоря, данный конструктор нужен в первую очередь для таких функций как std::static_pointer_cast и std::dynamic_pointer_cast. Но когда мощности наследования не хватает — имеет смысл использовать агрегацию — и тут этот конструктор понадобится.
1. Я вовсе не спорил с semenyakinVS, а лишь дополнил его, сказав, что не всегда грязное белье можно постирать. Поэтому…
2. Не вижу, как хоть что-то из вышеописанного опровергает мои отнюдь не архитектурные аргументы.
Пример —
внешний объект — Dom document, например xml.
внутри него — узлы, хранящие weak_ptr на документ и позволяющие его получить.
Тогда можно передать клиенту узел для обработки, а не пару «документ+узел».
Так документ будет жить за счёт жизни узла, а получить документ можно будет, вызвав у узла метод GetDocument(), создающий shared-ссылку из weak.
Другой пример: человек, у него есть руки-ноги. Держишь человека за руку, логично ожидать, что он не убежит (не удалится)
В COM
Не дописал. В com передается для похожих целей pOuterUnknown для целей агрегации
Кажется, понял. И, если я всё правильно понял, я, кажется, сталкивался с такой проблемой во время работы над системой отображения иерархии объектов (иерахрические view в mvc, как в Cocoa да и, наверно, почти во всех GUI-архитектурах). Я решил вопрос поставив следующее условие: родительская вьюшка должна жить до тех пор, пока имеется хоть одна ссылка на неё либо на вложенные в неё вьюшки. В вашем примере — если есть хотя бы одна ссылка на руку/ногу или на тело (если нет ссылки на ногу, например, но есть на тело — нога удаляется).

По поводу GetDocument() — а это безопасно «увеличивать» степень владения объектом? Я всегда писал свой код исходя из того, что делать shared-ссылку из weak-ссылки архитектурно некорректно: мы вводим объект в «сильное» владение из контекста, в котором подразумевалось «слабое» владение… Или в некоторых случаях это всё-таки допустимо?
Разумеется, это допустимо! Ведь без этой операции слабые ссылки и вовсе теряют смысл :)

Конкретно же в этом случае можно отказаться даже от слабых ссылок и хранить просто указатель — ведь дерево не исчезнет пока жива хоть одна вершина.
Разумеется, это допустимо! Ведь без этой операции слабые ссылки и вовсе теряют смысл :)


Ага… Вот как. Буду знать. Я думал что они используются для получения защищённого «слабого» доступа к объекту — то есть от raw-указателя отличаются тем, что позволяют выполнять проверку на то, есть ли ещё объект по ссылке.

Конкретно же в этом случае можно отказаться даже от слабых ссылок и хранить просто указатель — ведь дерево не исчезнет пока жива хоть одна вершина.


Не совсем, там API именно на умных указателях построено.

В таком вот стиле
StrongRef<View> theView =  window()->rootView();
StrongRef<View> theSubview = theView.createSubview();
StrongRef<View> theSubSubview = theSubview.createSubview(); // Иерархия: theView <- theSubview <- theSubSubview.

// Освобождаем theSubview. Вьюшка, на которую ссылалась эта ссылка не удаляется -
// на неё есть ещё один StrongRef во вьюшке, на которую ссылается theSubSubview.
theSubview = StrongRef<View>::Null();

// А вот теперь удаляется и та вьюшка, на которую theSubview ссылалась, и та, которая на которую theSubSubview -
// мы убрали ссылку на вьюшку, которая ссылается на парент-вьюшку.
theSubSubview = StrongRef<View>::Null();


П.С.: Как-то кривовато код подсвечивается… Вроде С++ поставил ему, а всё равно криво как-то.
Ага… Вот как. Буду знать. Я думал что они используются для получения защищённого «слабого» доступа к объекту — то есть от raw-указателя отличаются тем, что позволяют выполнять проверку на то, есть ли ещё объект по ссылке.
А как вы, собственно, можете получить доступ к объекту, на который указывает weak_ptr, кроме как вызвав метод lock, который возвращает shared_ptr?
М-да, внимательнее почитаю о стандартных умных указателях… Если честно, я сразу свои писал. Да, стыдно.
Если ваш слабый указатель ведет себя по-другому, проверьте архитектуру своей программы. Представьте такую ситуацию:

  StrongRef<Foo> p1 = new Foo();
  WeakRef<Foo> p2 = p1;

  if (p2) p2->bar(); // Или любой другой способ разыменования слабого указателя с предварительной проверкой


Вроде бы все нормально? А теперь представим, что метод bar() косвенно очищает p1 где-то внутри. Что получилось? А получилось, что пока метод объекта исполнялся, сам объект удалили. Упс!

Именно поэтому в STL нельзя разыменовать слабый указатель иначе как создав временный сильный.
А теперь представим, что метод bar() косвенно очищает p1 где-то внутри.


А не будет ли такое поведение само по себе архитектурно ошибочным?
Нет. Ведь это может быть слушатель сетевого интерфейса, который вызывает обработчик сетевых запросов, который обращается к маршрутизатору сетевых запросов, который передает запрос службе управления, которая разбирает запрос и обнаруживает команду на перезапуск сервера.

Умные указатели должны работать независимо от того, насколько запутана архитектура программы.
Добавлю, что weak_ptr может еще неожиданно обнулиться в многопоточном приложении, когда в нашем потоке есть только слабая ссылка, а сильные ссылки удалились в фоновом потоке.
Необходимое получение сильного указателя гарантирует, что объект будет жив, пока мы с ним работаем.

Ну и вот так пользоваться weak ptr в многопоточной среде небезопасно:
weak_ptr<Obj> w;
...
if (w.lock())
{
   // до повторного вызова lock счетчик ссылок может обнулиться в другом потоке, 
   // объект разрушиться. 
   // В дебаге мы словим assert, а в релизе - неопределенное поведение, 
   // причем это будет проявляться нестабильно
   w.lock()->DoSomething()
}

Вот годная статья «Пять подводных камней при использование shared_ptr»
habrahabr.ru/post/191018
Спасибо. На вашем примере окончательно понял зачем нужен lock(). Действительно, без такой штуки нельзя нормально работать в многопоточном коде со слабыми указателями.
Набросал proof of concept (документ с рекурсивным добавлением глав), иллюстрирующий пользу от данного конструктора, а также от enable_shared_from_this:
документ не удаляется, пока есть ссылки хотя бы на одну из его глав. Т.к. каждая глава хранит weak_ptr на документ и обычный указатель на parent, всегда есть возможность получить как сам документ, так и ходить вверх по родительским главам.

Проверено на VS 2013
#include <memory>
#include <vector>
#include <set>
#include <stdexcept>
#include <algorithm>
#include <cassert>
#include <string>
#include <iostream>
#include <functional>

using namespace std;

struct Document;
typedef weak_ptr<Document> DocumentWeakPtr;
typedef shared_ptr<Document> DocumentSharedPtr;
typedef shared_ptr<const Document> ConstDocumentSharedPtr;

struct Chapter;
typedef weak_ptr<Chapter> ChapterWeakPtr;
typedef shared_ptr<Chapter> ChapterSharedPtr;
typedef shared_ptr<const Chapter> ConstChapterSharedPtr;

// Интерфейс контейнера глав
struct IChapterContainer
{
	virtual size_t GetChapterCount()const throw() = 0;
	virtual ChapterSharedPtr GetChapter(size_t index) = 0;
	virtual ConstChapterSharedPtr GetChapter(size_t index)const = 0;
	virtual const ChapterSharedPtr & AddChapter(const ChapterSharedPtr& chapter) = 0;
	virtual const ChapterSharedPtr & RemoveChapter(const ChapterSharedPtr& chapter) = 0;
	virtual DocumentSharedPtr GetDocument() throw () = 0;
	virtual ConstDocumentSharedPtr GetDocument()const throw() = 0;
protected:
	~IChapterContainer(){}
};

// Реализация контейнера глав
template <typename ThisType>
struct ChapterContainer 
	: IChapterContainer
	, enable_shared_from_this<ThisType>
{
	// Добавляет главу в контейнер (строгая гарантия безопасности исключений)
	const ChapterSharedPtr & AddChapter(const ChapterSharedPtr& chapter) override
	{
		// Главу нельзя добавить, если она находится в каком-либо контейнере
		if (chapter->m_parent)
		{
			throw logic_error("The chapter is already added to somewhere");
		}

		// Запрещаем добавлять главы, относящиеся к чужому документу
		if (chapter->m_document.lock() != GetDocument())
		{
			throw logic_error("The chapter belongs to a different document");
		}
		
		m_chapters.push_back(chapter);
		chapter->m_parent = this;
		return chapter;
	}

	// Удаляет главу из контейнера (строгая гарантия безопасности исключений)
	const ChapterSharedPtr & RemoveChapter(const ChapterSharedPtr& chapter)
	{
		// Нельзя удалить чужую главу
		if (chapter->m_parent != this)
		{
			throw invalid_argument("The chapter does not belong to this chapter container");
		}

		auto pos = find_if(m_chapters.begin(), m_chapters.end(), [&](const ChapterWeakPtr& item){
			return item.lock() == chapter;
		});

		assert(pos != m_chapters.end());
		m_chapters.erase(pos);
		chapter->m_parent = nullptr;

		return chapter;
	}

	virtual size_t GetChapterCount() const throw() override
	{
		return m_chapters.size();
	}

	virtual ChapterSharedPtr GetChapter(size_t index) override
	{
		return m_chapters.at(index).lock();
	}

	virtual ConstChapterSharedPtr GetChapter(size_t index) const override
	{
		return m_chapters.at(index).lock();
	}

private:
	vector<ChapterWeakPtr> m_chapters;
};

// Глава. Хранит заглавие, ссылки на документ-владелец и на родительский контейнер
// Глава также может хранить внутри себя дочерние главы
struct Chapter 
	: ChapterContainer<Chapter>
	
{
	friend struct Document;
	friend struct ChapterContainer < Chapter > ;
	friend struct ChapterContainer < Document >;

	virtual ConstDocumentSharedPtr GetDocument()const throw() override
	{
		return m_document.lock();
	}

	virtual DocumentSharedPtr GetDocument() throw() override
	{
		return m_document.lock();
	}

	// Возвращаем ссылку на родительский контейнер
	shared_ptr<const IChapterContainer> GetParent()const throw()
	{
		return {GetDocument(), m_parent};
	}

	// Возвращаем ссылку на родительский контейнер
	shared_ptr<IChapterContainer> GetParent() throw()
	{
		return{ GetDocument(), m_parent };
	}


	string GetTitle()const
	{
		return m_title;
	}

	~Chapter()
	{
		cout << "The chapter" << m_title << " has been destroyed" << endl;
	}

	Chapter(const Chapter&) = delete;
	Chapter& operator=(const Chapter&) = delete;
private:
	Chapter(const DocumentSharedPtr & document, const string& title = string())
		: m_document(document)
		, m_title(title)
	{
	}

	DocumentWeakPtr m_document;
	string m_title;
	IChapterContainer* m_parent = nullptr;
};

// Документ. Управляет главами
struct Document 
	: ChapterContainer<Document>
{
	Document(const Document&) = delete;
	Document& operator=(const Document&) = delete;

	static DocumentSharedPtr Create()
	{
		// Нельзя использовать make_shared из-за приватного деструктора
		return DocumentSharedPtr(new Document());
	}

	virtual DocumentSharedPtr GetDocument() throw () override
	{
		return shared_from_this();
	}

	virtual ConstDocumentSharedPtr GetDocument() const throw() override
	{
		return shared_from_this();
	}

	// Создает главу и сохраняет ее внутри документа. Обеспечивает строгую гарантию безопасности исключений
	shared_ptr<Chapter> CreateChapter(const string& title = string())
	{
		// Получаем сильную ссылку на самих себя
		auto strongThis = shared_from_this();

		// Создаем главу, которая сохранит слабую ссылку на нас
		// make_unique не доступен из-за приватного конструктора, поэтому создаем через new
		unique_ptr<Chapter> chapter(new Chapter(strongThis, title)); // Может выбросить исключение, но не страшно

		// Помещаем ее во множество глав документа
		auto result = m_chapters.insert(move(chapter)); // Может выбросить исключение, но не страшно

		// Оборачиваем указатель указатель в shared_ptr
		return shared_ptr<Chapter>(strongThis, result.first->get()); // не выбрасывает исключений
	}

	~Document()
	{
		cout << "The document is being destroyed" << endl;
	}
private:
	Document() = default;

	set<unique_ptr<Chapter>> m_chapters;
};

void PrintTableOfContents(const Document& doc)
{
	function<void(const IChapterContainer& container, const string& prefix)> PrintContainer;
	PrintContainer = [&](const IChapterContainer& container, const string& prefix)
	{
		auto numChapters = container.GetChapterCount();
		for (size_t i = 0; i < numChapters; ++i)
		{
			auto chapter = container.GetChapter(i);
			auto intro = prefix + to_string(i + 1) + ".";
			cout << intro << " " << chapter->GetTitle() << endl;
			PrintContainer(*chapter, intro);
		}
	};
	PrintContainer(doc, string());
}

void main()
{
	ChapterSharedPtr someChapter;
	{
		auto document = Document::Create();

		assert(document->GetDocument() == document);

		auto chapter1 = document->AddChapter(document->CreateChapter("Chapter 1"));
		assert(chapter1->GetParent() == document);

		auto chapter2 = document->AddChapter(document->CreateChapter("Chapter 2"));
		auto chapter3 = document->AddChapter(document->CreateChapter("Chapter 3"));
		auto chapter21 = chapter2->AddChapter(document->CreateChapter("Chapter 2.1"));
		auto chapter22 = chapter21->GetParent()->AddChapter(document->CreateChapter("Chapter 2.2"));
		assert(chapter21->GetParent() == chapter2);

		PrintTableOfContents(*document);

		cout << "-------Moving Chapter 1 into Chapter 2.1" << endl;

		chapter21->AddChapter(document->RemoveChapter(chapter1));
		assert(chapter1->GetParent() == chapter21);

		PrintTableOfContents(*document);

		cout << "------- Removing Chapter 2.1" << endl;

		chapter2->RemoveChapter(chapter21);
		assert(!chapter21->GetParent());

		PrintTableOfContents(*document);

		someChapter = chapter2;
		// Несмотря на то, что ссылка document уничтожится при выходе из блока,
		// счетчик ссылок на документ не обнулится за счет aliasing-ссылки someChapter из внешнего блока
	}

	cout << "------- Printing the document obtained through the chapter.GetDocument()" << endl;

	// Восстанавливаем ссылку на документ по имеющейся ссылке на одну из глав
	auto doc = someChapter->GetDocument();
	PrintTableOfContents(*doc);

	// При выходе из этого блока счетчик ссылок на документ обнулится и он с главами будет выпилен
	cout << "------- Leaving the function" << endl;
}

Вывод в stdout
1. Chapter 1
2. Chapter 2
2.1. Chapter 2.1
2.2. Chapter 2.2
3. Chapter 3
-------Moving Chapter 1 into Chapter 2.1
1. Chapter 2
1.1. Chapter 2.1
1.1.1. Chapter 1
1.2. Chapter 2.2
2. Chapter 3
— Removing Chapter 2.1
1. Chapter 2
1.1. Chapter 2.2
2. Chapter 3
— Printing the document obtained through the chapter.GetDocument()
1. Chapter 2
1.1. Chapter 2.2
2. Chapter 3
— Leaving the function
The document is being destroyed
The chapterChapter 2.2 has been destroyed
The chapterChapter 2.1 has been destroyed
The chapterChapter 3 has been destroyed
The chapterChapter 2 has been destroyed
The chapterChapter 1 has been destroyed



Оно же на гитхаб
Есть, правда, одно НО в этом примере. Т.к. документ владеет всеми находящимися в нем главами, удалятся они лишь вместе с ним самим
> Возможная польза от этого странного эффекта является предметом для обсуждения.
А я уже осознал пользу. Представим себе, в сегменте данных висит объект-затычка без данных (или с константными данными), которым управлять вообще не нужно. Оказывается, на этот объект можно тоже создавать shared_ptr.
Sign up to leave a comment.

Articles