10 лет практики. Часть 2: ресурсы

    Здравствуйте. Я планировал написать большую статью об управлении ресурсами в С++.
    Но на практике, тема эта такая сложная и многогранная, что я хочу остановиться на определённой методике, которой пользуюсь сам. Данная методика не является спасением на все случаи жизни, но экономит много времени и нервов при работе с объектами. При этом, не является широко известной.

    Подход этот называется «everything belongs somewhere». О нём я впервые узнал, пересаживаясь c Qt на замечательный фреймворк U++, созданный группой авторов во главе с Миреком Фидлером (Mirek Fídler). Произошло это около пяти лет назад, так что я поделюсь не только самим методом, но и практическими советами исходя из опыта его применения.

    Вкратце, суть методики можно описать фразой: «всё кому-то принадлежит».
    Класс-хозяин несёт в себе все ресурсы, необходимые для полноценной работы. Например:
    Объекты, описывающие узлы бункера, включаются в класс бункера. Объекты-помощники, а также объекты с данными для класса сложных расчётов, включаются в него. И так далее.
    Исключение составят несколько глобальных переменных, представляющих собой самые основные объекты. В некотором роде, и они являются членами безымянного глобального «класса».

    Теперь мы говорим следующее:
    1. объект определяется в виде обычного члена класса, либо помещается в контейнер, являющийся членом класса
    2. право владения объектом не передаётся

    Заметьте: определяется не в виде указателя, не в виде ссылки, а обычным образом. Мы можем взять указатель на объект, передавать его куда-то, но только для использования. То есть сторонний код не может сделать delete или new нашему указателю. Им управляет только класс-хозяин.


    Что делать, если объект будет создан не сразу?
    В этом случае, понадобится либо одиночный контейнер (вроде unique_ptr), либо контейнер-массив. Главная их функция — автоматическое удаление объекта в деструкторе. Всё!

    А вот здесь остановимся и подумаем, что же мы получили.

    1. Мы избавились от ручных вызовов new/delete. Причём избавились так хорошо, что даже в случае выбросов исключения, а также любых других ситуаций, наши ресурсы будут гарантированно удалены.

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

    Согласитесь, детерминированное управление ресурсами — это большой контраст с возможностью в любой момент наткнуться на процедуру удаления мусора (скажем, пока играется видео, или нужно остановить сервопривод), либо возможностью тряски мусоросборщика в тщетных попытках удалить ресурс в требуемый момент.

    3. Когда объект-хозяин выдаёт указатель на свой член, он может гарантировать, что по этому указателю существует нужный объект нужного класса. Когда мы пользуемся таким указателем, мы работаем в области видимости объекта-хозяина. А значит нам гарантируется валидность указателя. Понимаете? Время жизни объекта по указателю гарантируется самим компилятором, который на этапе компиляции проверит видимость объекта-хозяина.
    Это же практически мечта: эффективно работать через указатели, валидность которых проверена ещё на этапе компиляции! И всё это — без накладных расходов.

    4. Наконец, когда мы поняли, что у нас вопросы создания, удаления и валидности доступа решены без накладных расходов, мы приходим к тому, что «сложные» умные указатели со счётчиком ссылок и более сложными механизмами внутри, становятся попросту не нужны.

    Мы накладываем ограничения на структуру программы. Проводим более тщательное проектирование и планирование. И за счёт этого, мы приходим к гораздо более детерминированной работе с ресурсами без накладных расходов. Отказываемся от всех явных вызовов delete, большинства явных вызовов new и контейнеров со счётчиками ссылок — и уходим от всех проблем, им сопутствующих.

    Такая программа качественно отличается от программы на С. Здесь мы скрываем из области видимости всё, что не относится к текущему функционалу. Здесь, чаще всего, у нас нет десятков глобальных переменных или объектов и сотни глобальных функций, их обслуживающих. Вместо этого, глобальных переменных-объектов, как правило, несколько. Каждый из них является достаточно автономным и реализует достаточно большой функционал, реализуемый включаемыми объектами. В прошлой статье я упоминал, что эти объекты желательно делать закрытыми членами, так как это минимизирует сцепления внутри иерархии классов.

    Вот, если коротко, в чём заключается подход.

    Теперь о подводных камнях.
    Прежде всего, это может значить совершенно другой стиль декомпозиции на классы, по сравнению с мейнстримовым подходом, активно использующим умные указатели со счётчиком ссылок. Зачастую, наложить указанные ограничения очень непросто, и подход с умными указателями кажется более привлекательным. Но, как показывает практика разработчиков фреймворка и практика автора статьи — лучше постараться, и сделать всё в соответствии с подходом. Эти усилия в десятки раз окупятся при отладке и поддержке программы.

    Во-вторых, при активном использовании, у вас появится необходимость в хороших, быстрых контейнерах, которые не будут накладывать на классы элементов ограничения в виде операторов копирования. Примитивная реализация таких контейнеров будет довольно простой. Эффективная реализация — вопрос отдельной статьи. Потому что для эффективной работы нужна реализация передачи (оно же разрушающее копирование), а также переноса (оно же сверхбыстрое копирование) — если будет интересно, эти темы будут обсуждены далее.

    В-третьих, из этих правил может вытекать следующая стратегия работы с GUI. Контролы принадлежат их логическому владельцу (не окну, а тому, кто хранит в них свою семантику, ведь он её владелец). В этом смысле, у вас есть два выбора: апологеты MVC могут всё оставить как есть. Либо, вы можете воспользоваться таким подходом, который, как показывает практика, зачастую себя окупает. Разумеется, это не священная корова, а наоборот — подлежит критическому осмыслению и проверке на практике.

    Пожалуйста, указывайте на неточности, пробуйте, критикуйте, применяйте. Буду рад любым откликам.

    Литература:
    1. U++ overview.

    В следующей части предполагается обсудить передачу (pick beaviour) и перенос (moveable) объектов. Будет показано, как эти возможности дают существенный прирост скорости работы с объектами. А контейнеры на их основе в 4-5 раз быстрее STL.
    После чего можно будет переходить к турбо-скоростной и очень безопасной реализации многопоточности, вдохновлённой Эрлангом.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 40

      +4
      Не совсем понял как же все таки происходит управление объектами, кто отвечает за их создание/уничтожение. Во время прочтения сложилось впечатление, что мы еще на этапе проектирования программы определяем какие конкретно у нас объекты будут и знаем заранее их конкретное количество?
      Как быть в случае динамического создание объектов, если нам может понадобится список из 5 объектов, а может из 105, как тут обойтись без new/delete?
        +1
        В этом случае, как я писал в статье, используются контейнеры:
        void Worker::Work()
        {
            Worker::container.AddElement(Element(params)); //так
            Worker::container.AddElement(new Element(params)); //или на худой конец так - в зависимости от типа контейнера
        }
        
        0
        И что, такой подход нормально работает когда нужно сделать что-то сложнее формы с гвоздями прибитыми на ней кнопками?
          0
          Очевидно да. Учитывая, с одной стороны, что на нём построен фреймворк с IDE и массой библиотек (в т.ч. связанных с GUI), а с другой — исходя из опыта разработки массы программ мною, могу точно сказать — работает отлично.
          Подход предлагает более детерминированное управление ресурсами за счёт увеличения времени на проектирование иерархии классов, учитывающей время жизни ресурсов. То есть определённая альтернатива для тех, кому важно писать более стабильно работающие программы.
            +1
            Честно сказать, вижу на скринах вижу нечто совершенно не гибкое и уже устаревшее аля форточки 95. Сейчас такие программы уже мало кому нужны, их и так over 9000 написано. А есть ли там продвинутый фреймворк для 2D и 3D графики?
              +1
              Есть.
            +1
            Поддержу автора статьи, — очень даже хороший подход, и гибкости не лишает. Большие проекты тоже есть, у меня, например.
            0
            п.3 "… Время жизни объекта по указателю гарантируется самим компилятором, который на этапе компиляции проверит видимость объекта-хозяина." Поясните пожалуйста. Лучше примером кода.
              0
              Вокруг этого всё и строится. Все ограничительные пункты как раз и созданы, чтобы максимально приблизить это состояние.
              О чём конкретно речь. Указатель выдаётся его хозяином. При этом, очевидно, чтобы хозяин выдал указатель, сам хозяин должен быть виден из вызываемого кода. Эта область ограничена областью видимости хозяина.
              Значит, если мы «видим» хозяина, значит хозяин может выдать нам валидный указатель на своего члена. Это гарантирует сам хозяин. Потому что когда он выдаёт этот указатель, либо это указатель на обычный член-объект (и здесь вообще говорить не о чем), либо это указатель на некий объект в контейнере, создаваемый по какому-то условию — это в более сложных случаях. И тогда, опять, мы на уровне хозяина точно знаем — существует ли объект, а значит можем гарантировать факт его существования.
            • UFO just landed and posted this here
                0
                Конечно, предлагаемую методику можно свести к умным указателям. Но зачем? Это одна из тех вещей, которые, в общем-то, становятся не нужны.

                Вообще, если честно, из меня плохой рассказчик. Мне ближе практика. Потому с удовольствием прокомментирую ваши примеры:
                1. Если сокетами владеет некий класс, а сами сокеты, в конечном итоге, хранятся в его контейнере, то всё будет хорошо. А отдельным «воркерам» будут даваться ссылки/указатели на соответствующие сокеты.
                При этом, при закрытии программы и вызове деструктора хозяина сокетов, все оставшиеся сокеты будут уничтожены. И всё это — без каких-либо оверхедов в рантайме, свойственных умным указателям (не забываем, что рефкаунтинг скорее всего атомарный, что ещё делает сравнение ещё более актуальным).

                2. Отсутствие проблем с циклическими зависимостями — одна из основных причин, почему умные указатели могут оказаться не лучшей практикой, особенно в сравнении с более простым подходом.
                Давайте вспомним: хозяин у объекта всегда один, и только один. То есть, никаких больше циклических зависимостей.

                Готов обсудить ещё примеры. Это гораздо интереснее, чем теоретическая писанина.)
                • UFO just landed and posted this here
                    0
                    Спасибо за хорошие комментарии и привязку к реальным примерам.

                    Вопрос «почему» ставится только в том случае, если предлагаемый подход не работает. В моём случае, это не очевидно, поэтому мой вопрос именно «зачем».

                    Теперь давайте разберём ваш пример.
                    В начале вы говорите о двух потоках, потом — что сущности В используются в ряде других потоков. Этот момент не совсем понятен.
                    Далее, непонятно проектное решение «A должен «жить» пока живёт хоть один из его «детей» B».
                    То есть я не придираюсь — мне правда непонятно.

                    Из того что мне удалось понять, я бы классы В клал в контейнер внутри А. Сам А при этом выдавал бы объекты В другим по запросу других потоков. О том, как гарантировать существование А в других потоках, вполне можно бы было подумать. А сам А будет гарантировать валидность указателей на В.
                    Если вас беспокоит, что во время использования В, его хозяин А может быть уничтожен, то это, с точки зрения данного подхода, значило бы, что В следует включать в какой-то более общий класс-хозяин.
                    Кстати, централизованное хранение В, может помочь в более простой диспетчеризации, или, например, ведении статистики используемых соединений, то есть сокетов, то есть, простите, В.

                    Судя по всему, речь идёт о реализации веб-сервера. Но зачем такие сложные счётчики ссылок и времени жизни — мне положительно не ясно (особенно учитывая, что мои серверные движки работают без них).
                    Предлагаю идти от задачи (здесь вам придётся чётко поставить задачу на требуемый функционал), после чего я бы на примере разобрал как можно бы было спроектировать систему так, чтобы счётчики не понадобились, то есть применить более простой подход, который представлялся в статье. Вы бы увидели подход в действии, мы могли бы обсудить конкретные плюсы и минусы предложенной реализации. Как вам такое предложение?
                      0
                      Возможно, я не слишком внятно выразился. В вашей постановке задачи уже содержится необходимость в проектных решениях с использованием подсчёта ссылок.
                      Как правило, если спроектировать чуть иначе, то их можно избежать. Поэтому я предлагаю вернуться к изначальной задаче, которую я попытаюсь реализовать немного иначе.
                      • UFO just landed and posted this here
                          0
                          Любой подход не является универсальным подходом «на все случаи жизни». Это очевидно.
                          Я лишь хочу сказать о том, что в данной статье предлагается более простое и внятное решение целого круга задач, не использующее умные указатели со счётчиками.
                          И я утверждаю, что круг задач для применения подхода гораздо выше, чем принято считать. Я лишь предлагаю учиться проектировать более умно, и создавать за счёт этого более «лёгкие» системы.

                          Поскольку я практик, то предложил обсудить конкретные примеры и предложил их более эфффективную реализацию, основанную на подходе из статьи. Как, например, я предложил конкретное, более простое решение, для примера ниже. Там из-за простого обхода динамически изменяемого списка, стали городить много всего, с моей точки зрения, лишнего.

                          Наверняка есть какой-то класс задач, где рефкаунты просто необходимы, где они «ложатся» идеально. Но я утверждаю, что класс этих задач у'же, чем принято считать. Подозреваю, что по крайней мере некоторые из указанных вами пунктов можно реализовать «легче».

                          Нужно говорить конкретно и обсуждать совершенно конкретные задачи. И я предлагаю это делать здесь, в комментариях. Надеясь, что это кому-то будет полезно.
                          • UFO just landed and posted this here
                    0
                    Ваш третий пример, про «в колбэке дочернее окно удалено» — не совсем понятен. Поясните, пожалуйста.
                    • UFO just landed and posted this here
                        0
                        Почти стал забывать, что такое ад с итераторами и энумераторами.) В U++/NTL c самых ранних версий придумали как вернуться к простым целочисленным индексам без потери функциональности и производительности. (Разумеется, это неприкрытая реклама.)

                        Что касается вашего примера.
                        Судя по всему, мы мыслим несколько разными шаблонами проектирования. То есть, мне сложно представить, что именно в реальной жизни решает тот пример, упрощённую реализацию которого вы привели.
                        С моей точки зрения, «нормальным» аналогом вашего примера будет вот такой код:
                        #include <Core/Core.h>
                        class window
                        {
                        public:
                        	window()          :parent(NULL) {}
                        	window(window *p) :parent(p)    {}
                        	
                        	window * create_window()
                        	{
                        		return & windows.Add();
                        	}
                        	
                        	bool delete_window (window *w)
                        	{
                        		for (int i=0; i<windows.GetCount(); ++i)
                        			if (&windows[i] == w)
                        			{
                        				windows.Remove(i);
                        				return true;
                        			}
                        		
                        		return false;
                        	}
                        	
                        	void destroy()
                        	{
                        		windows.Clear();
                        	}
                        	
                        private:
                        	window        *parent;
                        	Array<window>  windows; //контейнер с авто-очисткой
                        };
                        
                        int _tmain(int argc, _TCHAR* argv[])
                        {
                        	window root;
                        	root.create_window();
                        	root.create_window();
                        	
                        	root.destroy();
                        	//в этом случае мало что изменит, потому что окна
                        	//внутри контейнера всё равно будут удалены в
                        	//деструкторе root::windows
                        	//в этом случае root был и остался абсолютно валидным
                        }
                        

                        Но, видимо, это не совсем то, что вы хотели мне продемонстрировать.
                        • UFO just landed and posted this here
                            0
                            Спасибо, теперь понятно. То есть, понятна данная конкретная проблема, но непонятно зачем писать так, чтобы данная проблема появлялась.

                            Если это проблема чисто с итератором — к дискуссии это не имеет отношения. Даже с STL контейнерами можно написать так, что это будет работать. Конечно, это будет не так элегантно, как for_each(). Кроме того, у меня такой проблемы бы не было в принципе, поскольку U++ контейнеры имеют простые целочисленные индексы (хотя итераторы тоже есть).
                            Да и наконец, можно сделать колбэк, возвращающий bool == true в случае, если объект был удалён. Я конечно могу ошибаться, но думаю (навскидку), что схема с простыми контейнером и чуть изменённым колбэком, имеет накладных расходов гораздо меньше, чем схема с контейнером и атомарным подсчётом ссылок в элементах. Не говоря уже о подводных камнях shared_ptr.
                            Если есть проблема оповещать родителя о своём удалении в деструкторе окна, то она также имеет очевидное решение: window::~window() {if (parent) parent->OnDeleted(this);}

                            Либо у вас есть ещё какой-то функционал, которого нет в примере, но который делает вашу схему очень привлекательной.
                            • UFO just landed and posted this here
                                0
                                Ну, кто я вообще такой, чтобы говорить о том как надо. )
                                Собственно, и про методику эту я изначально написал, что она, наверняка, не на все случаи жизни. Дело лишь в том, что, с моей точки зрения, её правильное применение упрощает код, отладку и поддержку.

                                Соответственно, всё что я тут пишу, является не более чем частным мнением, которое, в общем-то, не претендует на какую-либо глобальность. Жаль только, что заметка не вызвала того интереса, который я ожидал — всё-таки, возможность привести программу к более простым и таким же эффективным решениям, это очень неплохая возможность. Для любого более или менее понимающего профессионала. Так мне кажется.

                                Так вот, с моей точки зрения, обход списка с его одновременной модификацией — вещь, о необходимости которой я бы сильно задумался. Особенно в тех случаях, которые приводят к такому глобальному оверхеду.
                                Предположим, что необходимость пройти по изменяющемуся списку всё-таки есть. Что я бы тут посоветовал… да хотя бы классический двусвязный список. В итерации прохода, сначала сохраняем указатели на текущий, предыдущий и следующий элементы. Выполняем функцию для текущего элемента. В ходе работы функции, можно уведомить родителя об уничтожении текущего объекта или добавлении новых — так, чтобы список остался валидным (как правило, никакой операции здесь производиться не будет, то есть оверхед минимальный). Теперь возвращаемся из функции в проход по списку.
                                Сравниваем указатель на текущий элемент и предыдущий элемент у сохранённого указателя на следующий элемент. Если они совпадают, просто сдвигаемся вперёд. Если нет — значит делаем текущий элемент равным предыдущему элементу сохранённого указателя на следующий элемент и двигаемся назад до тех пор, пока предыдущий указатель текущего элемента не будет равен сохранённому предыдущему элементу (это для случая если добавлено несколько элементов сразу; если такого быть не может — возвраты вообще не нужны). Собственно, вот и весь обход. Всего несколько строчек и минимальный оверхед.
                                Разумеется, если подумать подольше, можно найти более оптимальное решение. По крайней мере, более быстрое и внятное в целом по программе, чем обёртки с подсчётом ссылок.
                                • UFO just landed and posted this here
                  • UFO just landed and posted this here
                      0
                      Всё-таки, не стоит категорично утверждать, не разобравшись в предмете.

                      Но за комментарий спасибо: он выявил, что определённая путаница происходит и по моей вине тоже. То, что в стандарте C++0x называется move, я (в стандартах фреймворка) называю pick_. Moveable — это другое. Поскольку STL давно не пользуюсь, «проморгал» этот кусок в новом стандарте.
                      В этом смысле, спасибо за комментарий, буду думать как убрать путаницу в тексте.

                      На самом деле, упомянутый в конце статьи вариант именований мне кажется более обоснованным.
                      pick — это передача содержимого за счёт передачи внутренних указателей.

                      Теперь о moveable. Представим себе реализацию вектора, где объекты идут в обычном динамическом массиве. Чтобы добавить элемент, или вставить элемент в вектор, требуется создать массив большего размера, после чего провести копирование элементов. В крайнем случае, перенос элементов.
                      Но можно сделать по-другому. На самом деле, многие классы являются «простыми», в том смысле, что при обычном побайтовом копировании, не происходит ничего плохого. Это можно рассматривать как грязный хак, но он поддерживается всеми основными компиляторами самых разных версий, он прекрасно работает, и он позволяет копировать группы объектов, не вызывая даже inplace конструкторов. Да, это накладывает определённые ограничения на сам класс, но, поверьте, оно того стоит. Так вот, если фреймворку удаётся основные классы (включая контейнеры и строки) сделать ещё и moveable, это даёт серьёзный прирост скорости при управлении контейнерами.

                      Если мы говорим о производительности, стоит также упомянуть стратегию расширения контейнеров, более «умную» реализацию строкового класса, и ещё несколько решений.
                      • UFO just landed and posted this here
                          –3
                          Практика показывает, что memcpy можно использовать не только для POD. В этом всё и дело.
                            +1
                            обычно peek это операция получения элемента без его удаления из контейнера
                        +1
                        Такой подход очень хорош.

                        Но только до момента, когда у нас появляются несколько нитей.
                          0
                          Почему? Для многопоточной среды есть решения, не противоречащие этой методике. Об одном из них я собираюсь рассказать в следующих заметках.
                            +1
                            Потому, что это всё работает, с таком сценарии: нить создаётся, ей передаются ссылки/указатели на данные «прародителя». Затем нить завершается, «прародитель» также ждёт завершения нити, а потом можёт удалять свои данные.

                            Проблема в том, что не все задачи хорошо кладутся в такой вариант работы.
                              0
                              Для более сложных сценариев актуальна передача указателей от более живучих (персистентных) объектов. Либо — давайте конкретную задачу, обдумаем.
                                0
                                Пусть будет классический пример — есть Сервис, который из внешнего источника (например, по сети) получает некие данные (пусть будет N мегабайт), которые отдаёт на обработку нити, которую берёт из пула.

                                Вот передача владения входными данными с использованием smart_ptr'а от Сервера к нити — стандартна, минимум действий со стороны программиста, надёжна, защищена от утечек.

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

                                ps: а сам подход — очень правильный. У него, к сожалению, есть своя область применения, за пределами которой его плюсы становятся не такими заметными.
                                  0
                                  Поток при завершении работы может уведомлять класс-хозяин (путём вызова обычной функции-члена), что данные следует удалить. При этом, если нужно не просто удалить объект, а выполнить сопутствующие операции (например, обновить GUI), это всё можно сделать в той же функции. И никаких накладных расходов.
                                  Если я правильно понял ваш пример.
                                    0
                                    Callback, в котором происходит удаление ресурса — это тот же самый ручной free/delete, от которого и хочется избавиться, так ведь? Так как у нас (с таким подходом) сразу возникает вопрос «а как гарантировать, что нить 'не забудет' вызвать этот callback?».
                                      0
                                      Если данные будем хранить в контейнере с авто-удалением (см. статью), то память не утечёт, даже если колбэк не будет вызван.

                                      В случае, если диспетчирование не нужно, я готов согласиться с тем, что следовало бы без фанатизма по отношению к этому подходу, спокойно передать в конструктор потока указатель в обёртке вроде smart_ptr (не shared_ptr!).

                                      К сожалению, в реальной жизни всё оказывается сложнее. Как правило, мы не готовы качать «сколько угодно» больших кусков в память за раз. Имея контейнер, мы всегда можем регулировать, сколько единиц данных и какого размера у нас открыто (и кому-то придётся постоять в очереди — это лучше чем нехватка памяти или дикий своп). Диспетчирование будет очень полезно при отладке: оно упростит отслеживание возможных утечек памяти.

                                      Резюмирую. Ваша реализация через smart_ptr — гораздо легче shared_ptr, что делает его хорошей альтернативой моему подходу в простом случае.
                                      То что предлагаю здесь я, не намного лучше или хуже использования умного указатель без подсчёта ссылок — в общем случае. В реальной жизни, я бы воспользовался любым из них, в зависимости от необходимости диспетчирования.
                                        0
                                        Если данные будем хранить в контейнере с авто-удалением (см. статью), то память не утечёт
                                        Если нить завершилась, а этот кусок данных остался занимать своё место в памяти — это немногим лучше обычного memory leak. Обработал сервер 100 запросов, съел ещё 100*N мегабайт. Потом ещё 100 запросов обработал…, а потом ОС его убьёт.

                                        в обёртке вроде smart_ptr (не shared_ptr!).
                                        Хм… smart_ptr — это общее название всех таких указателей, shared_ptr — это тоже один из smart_ptr'ов. Я не совсем понял, что имелось в виду.
                          0
                          Отличный подход.
                          Сам использую нечто подобное, только я пошёл несколько дальше и отказался от указателей и ссылок, используя вместо этого глобальный индексы. Унаследовав все классы моей иерархии от единого предка, который при создании регистрирует себя на фабрике индексов. Когда нужно обратится к какому-то объекту, то контейнер по индексу возвращает специальный временный объект, с перегруженным оператором ->. Кроме всех вышеперечисленных преимуществ, такая идеология позволяет избежать некорректного поведения при обращении к уже удалённому объекту и очень легко реализовать многопоточность.

                          Only users with full accounts can post comments. Log in, please.