четверг, 10 декабря 2015 г.

Семантика владения в C++

C++ 11

Язык C++ сильно изменился с выходом нового стандарта, C++ 2011. Сменилась генеральная линия партии: теперь разработчикам не рекомендуется вообще использовать new и delete, потому что всё можно сделать проще без них. Вот сразу пример:

Кратко о выделении памяти

В C++ есть два места, где можно размещать переменные — это стек (stack) и динамическая куча (heap).
  • Стек выделяется для каждого потока каждой программы операционной системой. При вызове функции стек растёт, потому что через вершину стека передаются параметры функции и адрес возврата. В начале работы функции стек растёт ещё раз, потому что в нём располагаются локальные переменные. При выходе из функции размер стека уменьшается.
  • Куча не размечена, операционная система даёт программисту возможность выделять или освобождать отдельные куски этой кучи. При этом куча подвержена фрагментации, и нет гарантий, что при общей нехватке памяти в системе запрошенный программой кусок памяти удастся выделить.

Кратко о семантике владения

Пожалуй, самая популярная в наше время парадигма программирования - объектная. В рамках парадигмы любая программа представляется как набор объектов, например:
  • Объект Application, который хранит основной цикл интерактивного приложения
  • Абстрактный объект Shape (т.е. фигура без каких-либо уточнений) и конкретные классы RectangleShape, CircleShape и так далее.
  • Объекты Enemy и EnemyBehavior, хранящие соответственно визуальное представление и поведение врага в игре
Обычно любым объектом (кроме самого главного) кто-то владеет. Иначе говоря, кто-то
  • создаёт объект сразу или по мере необходимости
  • управляет поведением объекта или даёт управлять им кому-то ещё
  • удаляет объект
Отношение владения формирует иерархию объектов. Иерархия хорошо представляется в виде дерева — в дереве легко понять, кто кем владеет:
  • корень дерева прямо или косвенно владеет всем, корнем не владеет никто
  • листья дерева никем не владеют 
Кстати говоря, дерево является графом без циклов.

Сильные указатели

Типы std::unique_ptr<T> и std::shared_ptr<T> являются умными указателем, подходящим для древовидной иерархии владения. Советы:
  • Используйте shared_ptr, если объектом владеют несколько родителей. Например, если одна и та же картинка используется в качестве фона нескольких кнопок.
  • Используйте функцию std::make_shared для создания shared_ptr, это лучше в плане читаемости и скорости выполнения кода.
  • Используйте функцию std::make_unique для создания unique_ptr, это лучше в плане читаемости кода.

Слабые указатели

Допустим, что в нашем дереве мы захотим иметь ссылку из дочернего узла на родительский, или даже на соседнюю ветку. Например, хотим в объекте-враге хранить указатель на цель, котоую он сейчас атакует.
Если мы создадим shared_ptr, в графе владения возникнет цикл. 
Циклы удаляются только в средах выполнения со сборкой мусора (именно в этом причина снижения производительности из-за сборки мусора в «современных» языках, таких как Java и C#).
Во избежание создания циклов используйте weak_ptr для ссылания на родителя или другую ветвь. Суть weak_ptr очень проста:
  • он никак не владеет тем, на что указывает, и не меняет время жизни
  • значение указателя внутри weak_ptr обратится в ноль в тот момент, когда объект, на который указывает weak_ptr, уничтожится.
  • weak_ptr тесно связан с shared_ptr, и они взаимно приводятся друг в друга
Подробнее и с примерами про weak_ptr и shared_ptr можно прочитать здесь: http://archive.kalnitsky.org/2011/11/02/smart-pointers-in-cpp11/  

Применения семантики владения

Семантика владения, основанная на unique_ptr, shared_ptr, weak_ptr и полном отсутствии new/delete, позволяет проще и надёжнее писать различные программы:
  • Игры, применяющие паттерн SceneGraph и обрабатывающие физические коллизии между объектами
  • Компиляторы, использующие Abstract Syntax Tree для промежуточного хранения модели кода
  • WYSIWYG-редакторы, загружающие модель документа в память программы и отображающие документ на экране