Почему работает std:: shared_ptr <void>
Я нашел некоторый код, используя 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, другие реализации действительно нарушают этот код?
Ответы
Ответ 1
Фокус в том, что std::shared_ptr
выполняет стирание стилей. В принципе, когда создается новый shared_ptr
, он будет хранить внутренне функцию deleter
(которая может быть задана как аргумент конструктору, но если не присутствует по умолчанию для вызова delete
). Когда shared_ptr
уничтожается, он вызывает эту сохраненную функцию и вызывает вызов deleter
.
Простой эскиз стираемого типа, который упрощается с помощью std:: function, и избегая всех подсчетов ссылок и других проблем, можно увидеть здесь:
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)
Когда a shared_ptr
копируется (или по умолчанию сконструирован) из другого, удаляется делегат, так что, когда вы строите shared_ptr<T>
из shared_ptr<U>
, информация о том, какой деструктор для вызова также передается в deleter
.
Ответ 2
shared_ptr<T>
логически [*] имеет (по крайней мере) два соответствующих элемента данных:
- указатель на управляемый объект
- указатель на функцию делетера, которая будет использоваться для ее уничтожения.
Функция deleter вашего shared_ptr<Test>
, учитывая способ ее создания, является нормальной для Test
, которая преобразует указатель в Test*
и delete
it.
Когда вы нажимаете shared_ptr<Test>
в вектор shared_ptr<void>
, оба из них копируются, хотя первый преобразуется в void*
.
Таким образом, когда векторный элемент уничтожается, беря с собой последнюю ссылку, он передает указатель на удаленный сервер, который уничтожает его правильно.
На самом деле это немного сложнее, потому что shared_ptr
может принимать функтор deleter, а не только функцию, поэтому даже данные за объект могут храниться, а не только указатель на функцию. Но для этого случая нет таких дополнительных данных, достаточно было бы просто сохранить указатель на экземпляр функции шаблона с параметром шаблона, который захватывает тип, через который должен быть удален указатель.
[*] логически в том смысле, что он имеет к ним доступ - они могут быть не членами самого shared_ptr, а вместо некоторого управления node, на которое он указывает.
Ответ 3
Это работает, потому что использует стирание типа.
В принципе, когда вы создаете shared_ptr
, он передает один дополнительный аргумент (который вы действительно можете предоставить, если хотите), что является функтором удаления.
Этот функтор по умолчанию принимает в качестве аргумента указатель на тип, который вы используете в shared_ptr
, поэтому void
здесь, соответствующим образом применяет статический тип, который вы использовали test
, и вызывает деструктор на этом объекте.
Любая достаточно развитая наука кажется магией, не так ли?
Ответ 4
Конструктор shared_ptr<T>(Y *p)
действительно, кажется, вызывает shared_ptr<T>(Y *p, D d)
, где d
является автоматически созданным делетером для объекта.
Когда это происходит, тип объекта Y
известен, поэтому удаленный объект для этого объекта shared_ptr
знает, какой деструктор должен вызывать, и эта информация не теряется, когда указатель хранится в векторе shared_ptr<void>
.
В действительности спецификации требуют, чтобы для объекта receving shared_ptr<T>
для принятия объекта shared_ptr<U>
он должен быть истинным, и U*
должен быть неявно конвертируемым в T*
, и это, безусловно, имеет место с T=void
потому что любой указатель может быть преобразован в void*
неявно. Ничего не сказано о дебетере, который будет недействительным, поэтому спецификации требуют, чтобы это работало правильно.
Технически IIRC a shared_ptr<T>
содержит указатель на скрытый объект, содержащий ссылочный счетчик и указатель на фактический объект; путем хранения делетера в этой скрытой структуре, можно сделать эту, по-видимому, магическую функцию, продолжая удерживать shared_ptr<T>
как большую, чем обычный указатель (однако разыменование указателя требует двойной косвенности
shared_ptr -> hidden_refcounted_object -> real_object
Ответ 5
Я собираюсь ответить на этот вопрос (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 через шаблонную функцию.
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->
};
Обратите внимание, что происходит выше, если T является void, а U - вашим классом "тест". Он будет вызывать make_sp_counted_impl() с указателем на U, а не указателем на T. Управление уничтожением выполняется здесь. Класс shared_ptr_base управляет подсчетом ссылок в отношении копирования и назначения и т.д. Сам класс shared_ptr управляет типичным использованием перегрузок операторов (- > , * и т.д.).
Таким образом, хотя у вас есть shared_ptr для void, под вами управляет указатель типа, который вы передали в новый. Обратите внимание: если вы конвертируете свой указатель в void * перед тем, как поместить его в shared_ptr, он не сможет скомпилировать файл checked_delete, чтобы вы действительно были там в безопасности.
Ответ 6
Test*
неявно конвертируется в void*
, поэтому shared_ptr<Test>
неявно конвертируется в shared_ptr<void>
из памяти. Это работает, потому что shared_ptr
предназначен для управления уничтожением во время выполнения, а не во время компиляции, они будут внутренне использовать наследование, чтобы вызвать соответствующий деструктор, как это было во время выделения.