Прежде чем начать рассмотрение нововведений, давайте вспомним,что было в распоряжении программиста в стандарте C++03 касательно указателей. А было там не густо: “голые” указатели и один “умный” – std::auto_ptr. Т.к. std::auto_ptr был изначально спроектирован специфично(мягко говоря), очень быстро пришло осознание того, что в C++ должны прийти настоящие умные указатели. Они были доступны в компиляторах, которые поддерживали дополнение стандарта TR1 и конечно же в библиотеке boost. С настоящего момента, умные указатели являются официальной частью STL и могут(я бы сказал должны) использоваться любым программистом C++, для повышения качества кода. Умные указатели не просто привносят удобство работы с памятью, но и совершенно меняют парадигму программирования на C++, ставя жирную точку в споре о возвращаемых значениях и исключениях. Давайте рассмотрим, что же они представляют из себя и начнем мы с самого простого класса:
std::unique_ptr
std::unique_ptr является доработанной версией устаревшего std::auto_ptr, точнее идеи, которая стояла за оным.
std::auto_ptr помечен как deprecated в стандарте, а это значит вы должны прекратить его использовать, если использовали когда-либо
Из самого имени класса ясно, что он предназначен для эксклюзивного хранения указателя, т.е. в этом он повторяет поведение своего предтечи. Различия в них, на первый взгляд не очень значительные, – весьма существенны на деле. Объект класса unique_ptr является перемещаемым, но не копируемым, что в терминах С++ означает, что у него определен оператор перемещения(operator=(T&&)) и конструктор перемещения, а оператор копирования и конструктор копирования отсутствуют. После перемещения(равно как и при создании посредством конструктора по умолчанию) unique_ptr содержит в себе nullptr. Подобное поведение делает его тем, чем не смог стать auto_ptr со своей “кривой” семантикой и неказистым именем:
#include <memory>
#include <cassert>
int main()
{
std::unique_ptr<int> spPtr(new int);
//auto spAnotherPtr = spPtr; Ошибка - копирование запрещено
auto spAnotherPtr = std::move(spPtr);
assert(std::unique_ptr<int>() == nullptr);
assert(spPtr == std::unique_ptr<int>());
}
Еще одним отличием является возможность хранения указателя на массив объектов. Для этого при создании объекта unique_ptr необходимо использовать особый синтаксис: std::unique_ptr<T[]>. При создании unique_ptr, для хранения массива, теряется смысл в операторах “–>” и “.”, поэтому они исключены в соответствующей специализации шаблона. Зато добавлен оператор “[]”. Благодаря этой специализации мы получаем полноценный умный контейнер для хранения указателя на массив, с полным сохранением семантики массива; этакий аналог boost::scoped_array. Правда я не очень понимаю смысл сей специализации при ныне здравствующем std::vector; не иначе как лобби “оптимизаторов”
#include <memory>
#include <iostream>
class A
{
public:
~A()
{
std::cout << "Dtor\n";
}
};
int main()
{
{
std::unique_ptr<A> spPtr(new A());
std::unique_ptr<A[]> spArray(new A[10]);
}
}
Выполнив этот код вы увидите 11 последовательно выведенных слов “Dtor”
Продолжая тему операции удаления следует упомянуть еще одно важное отличие от auto_ptr: unique_ptr позволяет задать пользовательскую операцию удаления, что дает возможность работать не только с указателями, память под которые выделена с помощью new, но и с любыми другими.
#include <windows.h>
#include <memory>
#include <functional>
int main()
{
HANDLE File = ::CreateFile(L"test.txt",
GENERIC_WRITE,
FILE_SHARE_READ,
NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
{
std::unique_ptr<void, decltype(std::ptr_fun(&::CloseHandle))>
spHandle(File, std::ptr_fun(&::CloseHandle));
}
}
Выглядит ужасно, не правда ли? Тем не менее концепция ясна, я думаю. Дальше вы увидите, что это можно реализовать гораздо красивее в другом умном указателе.
Резюме:
unique_ptr является отличной заменой auto_ptr и должен быть использован везде, где указатель подразумевает эксклюзивное хранение. К примеру, если указатель является членом класса, то он является отличным кандидатом для хранения его в unique_ptr.
std::shared_ptr
Если unique_ptr стал преемником auto_ptr, то shared_ptr не имеет предтечи в прошлом стандарте. Полагаю, что читатель уже догадался из названия, что этот тип указателя, в противовес unique_ptr, является разделяемым. Это значит, что он может быть скопирован и каждая его копия будет обращаться к одному и тому же указателю. При разрушении объекта shared_ptr ресурс, хранящийся в нем, будет освобожден тогда и только тогда, когда не существует других объектов shared_ptr ссылающихся на этот ресурс. Или другими словами: указатель будет освобожден тогда, когда будет уничтожена последняя копия изначального shared_ptr, если хоть одна было создана. Это реализовано посредством счётчика ссылок.
Это, пожалуй, наиболее плотно используемый тип умного указателя, т.к. на фоне остальных указателей он выглядит наиболее мощным и легко используемым. Действительно, зачем заморачиваться с пониманием unique_ptr и прочего, когда есть shared_ptr? – удобный и эффективный метод организации RAII. Но мы, все же, разработчики C++ и не будем уподобляться тем, кто так считает. Это не верный подход.
Поясню свою точку зрения: shared_ptr обладает большими возможностями, но они не бесплатны, за все придется платить памятью и быстродействием. Также придется платить семантическим диссонансом, который неизбежен при использовании shared_ptr там, где должен быть использован unique_ptr.
Кстати, кое в чем unique_ptr превосходит shared_ptr функционально, а именно: shared_ptr не позволяет, “из коробки”, хранить в себе указатели на массивы, - сомнительное преимущество, но все же…
#include <memory>
#include <cassert>
int main()
{
std::shared_ptr<int> spPtr(new int);
assert(spPtr.unique());
{
auto spAnotherPtr = spPtr;
assert(spPtr == spAnotherPtr);
assert(!spPtr.unique());
assert(spPtr.use_count() == 2);
}
assert(spPtr.unique());
}
Перейдем к возможности задания пользовательской операции удаления в shared_ptr. Сначала приведу пример, а затем обсудим:
#include <windows.h>
#include <memory>
int main()
{
HANDLE File = ::CreateFile(L"test.txt",
GENERIC_WRITE,
FILE_SHARE_READ,
NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
{
std::shared_ptr<void> spHandle(File, &::CloseHandle);
}
}
Для наглядности я использовал тот же пример, что и для unique_ptr. Не находите, что он выглядит несколько опрятнее? Все это происходит благодаря еще одной особенности shared_ptr – этот шаблонный класс зависит только от типа указателя, т.е. тип операции удаления не влияет на инстанциацию шаблона! Это, также, означает, что shared_ptr созданный с одной операцией удаления может быть присвоен другому shared_ptr с другой операцией удаления. Также, все, что описано выше для пользовательской операции удаления справедливо и для пользовательского аллокатора. Это прекрасное свойство shared_ptr, которое делает его более удобным для использования с пользовательскими аллокаторами и операциями удаления. Почему точно также не реализован unique_ptr, спросите вы? Ответ прост – unique_ptr сделан максимально легким с минимальными накладными расходами, shared_ptr, же, сделан больше для удобства и это уже вам выбирать, чем жертвовать.
Применение данного типа умных указателей довольно обширно, и я не берусь перечислять все области. Назову то, что первым приходит в голову:
- все те места, где unique_ptr не может быть использован
- для копирования типов, у которых отсутствует операция копирования. К примеру, когда std::promise нужно передать в другой поток.
- случаи, когда необходим разделяемый доступ к ресурсу
Рассмотрев сам указатель перейдем к рассмотрению различных утилит, которые стандарт предлагает программисту для облегчения работы с shared_ptr
std::make_shared и std::allocate_shared
Обе функции служат одной цели – создание shared_ptr в недрах реализации. allocate_shared отличается лишь тем, что позволяет задать пользовательский аллокатор(сиречь операцию выделения памяти) при создании shared_ptr. Т.к. использование собственного аллокатора является более редким случаем, я буду рассказывать о плюсах на примере make_shared, но это будет в равной степени относиться и к allocate_shared.
#include <memory>
#include <cassert>
class A
{
int m_i;
int m_j;
public:
A(int i, int j): m_i(i), m_j(j)
{}
bool operator==(const A& Rhs)
{
return m_i == Rhs.m_i && m_j == Rhs.m_j;
}
};
int main()
{
std::shared_ptr<A> spFirst(new A(1,2));
auto spSecond = std::make_shared<A>(1,2);
assert(*spFirst == *spSecond);
}
Из вышеприведенного кода видно, что код с make_shared позволяет не использовать в своей записи new и указывать тип указателя лишь один раз. Хотя для этого примера количество упоминаний типа не существенно в примере с длинными именами может дать неплохой выигрыш в длине строки, а, следовательно, и в её читабельности. Помимо чисто эстетического превосходства(в чем вы можете со мной не согласится) make_shared обладает объективным преимуществом, за счет сокрытия использования new в деталях реализации:
foo(std::shared_ptr<std::string>(new std::string("first")),
std::shared_ptr<std::string>(new std::string("second")));//#1
foo(std::make_shared<std::string>("first"),
std::make_shared<std::string>("second"));//#2
Строчка #1 является потенциальной утечкой памяти, тогда как строчка #2 является полностью безопасной.
Помимо элегантности и стойкости перед лицом исключений, make_shared может быть более эффективным как в потреблении памяти, так и в быстродействии. Для лучшего понимания я приведу 2 картинки:
Простое создание, посредством new
|
Посредством make_shared
|
При создании объекта shared_ptr “классическим” методом. требуется 2 шага: выделить память с помощью некоторого аллокатора и создать объект shared_ptr, который получит выделенную память в свою зону ответственности. Для такого создания всегда требуется два выделения памяти: под данные(например, пользователь выделяет посредством new) и под блок счётчика ссылок(всегда выделяется без участия пользователя). Таким образом при создании shared_ptr “классическим” методом происходит два выделения памяти в разных областях кучи, а при уничтожении последнего объекта shared_ptr, ссылающегося на данный блок данных, происходит два освобождения занятой памяти.
С другой стороны, при использовании make_shared, блок счетчика ссылок и данные можно расположить в памяти последовательно, в одном выделенном блоке памяти. Это достигается за счёт того, что создание объекта и выделение памяти под него изолируется от пользователя и происходит внутри функции make_shared.
Не берусь утверждать, что так реализовано в каждом компиляторе, но есть все шансы, что это так. В MSVC 2010 это именно так.
За счёт подобного размещения при создании объекта происходит одно выделение памяти, и одно освобождение памяти происходит при разрушении последнего ссылающегося на этот блок объекта shared_ptr. Не бог весть что, но, все же, лучше чем ничего, а вкупе с остальными плюсами make_shared является безусловным лидером, а следовательно и стандартом де факто при создании shared_ptr.
Правда, есть и один недостаток у make_shared: с помощью этой функции нельзя задать пользовательскую операцию удаления. Поэтому нельзя покрыть все случаи создания shared_ptr при помощи функции make_shared.
std::enable_shared_from_this
Для облегчения работы с shared_ptr, а точнее для решения вполне насущной проблемы, в стандарте представлен шаблонный класс enable_shared_from_this. Для понимания его предназначения я опишу проблему, которую он призван решить.
Предположим, что мы хотим написать класс-реализацию структуры данных которая обладает следующими свойствами:
- Содержит массив указателей на потомков
- Содержит указатель на родителя
- Имеет метод для смены родителя, который удаляет себя из предыдущего родителя и добавляет себя к новому
Звучит все, вроде бы, просто и реализация будет тривиальной для “голых” указателей. Но мы же прониклись идеей умных указателей и не желаем использовать их более простых товарищей! Попробуем реализовать требуемый класс с помощью умных указателей:
#include <memory>
#include <cassert>
#include <vector>
#include <algorithm>
#include <iterator>
class Node
{
private:
typedef std::shared_ptr<Node> Child_t;
typedef std::vector<Child_t> Children_t;
private:
Node* m_pParent;
Children_t m_ChildList;
public:
Node(): m_pParent(nullptr)
{
}
void AddChild(const Child_t& Child)
{
m_ChildList.push_back(Child);
Child->m_pParent = this;
}
void RemoveChild(const Child_t& Child)
{
auto NewEnd = std::remove(std::begin(m_ChildList),
std::end(m_ChildList), Child);
m_ChildList.erase(NewEnd, std::end(m_ChildList));
}
void SetParent(Node* pNewParent)
{
m_pParent->RemoveChild(/*Что тут писатьjQuery15209131074224684714_1327721993545?*/);
pNewParent->AddChild(/*Что тут писать???*/);
}
size_t ChildrenCount() const
{
return m_ChildList.size();
}
Node* Parent() const
{
return m_pParent;
}
};
int main()
{
auto spRoot = std::make_shared<Node>();
auto spChild = std::make_shared<Node>();
spRoot->AddChild(spChild);
assert(spChild->Parent() == spRoot.get());
assert(spRoot->ChildrenCount() == 1);
auto spAnotherRoot = std::make_shared<Node>();
spChild->SetParent(spAnotherRoot.get());
assert(spRoot->ChildrenCount() == 0);
assert(spAnotherRoot->ChildrenCount() == 1);
assert(spChild->Parent() == spAnotherRoot.get());
}
Комментарии в коде говорят сами за себя: как же нам удалить и добавить потомка имея на руках только this, тогда как нам нужен объект shared_ptr? Наивный подход мог бы выглядеть следующим образом:
void SetParent(Node* pNewParent)
{
std::shared_ptr<Node> thisPtr(this);
m_pParent->RemoveChild(thisPtr);//#1
pNewParent->AddChild(thisPtr);//#2
}
На строке #2 программа может упасть. В нашем случае этого не произойдет, т.к. spChild еще жив в функции main. Но в более реальном примере this на данном этапе уже будет удален. Но даже в этом примере программа упадет, хотя и не в этом месте, т.к. мы имеем два не связанных shared_ptr указывающих на один блок данных.
Дабы решить эту проблемы enable_shared_from_this и был придуман. Наследование от этого класса позволяет получать shared_ptr из this. При этом, это всегда будет shared_ptr с одним и тем же блоком счётчика ссылок, а значит мы не получим коллизий, которые получили в “наивном” решении при повторном создании shared_ptr из указателя, который уже находился в зоне ответственности другого shared_ptr.
Внесем необходимые исправления:
class Node: public std::enable_shared_from_this<Node>
и
void SetParent(Node* pNewParent)
{
m_pParent->RemoveChild(shared_from_this());
pNewParent->AddChild(shared_from_this());
}
Задача решена.
К сожалению, в силу ограничений языка мы имеем следующие ограничения при использовании enable_shared_from_this:
- Объекты классов, наследующих enable_shared_from_this и использующие метод shared_from_this, должны быть созданы как shared_ptr изначально.
- Метод shared_from_this не может быть использован в конструкторе класса, наследующего enable_shared_from_this.
Второе ограничение не позволяет реализовать конструктор, который получает родителя в качестве аргумента, который, в свою очередь, добавляет this как потомка:
Node(Node* pParent): m_pParent(pParent)
{
//Так делать нельзя!
m_pParent->AddChild(shared_from_this());
}
dynamic_pointer_cast , static_pointer_cast и const_pointer_cast
Следующими в полку функций-помощников являются методы дублирующие функциоанал базовых операторов dynamic_cast, static_cast и const_cast применяемых к “голым” указателям.
Это методы-обертки для упрощения адаптации к shared_ptr; используйте их при необходимых преобразованиях вместо применения базовых операторов к результату shared_ptr::get() . Их применение тривиально:
std::static_pointer_cast<void>(spChild);
Результатом выражения будет shared_ptr<void> объект.
owner_before
Последним методом, который заслуживает отдельного упоминания, является метод owner_before. Этот метод является альтернативной версией оператора <. Оператор < выполняет сравнение базируясь на указателе на данные(first.get() < second.get()), тогда как owner_before выполняет компиляторо-зависимое сравнение концептуально эквивалентное оператору <. Есть даже специальный функтор std::owner_less, предназначенный как замена стандартному std::less в контейнерах set и map, внутри этого функтора выполняется проверка посредством owner_before. При этом соблюдается правило эквивалентности известное вам по оператору <, - два shared_ptr(first и second) эквивалентны если выполняется следующее условие:
!first.owner_before(second) && !second.owner_before(first)
Поскольку комитет стандартизации не регламентировал то, как эта проверка должна выполнятся, а отдал это на откуп разработчикам компиляторов, то и говорить о реализации нечего. Важно лишь понимать, что установленная эквивалентность при сравнении посредством owner_before может быть не установлена при использовании оператора <:
#include <memory>
#include <cassert>
class A
{
int i;
};
class B
{
int j;
};
class C: public A, public B
{};
struct Z
{
B* m_B;
Z(): m_B(new B)
{}
};
int main()
{
//#1
{
auto spInitial = std::make_shared<C>();
std::shared_ptr<void> spDerived =
std::static_pointer_cast<B>(spInitial);
assert(spInitial != spDerived);
assert(spInitial < spDerived || spInitial > spDerived);
assert(!spInitial.owner_before(spDerived) &&
!spDerived.owner_before(spInitial));
}
//#2
auto spInitial = std::make_shared<Z>();
std::shared_ptr<B> spInner(spInitial, spInitial->m_B);
assert(!spInitial.owner_before(spInner) &&
!spInner.owner_before(spInitial));
}
В части #1, можно наблюдать то, что я описал выше: при сравнении объектов оператором < объекты не являются эквивалентными, т.к. spDerived хранит указатель на B часть указателя C, и поэтому они не являются эквивалентными. Но при сравнении посредством owner_before эквивалентность устанавливается, т.к. по сути оба этих указателя указывают на один блок памяти, только в разные его части, т.е. они эквивалентны по владению. Для упрощения понимания этой концепции можно думать об этом как об эквивалентности блоков счётчиков ссылок, т.е. происходит сравнение не указателей на данные, а указателей на блок счётчика ссылок. Я не утверждаю, что именно так реализовано в вашем компиляторе, но этот способ вполне логичен.
В части #2 затронут интересный конструктор shared_ptr, который позволяет делится владением, не делясь данными. Т.е. блок счётчика ссылок становится общим, а данные могут быть разными. Вплоть до того, что вы можете создать shared_ptr c nullptr и блоком счётчика ссылок из другого shared_ptr.
В части #2 я привел еще более надуманный и высосанный из пальца пример необходимости применения owner_before, а необходимо это т.к. мы имеем два shared_ptr с разными типами данных и оператор < не может быть применен, в то время как owner_before отрабатывает на ура.
Прошу прощения, за несколько сумбурное изложение материала касательно owner_before, а также примеры применения оного. Дело в том, что я не понимаю зачем нужен этот метод и никакого вразумительного объяснения я не нашел. Но и пройти мимо него я не мог. Возможно он добавлен как забота о будущем, не знаю. Не вижу я в нем смысла вне рамок std::weak_ptr, но об этом будет дальше. Теперь вы представляете себе, что с помощью него можно сделать, а как его применять уже решать вам. Буду рад если вы мне сообщите реальные примеры его применения.
boost::intrusive_ptr
Также, в рамках разговора о shared_ptr, я бы хотел упомянуть и boost::intrusive_ptr. Это тоже умный указатель, но он не включен в стандарт C++11. Он схож с std::shared_ptr, в целом, но позволяет задать операцию инкремента и декремента счётчика ссылок самостоятельно. Операция декремента, также, ответственна за освобождения памяти выделенной под объект
Для этого необходимо реализовать две функции: intrusive_ptr_add_ref и intrusive_ptr_release, которые принимают указатель на ваш тип в качестве аргумента. Подобный указатель может быть полезен при работе с COM технологией от Microsoft, к примеру.
Применение данного типа указателя рационально для типов данных, которым нужны пользовательские операции работы со счётчиками. Для всего остального есть shared_ptr.
Возможно, некоторые читатели воскликнули во время прочтения материала: “А что этот апологет умных указателей сделал “голым” указатель на род��теля в классе Node? Почему не shared_ptr?” Дело в том, что если указатель на родителя сделать объектом shared_ptr, тогда получится, что родитель(уже помещенный в shared_ptr) будет содержать потомка в котором содержится shared_ptr содержащий родителя. Но при таком варианте потомок никогда не будет удален, поскольку он сам содержит shared_ptr своего родителя! Эта проблема получила название проблемы перекрестных ссылок и вот еще её разновидности: объект содержит shared_ptr на себя же, или два разных объекта содержат shared_ptr друг на друга. В обоих случаях объекты не будут удалены никогда, т.к. из-за перекрестных ссылок, счетчик никогда не упадет до 0. Проблема перекрестных ссылок легко решаются с применением std::weak_ptr.
На самом деле, в примере с Node даже std::weak_ptr избыточен, т.к. не может быть такой ситуации когда родитель уничтожен, а потомки здравствуют. Именно поэтому там применен “голый” указатель. В другом там просто нет смысла.
std::weak_ptr
Итак, мы знаем в каком случае стоит применять weak_ptr, но не знаем, что это такое. Давайте восполнять пробел: weak_ptr не является самостоятельным умным указателем, а лишь довеском к shared_ptr. Конструируется weak_ptr из существующего shared_ptr и его единственной целью является предоставление пользователю информации об ассоциированным с ним shared_ptr без увеличения счётчика ссылок оного. Из объекта weak_ptr можно получить следующую информацию:
- Является ли ассоциированный shared_ptr “живым” или же счетчик ссылок обнулен и объект не существует более. Для этого в weak_ptr существует метод expitred()
- Можно получить сам объект shared_ptr непосредственно, точнее его копию. Данная операция увеличит счётчик ссылок, осуществляется это вызовом метода lock()
Непосредственно обратиться к shared_ptr, на который ссылается weak_ptr нельзя. Поэтому при необходимости обращения к самому объекту, который хранится в shared_ptr необходимо использовать lock().
Не стоит злоупотреблять функцией lock(), оставляя полученный shared_ptr в течение долгого времени. Следует избавляться от полученного объекта сразу, как в нем пропадает необходимость. Иначе сам принцип заложенный в weak_ptr , будет нарушен.
Таким образом, помимо решения проблемы циклических ссылок, weak_ptr обладает приятным бонусом отслеживания времени жизни объекта на который он ссылается. Используйте weak_ptr везде, где он может заменить shared_ptr,- это уменьшает связность.
#include <memory>
#include <cassert>
int main()
{
auto spShared = std::make_shared<int>();
std::weak_ptr<int> Weak(spShared);
assert(Weak.expired() == false);
assert(Weak.use_count() == 1);
auto spClone = Weak.lock();
assert(Weak.use_count() == 2);
spShared.reset();
assert(Weak.use_count() == 1);
spClone.reset();
assert(Weak.expired());
}
Кстати, для класса weak_ptr, вышеупомянутый, owner_before обретает новые краски. Дело в том, что у weak_ptr отсутствует оператор <, а следовательно его нельзя сравнить никаким образом кроме как при помощи owner_before. Поэтому, для хранения weak_ptr в std::set и std::map необходимо использовать std::owner_less в качестве сравнивающего функтора. Кроме того, с помощью этого метода можно провести сравнение между shared_ptr и weak_ptr! Не знаю кому это может пригодится, но тем не менее.
Итог
Требующая предельного внимания модель работы с памятью в C++ отжила своё. Являясь современным программистом на C++ вы должны понимать, что модель построенная на RAII пришла ей на смену и сопротивление бесполезно и вредно. Нет смысла в использовании “голых” указателей, там где они должны быть заменены своими умными собратьями(обратных случаев не так уж много и связаны они, в основном, со старым кодом).
P.S. Все вышеописанные, указатели могут принимать неполный тип в качестве аргумента шаблона.