Почему работают std:: shared ptr
Я нашел некоторый код, использующий std:: shared_ptr для выполнения произвольной очистки при завершении работы. Сначала я думал, что этот код не может работать, но затем я попробовал следующее:
#include <memory>
#include <iostream>
#include <vector>
class test {
public:
test() {
std::cout << "Test created" << std::endl;
}
~test() {
std::cout << "Test destroyed" << std::endl;
}
};
int main() {
std::cout << "At begin of main.ncreating std::vector<std::shared_ptr<void>>"
<< std::endl;
std::vector<std::shared_ptr<void>> v;
{
std::cout << "Creating test" << std::endl;
v.push_back( std::shared_ptr<test>( new test() ) );
std::cout << "Leaving scope" << std::endl;
}
std::cout << "Leaving main" << std::endl;
return 0;
}
Эта программа выдает результат:
At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed
У меня есть некоторые идеи о том, почему это может сработать, которые имеют отношение к внутренним функциям std::shared_ptrs, реализованным для G++. Поскольку эти объекты оборачивают внутренний указатель вместе со счетчиком, приведение от std::shared_ptr<test> к std::shared_ptr<void>, вероятно, не мешает вызов деструктора. Верно ли это предположение?
И, конечно же, гораздо более важный вопрос: гарантируется ли эта работа стандартом, или могут ли дальнейшие изменения во внутренних компонентах std:: shared_ptr, другие реализации действительно нарушить этот код?
6 ответов:
Фокус в том, что
std::shared_ptrвыполняет стирание типа. В принципе, когда создается новыйshared_ptr, он будет хранить внутри себя функциюdeleter(которая может быть задана в качестве аргумента конструктору, но если нет, то по умолчанию вызываетсяdelete). Когдаshared_ptrуничтожается, он вызывает эту сохраненную функцию, и та вызоветdeleter.Можно увидеть простой эскиз стирания типа, который упрощается с помощью функции std::и избегает всех отсчетов ссылок и других проблем здесь:
template <typename T> void delete_deleter( void * p ) { delete static_cast<T*>(p); } template <typename T> class my_unique_ptr { std::function< void (void*) > deleter; T * p; template <typename U> my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> ) : p(p), deleter(deleter) {} ~my_unique_ptr() { deleter( p ); } }; int main() { my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double> } // ~my_unique_ptr calls delete_deleter<double>(p)Когда
shared_ptrкопируется (или строится по умолчанию) из другого, делетер передается по кругу, так что при построенииshared_ptr<T>изshared_ptr<U>информация о том, какой деструктор вызывать, также передается по кругу вdeleter.
shared_ptr<T>логически [*] имеет (по крайней мере) два релевантных члена данных:
- указатель на управляемый объект
- указатель на функцию deleter, которая будет использоваться для ее уничтожения.
Функция делетера Вашего
shared_ptr<Test>, учитывая способ ее построения, является нормальной дляTest, которая преобразует указатель вTest*иdeletes.Когда вы толкаете свой
shared_ptr<Test>в векторshared_ptr<void>, оба из них скопированы, хотя первый преобразуется вvoid*.Таким образом, когда элемент вектора уничтожается, забирая с собой последнюю ссылку, он передает указатель на делетер, который уничтожает его правильно.
Это на самом деле немного сложнее, чем это, потому что
shared_ptrможет принимать deleter функтор, а не просто функцию, так что могут даже быть данные для каждого объекта, которые будут храниться, а не просто указатель функции. Но для этого случая нет таких дополнительных данных, достаточно было бы просто хранить указатель на экземпляр функции шаблона с параметром шаблона, который фиксирует тип, через который указатель должен быть удален.[*] логически в том смысле, что он имеет к ним доступ - они могут быть не членами самого shared_ptr, а вместо некоторого узла управления, на который он указывает.
Он работает, потому что использует стирание типов.
В принципе, когда вы строите
Этот функтор по умолчанию принимает в качестве аргумента указатель на тип, который вы используете вshared_ptr, он передает один дополнительный аргумент (который вы действительно можете предоставить, если хотите), который является функтором deleter.shared_ptr, таким образомvoidздесь, приводит его соответствующим образом к статическому типу, который вы использовалиtestздесь, и вызывает деструктор для этого объекта. Любая достаточно развитая наука ощущается как магия, не так ли ?
Конструктор
shared_ptr<T>(Y *p)действительно вызываетshared_ptr<T>(Y *p, D d), гдеd- автоматически сгенерированный делетер для объекта.Когда это происходит, тип объекта
Действительно, спецификации требуют, чтобы для объекта recevingYизвестен, поэтому делетер для этого объектаshared_ptrзнает, какой деструктор вызывать, и эта информация не теряется, когда указатель хранится в вектореshared_ptr<void>.shared_ptr<T>, чтобы принять объектshared_ptr<U>, было верно, что иU*должны быть неявно преобразуется вT*, и это, безусловно, имеет место сT=void, потому что любой указатель может быть преобразован вvoid*неявно. Ничего не сказано о deleter, который будет недействительным, так что действительно спецификации предписывают, что это будет работать правильно.Технически IIRC a
shared_ptr<T>содержит указатель на скрытый объект, который содержит счетчик ссылок и указатель на фактический объект; сохраняя deleter в этой скрытой структуре, можно сделать эту внешне волшебную функцию работа при сохранении размераshared_ptr<T>как обычного указателя (однако разыменование указателя требует двойной косвенностиshared_ptr -> hidden_refcounted_object -> real_object
Я собираюсь ответить на этот вопрос (2 года спустя), используя очень упрощенную реализацию shared_ptr, которую пользователь поймет.
Сначала я перейду к нескольким боковым классам, shared_ptr_base, sp_counted_base sp_counted_impl и checked_deleter, последний из которых является шаблоном.
class sp_counted_base { public: sp_counted_base() : refCount( 1 ) { } virtual ~sp_deleter_base() {}; virtual void destruct() = 0; void incref(); // increases reference count void decref(); // decreases refCount atomically and calls destruct if it hits zero private: long refCount; // in a real implementation use an atomic int }; template< typename T > class sp_counted_impl : public sp_counted_base { public: typedef function< void( T* ) > func_type; void destruct() { func(ptr); // or is it (*func)(ptr); ? delete this; // self-destructs after destroying its pointer } template< typename F > sp_counted_impl( T* t, F f ) : ptr( t ), func( f ) private: T* ptr; func_type func; }; template< typename T > struct checked_deleter { public: template< typename T > operator()( T* t ) { size_t z = sizeof( T ); delete t; } }; class shared_ptr_base { private: sp_counted_base * counter; protected: shared_ptr_base() : counter( 0 ) {} explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {} ~shared_ptr_base() { if( counter ) counter->decref(); } shared_ptr_base( shared_ptr_base const& other ) : counter( other.counter ) { if( counter ) counter->addref(); } shared_ptr_base& operator=( shared_ptr_base& const other ) { shared_ptr_base temp( other ); std::swap( counter, temp.counter ); } // other methods such as reset };Теперь я собираюсь создать две "свободные" функции с именем make_sp_counted_impl, которые будут возвращать указатель на вновь созданный.
template< typename T, typename F > sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func ) { try { return new sp_counted_impl( ptr, func ); } catch( ... ) // in case the new above fails { func( ptr ); // we have to clean up the pointer now and rethrow throw; } } template< typename T > sp_counted_impl<T> * make_sp_counted_impl( T* ptr ) { return make_sp_counted_impl( ptr, checked_deleter<T>() ); }Хорошо, эти две функции являются существенными о том, что произойдет дальше, когда вы создадите shared_ptr с помощью шаблонной функции.
Обратите внимание, что происходит выше, если T является пустым, а U-вашим "тестовым" классом. Он вызовет функцию make_sp_counted_impl () с указателем на U, а не на T. управление уничтожением осуществляется здесь. Класс shared_ptr_base управляет подсчетом ссылок в отношении копирования и присвоения и т. д. Класс shared_ptr сам управляет безопасным для типов использованием перегрузок операторов (->, * прием).template< typename T > class shared_ptr : public shared_ptr_base { public: template < typename U > explicit shared_ptr( U * ptr ) : shared_ptr_base( make_sp_counted_impl( ptr ) ) { } // implement the rest of shared_ptr, e.g. operator*, operator-> };Таким образом, хотя у вас есть shared_ptr для void, под ним вы управляете указателем типа, который вы передали в new. Обратите внимание, что если вы преобразуете указатель в void*, прежде чем поместить его в shared_ptr, он не сможет скомпилироваться на checked_delete, так что вы на самом деле в безопасности там тоже.
Test*неявно преобразуется вvoid*, следовательно,shared_ptr<Test>неявно преобразуется вshared_ptr<void>, из памяти. Это работает, потому чтоshared_ptrпредназначен для управления разрушением во время выполнения,а не во время компиляции, они будут внутренне использовать наследование, чтобы вызвать соответствующий деструктор, как это было во время выделения.
Comments