Pull to refresh

Секретный конструктор std::shared_ptr

Reading time4 min
Views47K
Original author: Anthony Williams
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, которые разделяют ответственность за удаление исходного указателя (если не очень понятно - далее на примерах будет понятней).

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

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

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


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

struct X{
    Y y;
};

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

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, который будет вместе с shared_ptr держать библиотеку открытой до тех пор, пока shared_ptr не будет уничтожен или сброшен (т.е. вызвана операция reset()).

Детали


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

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

Как обычно при создании shared_ptr указатель 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
Tags:
Hubs:
Total votes 46: ↑46 and ↓0+46
Comments26

Articles