This constructor is so secret, not even STL maintainers know about it...
Stephan T. Lavavej
Этот конструктор настолько секретный, что даже сопровождающие STL не знают о нём...
пер.: Door

У std::shared_ptr есть небольшой секрет: очень полезный конструктор, о котором большинство программистов даже не слышали. Он был добавлен только в стандарте С++11, и его не было даже в TR1 версии shared_ptr. Однако он поддерживается gcc с версии 4.3, и компилятором MSVC еще с времен Visual Studio 2010. В Boost он появился примерно с 1.35.0.

В большинстве обучающих материалов, в которых описывается std::shared_ptr ничего нет об этом конструкторе. Скотт Майерс ни словом не обмолвился о нем в «Effective Modern C++», другой автор — Nicolai Josuttis уделил этому конструктору около половины страницы в своей книге «The C++ Standard Library».



Итак, что представляет собой этот секретный конструктор?

Это псевдонимный конструктор (aliasing constructor).

Псевдонимы shared_ptr


Что же делает это конструктор? Он позволяет создать shared_ptr, который разделяет владение с другим shared_ptr, но (внимание!) имеет другой указатель. И я не имею ввиду указатель другого типа, который был просто приведен от одного типа к другому, я говорю об абсолютно другом значении указателя. То есть можно одновременно иметь shared_ptr<std::string> и shared_ptr<double>, которые разделяют ответственность за удаление исходного указателя (если не очень понятно — далее на примерах будет понятней).

Конечно, только один указатель будет удален, когда все владеющие им shared_ptr'ы будут удалены. То есть будет освобожден только исходный указатель, а не то, новое значение, которое мы получили в псевдонимном конструкторе.

Но если этот новый указатель не будет освобожден, зачем тогда использовать этот странный конструктор? Он позволяет передать объект shared_ptr, который ссылается на подобъекты (поля класса, например) и уберечь родительский объект от преждевременного удаления.

Совместное использование подобъектов


Предположим, что есть класс X, с полем типа Y, который тоже является некоторым классом

struct X{
    Y y;
};

Теперь представим, что у нас есть объект shared_ptr<X>, px а нам нужно передать в некоторую библиотечную функцию поле этого объекта px->y, причем в виде shared_ptr<Y>. Это можно сделать следующим образом: сконструировать shared_ptr<Y>, который будет указывать на нужное поле, указать свою функцию удаления, которая не деает ничего, таким образом библиотечная функция не удалит объект Y. Но что произойдет в случае, если в то время, как shared_ptr<Y> обрабатывается в библиотеке, исходный px выйдет из области видимости?

struct do_nothing_deleter{
    template<typename> void operator()(T*){}
};

void store_for_later(std::shared_ptr<Y>);

void foo(){
    std::shared_ptr<X> px(std::make_shared<X>());
    std::shared_ptr<Y> py(&px->y,do_nothing_deleter());
    store_for_later(py);
} // объект X удален

Теперь shared_ptr<Y> указывает в недра удаленного объекта, что, мягко говоря, нежелательно. Чтобы избежать такой ситуации, нужно воспользоваться псевдонимным конструктором. Вместо того, чтобы писать функцию удаления, которая не делает ничего, мы просто разделяем владение shared_ptr<Y> с shared_ptr<X>. Теперь shared_ptr<Y> будет держать родителя живым.

void bar(){
    std::shared_ptr<X> px(std::make_shared<X>());
    std::shared_ptr<Y> py(px,&px->y);
    store_for_later(py);
} // объект X все еще жив

Указателям необязательно быть в какой то связи. Единственное требование — чтобы время жизни нового указателя было как минимум таким же, как и у исходного shared_ptr. Если у нас есть новый класс X2, который содержит указатель на Y, мы все равно можем использовать наш псевдонимный конструктор

struct X2{
    std::unique_ptr<Y> y;
    X2():y(new Y){}
};

void baz(){
    std::shared_ptr<X2> px(std::make_shared<X2>());
    std::shared_ptr<Y> py(px,px->y.get());
    store_for_later(py);
} // объект X2 все еще жив

Такой подход может быть применен для классов, которые используют идиому pimpl. Или для деревьев, если необходима возможность пройтись по дочерним элементам по указателям, и быть уверенным, что дерево еще живо. Или можно держать динамически подгруженную библиотеку открытой пока используются указатели на переменные этой библиотеки. Если наш класс X подгружает библиотеку в конструкторе и выгружает в деструкторе, тогда мы можем сделать shared_ptr<Y>, который будет вместе с shared_ptr<X> держать библиотеку открытой до тех пор, пока shared_ptr<Y> не будет уничтожен или сброшен (т.е. вызвана операция reset()).

Детали


Сигнатура конструктора:

template<typename Other,typename Target>
shared_ptr(shared_ptr<Other> const& other,Target* p);

Как обычно при создании shared_ptr<T> указатель p должен быть типом Т*, или иметь возможность преобразования. Однако на тип Other нет никаких ограничений. Новый созданный объект разделяет владение с other, таким образом other.use_count() увеличивается на 1. Значение, возвращаемое методом get() нового объекта — это static_cast<T*>(p).

Есть небольшой нюанс: если other это пустой shared_ptr, например, полученный конструктором по умолчанию, тогда новый shared_ptr также будет пустым, и будет иметь use_count() == 0. Но значение не будет равным нулю, если p не был равен NULL.

int i;
shared_ptr<int> sp(shared_ptr<X>(),&i);
assert(sp.use_count()==0);
assert(sp.get()==&i);

Возможная польза от этого странного эффекта является предметом для обсуждения.

Несколько слов в заключение


Этот мало известный конструктор может принести большую пользу при создании объектов shared_ptr, которые будут ссылаться на различные части некоторой структуры данных и будут держать всю эту структуру живой.

Конечно, не каждом программисту это понадобится, но многих других этот конструктор убережет от лишней головной боли.

Ссылки по теме

Обсуждение на reddit
Описание конструктора на cppreference

Комментарии (24)


  1. knagaev
    29.07.2015 09:43
    +3

    Информация полезная (чтобы не городить костыли и подпорки) и интересная.
    Единственное — у вас опечатка в заголовке.
    Я сначала даже подумал, что это действительно какой-то секретный новый share_ptr :)


    1. kpdev Автор
      29.07.2015 09:50

      Спасибо за замечание :) поправлено


  1. kpdev Автор
    29.07.2015 09:49
    -1

    del


  1. Door
    29.07.2015 10:00
    +4

    И позволю себе процитировать слова Stephan T. Lavavej:

    This constructor is so secret, not even STL maintainers know about it...

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

    :)

    Спасибо за перевод.


    1. kpdev Автор
      29.07.2015 10:28
      +1

      Спасибо за цитату. Не смог удержаться, чтобы не утащить ее в эпиграф :)


  1. semenyakinVS
    29.07.2015 12:11
    +6

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

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


    1. dyadyaSerezha
      29.07.2015 13:13
      +2

      " с логикой приложения что-то категорически не так и нужно фиксить это, а не использовать сомнительные костыли." —

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


      1. DISaccount
        29.07.2015 17:11
        +1

        Поддержу semenyakinVS

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

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


        1. mayorovp
          29.07.2015 18:58
          +2

          Если мы говорим про первый вариант, то данное использование вообще не верно, т.к. объект явно разделяемый, а объектов синхронизации нет => архитектурная ошибка.
          Почему же? Если первый поток забывает про объект сразу после совершения асинхронного вызова — то объект не является разделяемым между потоками, он между ними передается.

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

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

          PS вообще говоря, данный конструктор нужен в первую очередь для таких функций как std::static_pointer_cast и std::dynamic_pointer_cast. Но когда мощности наследования не хватает — имеет смысл использовать агрегацию — и тут этот конструктор понадобится.


        1. dyadyaSerezha
          29.07.2015 19:40

          1. Я вовсе не спорил с semenyakinVS, а лишь дополнил его, сказав, что не всегда грязное белье можно постирать. Поэтому…
          2. Не вижу, как хоть что-то из вышеописанного опровергает мои отнюдь не архитектурные аргументы.


    1. alexeymalov
      29.07.2015 20:39
      +4

      Пример —
      внешний объект — Dom document, например xml.
      внутри него — узлы, хранящие weak_ptr на документ и позволяющие его получить.
      Тогда можно передать клиенту узел для обработки, а не пару «документ+узел».
      Так документ будет жить за счёт жизни узла, а получить документ можно будет, вызвав у узла метод GetDocument(), создающий shared-ссылку из weak.
      Другой пример: человек, у него есть руки-ноги. Держишь человека за руку, логично ожидать, что он не убежит (не удалится)
      В COM


      1. alexeymalov
        29.07.2015 20:45
        +1

        Не дописал. В com передается для похожих целей pOuterUnknown для целей агрегации


      1. semenyakinVS
        29.07.2015 21:45

        Кажется, понял. И, если я всё правильно понял, я, кажется, сталкивался с такой проблемой во время работы над системой отображения иерархии объектов (иерахрические view в mvc, как в Cocoa да и, наверно, почти во всех GUI-архитектурах). Я решил вопрос поставив следующее условие: родительская вьюшка должна жить до тех пор, пока имеется хоть одна ссылка на неё либо на вложенные в неё вьюшки. В вашем примере — если есть хотя бы одна ссылка на руку/ногу или на тело (если нет ссылки на ногу, например, но есть на тело — нога удаляется).

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


        1. mayorovp
          29.07.2015 21:52

          Разумеется, это допустимо! Ведь без этой операции слабые ссылки и вовсе теряют смысл :)

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


          1. semenyakinVS
            29.07.2015 23:15

            Разумеется, это допустимо! Ведь без этой операции слабые ссылки и вовсе теряют смысл :)


            Ага… Вот как. Буду знать. Я думал что они используются для получения защищённого «слабого» доступа к объекту — то есть от 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();
            


            П.С.: Как-то кривовато код подсвечивается… Вроде С++ поставил ему, а всё равно криво как-то.


            1. mayorovp
              30.07.2015 06:28

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


              1. semenyakinVS
                30.07.2015 11:41

                М-да, внимательнее почитаю о стандартных умных указателях… Если честно, я сразу свои писал. Да, стыдно.


                1. mayorovp
                  30.07.2015 12:24

                  Если ваш слабый указатель ведет себя по-другому, проверьте архитектуру своей программы. Представьте такую ситуацию:

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


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

                  Именно поэтому в STL нельзя разыменовать слабый указатель иначе как создав временный сильный.


                  1. semenyakinVS
                    30.07.2015 13:52

                    А теперь представим, что метод bar() косвенно очищает p1 где-то внутри.


                    А не будет ли такое поведение само по себе архитектурно ошибочным?


                    1. mayorovp
                      30.07.2015 14:00

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

                      Умные указатели должны работать независимо от того, насколько запутана архитектура программы.


                  1. alexeymalov
                    30.07.2015 14:01

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

                    Ну и вот так пользоваться weak ptr в многопоточной среде небезопасно:

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

                    Вот годная статья «Пять подводных камней при использование shared_ptr»
                    habrahabr.ru/post/191018


                    1. semenyakinVS
                      30.07.2015 14:14

                      Спасибо. На вашем примере окончательно понял зачем нужен lock(). Действительно, без такой штуки нельзя нормально работать в многопоточном коде со слабыми указателями.


  1. alexeymalov
    30.07.2015 01:14
    +2

    Набросал 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-


    1. alexeymalov
      30.07.2015 02:03
      +1

      Есть, правда, одно НО в этом примере. Т.к. документ владеет всеми находящимися в нем главами, удалятся они лишь вместе с ним самим