В примере, рассмотренном в предыдущей статье, мы использовали два новых класса: osg::ref_ptr<> и osg::Node. Класс osg::Node представляет собой базовый элемент графа сцены. Переменная root обозначает корневой узел (ноду) для модели самолета, которая используется для визуализации данных сцены.

Между тем, класс osg::ref_ptr<> представляет собой шаблонный класс, созданный для управления объектом корневой ноды. Это так называемый “умный” указатель, который предоставляет дополнительные возможности для эффективного управления памятью в программе.

Понятие об управлении памятью в OSG

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

Управление памятью является критически важной задачей в OSG и его концепция базируется на двух тезисах:

  1. Выделение памяти: обеспечение выделения нужного для хранения объекта объема памяти.
  2. Освобождение памяти: Возврат выделенной памяти системе, в тот момент когда в ней нет необходимости.

Многие современные языки программирования, такие как C#, Java и Visual Basic используют так называемый сборщик мусора для освобождения выделенной памяти. В основном это реализуется подсчетом объектов, ссылающихся на данную область памяти, и освобождение указанного блока памяти, когда число ссылок становится равно нулю.

Концепция языка C++ Не предусматривает подобного подхода, однако мы можем имитировать её путем использования так называемых “умных” указателей.

Классы ref_ptr<> и Referenced

К счастью в OSG обеспечивается собственный механизм умных указателей на основе класса osg::ref_ptr<>, для реализации автоматической сборки мусора. Для его правильной работы OSG предоставляет ещё один класс osg::Referenced для управления блоками памяти, для которых осуществляется подсчет ссылок на них.

Класс osg::ref_ptr<> предоставляет несколько операторов и методов.

  • get() - публичный метод возвращающий “сырой” указатель, например, при использовании в качестве аргумента шаблона osg::Node данный метод вернет osg::Node*.
  • operator*() - фактически оператор разыменования.
  • operator->() и operator=() - позволяют использовать osg::ref_ptr<> как классический указатель при доступе к методам и свойствам объектов, описываемых данным указателем.
  • operator==(), operator!=() и operator!() - ползоляют выполнять над умными указателями операции сравнения.
  • valid() - публичный метод, возвращающий истину, если управляемый указатель имеет корректное значение (не NULL). Выражение some_ptr.valid() эквивалентно выражению some_ptr != NULL, если some_ptr - умный указатель.
  • release() - публичный метод, полезен, когда требуется возвратить управляемый адрес из функци. Про него будет подробнее рассказано позже.

Класс osg::Referenced является базовым классом для всех элементов графа сцены, таких как ноды, геометрия, состояния рендеринга и другие объекты, размещаемые на сцене. Таким образом, создавая корневой узел сцены, мы косвенно наследуем весь функционал, предоставляемый классом osg::Referenced. Поэтому в нашей программе присутсвует объявление

osg::ref_ptr<osg::Node> root;

Класс osg::Referenced содержит целочисленный счетчик ссылок на выделенный блок памяти. Этот счетчик инициализируется нулем в конструкторе класса. Он увеличивается на единицу, когда создается объект osg::ref_ptr<>. Этот счетчик уменьшается, как только удаляется какая-лиюо ссылка на объект, описываемый данным указателем. Объект автоматически уничтожается, когда на него перестают ссылаться какие-либо умные указатели.

Класс osg::Referenced имеет три основных метода:

  • ref() - публичный метод, увеличивающий на 1 счетчик ссылок.
  • unref() - публичный метода, уменьшающий на 1 счетчик ссылок.
  • referenceCount() - публичный метод, возвращающий текущее значение счетчика ссылок, что бывает полезно при отладке кода.

Эти методы доступный во всех классах, производных от osg::Referenced. Однако, следует помнить о том, что ручное управление счетчиком ссылок может привести к непредсказуемым последствиям, и пользуясь этим следует четко представлять себе что вы делаете.

Сборка мусора: как и почему?

Существуют несколько причин, по которым следует использовать умные указатели и уборку мусора:

  • Минимизация критических ошибок: использование умных указателей позволяет автоматизировать выделение и освобождение памяти. Отсутствуют опасные “сырые” указатели.
  • Эффективное управление памятью: память, выделенная под объект освобождается сразу, как только объект становится не нужен, что ведет к экономному использованию ресурсов системы.
  • Облегчение отладки приложения: имея возможность четко отслеживать число ссылок на объект, мы имеем возможности для разного рода оптимизаций и экспериментов.

Допустим, что граф сцены состоит из корневой ноды и нескольких уровней дочерних узлов. Если корневая нода и все дочерние ноды управляются с использованием класса osg::ref_ptr<>, то приложение может отслеживать только указатель на корневую ноду. Удаление этой ноды приведет к последовательному, автоматическому удалению всех дочерних узлов.

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

Умные указатели настоятельно рекомендуются разработчиками OSG к использованию в проектах, однако есть несколько принципиальных моментов, на которые следует обратить внимание:

  • Экземпляры osg::Referenced и его производных могут быть созданы исключительно на куче. Они не могут быть созаны на стеке как локальные переменные, так как деструкторы этих классов объявлены как proteced. Например
    osg::ref_ptr<osg::Node> node = new osg::Node; // правильно
    osg::Node node; // неправильно
    
  • Можно создавать временные узлы сцены используя и обычные указатели C++, однако такой подход будет небезопасным. Лучше применять умные указатели, гарантирующие корректное управление графом сцены
    osg::Node *tmpNode = new osg::Node; // в принципе, будет работать...
    osg::ref_ptr<osg::Node> node = tmpNode; // но лучше завершить работу с временным указателем таким образом!
    
  • Ни в коем случае не стоит использовать в дереве сцены циклических ссылок, когда узел ссылается сам на себя непосредствеено или косвенно через несколько уровней

В приведенном примере графа сцены нода Child 1.1 ссылается сама на себя, а так же нода Child 2.2 ссылается на ноду Child 1.2. Такого рода ссылки могут привести к неверному расчету количества ссылок и неопределенному поведению программы.

Отслеживание управляемых объектов

Для иллюстрации работы механизма умных указателей в OSG напишем следующий синтетический пример

main.h

#ifndef     MAIN_H
#define     MAIN_H

#include    <osg/ref_ptr>
#include    <osg/Referenced>
#include    <iostream>

#endif // MAIN_H

main.cpp

#include    "main.h"

class MonitoringTarget : public osg::Referenced
{
public:

    MonitoringTarget(int id) : _id(id)
    {
        std::cout << "Constructing target " << _id << std::endl;
    }

protected:

    virtual ~MonitoringTarget()
    {
        std::cout << "Dsetroying target " << _id << std::endl;
    }

    int _id;
};

int main(int argc, char *argv[])
{
    (void) argc;
    (void) argv;

    osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0);
    std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl;
    osg::ref_ptr<MonitoringTarget> anotherTarget = target;
    std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl;

    return 0;
}

Создаем класс-наследник osg::Referenced, не делающий ничего, кроме как в конструкторе и деструкторе сообщающий о том, что создан его экземпляр и выводящий идентификатор, определяемый при создании экземпляра. Создаем экземпляр класса с использованием механизма умных указателей

osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(0);

Далее выводим счетчик ссылок на объект target

std::cout << "Referenced count before referring: " << target->referenceCount() << std::endl;

После этого создаем новый умный указатель, присваивая ему значение предыдущего указателя

osg::ref_ptr<MonitoringTarget> anotherTarget = target;

и снова выводим счетчик ссылок

std::cout << "Referenced count after referring: " << target->referenceCount() << std::endl;

Посмотрим, что у нас получилось, проанализировав вывод программы

15:42:39: Отладка запущена
Constructing target 0
Referenced count before referring: 1
Referenced count after referring: 2
Dsetroying target 0
15:42:42: Отладка завершена

При запуске конструктора класса выводится соответствующее сообщение, говорящее нам о том, что память под объект выделена и конструктор нормально отработал. Далее, после создания умного указателя, мы видим, что счетчик ссылок на созданный объект увеличился на единицу. Создание нового указателя, с присвоением ему значения старого указателя - по сути создание новой ссылки на тот же самый объект, поэтому счетчик ссылок увеличивается ещё на единицу. При выходе из программы вызвается деструктор класса MonitoringTarget.

Проведем ещё один эксперимент, дописав в конец функции main() такой код

for (int i = 1; i < 5; i++)
{
	osg::ref_ptr<MonitoringTarget> subTarget = new MonitoringTarget(i);
}

приводящий к такому “выхлопу” программы

16:04:30: Отладка запущена
Constructing target 0
Referenced count before referring: 1
Referenced count after referring: 2
Constructing target 1
Dsetroying target 1
Constructing target 2
Dsetroying target 2
Constructing target 3
Dsetroying target 3
Constructing target 4
Dsetroying target 4
Dsetroying target 0
16:04:32: Отладка завершена

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

С автоматическим освобождением памяти связана дргуая важная особенность работы с умными указателями. Так как деструктор классов производных от osg::Referenced выполнен защищенным, мы не модем явно вызвать оператор delete для удаления объекта. Единственный способ удалить объект - обнулить количество ссылок на него. Но тогда наш код становится небезопасным при многпоточной обработке данных - мы можем обращаться к уже удаленному объекту из другого потока.

К счастью OSG Обеспечивает решение этой проблемы средствами своего планировщика удаления объектов. Этот планировщик основан на использовании класса osg::DeleteHandler. Он работает так, что не выполняет операцию удаления объекта сразу, а выполняет её через некоторое время. Все объекты, подлежащие удалению, временно запоминаются, пока не наступит момент для из безопасного удаления, и тогда они все разом удаляются. Планировщик удаления osg::DeleteHandler управляется бэкэндом рендера OSG.

Возврат из функции

Добавим в код нашего примера следующую функцию

MonitoringTarget *createMonitoringTarget(int id)
{
    osg::ref_ptr<MonitoringTarget> target = new MonitoringTarget(id);

    return target.release();
}

и заменим вызов оператора new в цикле на вызов этой функции

for (int i = 1; i < 5; i++)
{
	osg::ref_ptr<MonitoringTarget> subTarget = createMonitoringTarget(i);
}

Вызов release() уменьшит количество ссылок на объект до нуля, но вместо удаления памяти возвращает напрямую фактический указатель на выделенную память. Если этот указатель присваивается другому умному указателю, то утечек памяти не будет.