Общие пустые указатели.Почему это работает? - PullRequest
20 голосов
/ 26 января 2011

Чтобы решить очень специфическую проблему в моем приложении, мне нужен разделяемый указатель на распределенные данные, но для внешнего мира базовый тип данных должен оставаться скрытым.

Я мог бы решить эту проблему, создав некоторый класс Root, который наследуют все мои другие классы, и использовать shared_ptr для этого класса Root, например:

std::shared_ptr<Root>

Тем не менее:

  • Я не хочу, чтобы все мои классы наследовали от этого корневого класса, просто чтобы иметь этот общий указатель
  • Иногда я хочу вернуть общий указатель на std :: vector, или std :: list, или std :: set, ... которые явно не наследуются от моего корневого класса

Как ни странно, кажется, что вы можете создать shared_ptr для void, и это, кажется, работает правильно, как показано в этом примере:

class X
   {
   public:
      X() {std::cout << "X::ctor" << std::endl;}
      virtual ~X() {std::cout << "X::dtor" << std::endl;}
   };

typedef std::shared_ptr<void> SharedVoidPointer;

int main()
{
X *x = new X();
SharedVoidPointer sp1(x);
}

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

Конечно, это решает мою проблему, поскольку теперь я могу возвращать данные с помощью элемента данных SharedVoidPointer и быть уверенным, что они правильно очищены в том месте, где должны быть.

Но гарантированно ли это работает во всех случаях? Это явно работает в Visual Studio 2010, но правильно ли это работает и на других компиляторах? На других платформах?

Ответы [ 2 ]

27 голосов
/ 26 января 2011

Используемый вами конструктор shared_ptr на самом деле является шаблоном конструктора, который выглядит следующим образом:

template <typename U>
shared_ptr(U* p) { }

Он знает внутри конструктора, каков действительный тип указателя (X), и используетэта информация для создания функтора, который может правильно delete указывать и гарантировать, что вызывается правильный деструктор.Этот функтор (называемый «deleteter» shared_ptr) обычно хранится вместе со счетчиками ссылок, которые используются для совместного владения объектом.

Обратите внимание, что это работает, только если вы передаете указатель правильного типаконструктору shared_ptr.Если бы вы вместо этого сказали:

SharedVoidPointer sp1(static_cast<void*>(x));

, тогда это не сработало бы, потому что в шаблоне конструктора U будет void, а не X.Тогда поведение было бы неопределенным, потому что вам не разрешено вызывать delete с указателем void.

В общем, вы в безопасности, если всегда вызываете new при построении shared_ptr и не отделяйте создание объекта (new) от передачи права собственности на объект (создание shared_ptr).

10 голосов
/ 20 февраля 2011

Я думаю, что неявный вопрос заключался в том, что вы не можете удалить с помощью void*, поэтому кажется странным, что вы можете удалить с помощью shared_ptr<void>.

Вы не можете удалить объект с помощью необработанного void* главным образом потому, что он не знает, какой деструктор вызвать. Использование виртуального деструктора не помогает, потому что void не имеет vtable (и, следовательно, виртуального деструктора).

Джеймс МакНеллис четко объяснил, почему shared_ptr<void> работает, но здесь есть кое-что интересное: Предполагая, что вы следуете документированной лучшей практике , чтобы всегда использовать следующую форму при вызове new ...

shared_ptr<T> p(new Y);

... при использовании shared_ptr необязательно иметь виртуальный деструктор. Это верно, если T равно void, или в более знакомом случае, когда T представляет собой полиморфную основу Y .

Это идет вразрез с давним общепринятым мнением: Что интерфейсные классы ДОЛЖНЫ иметь виртуальных деструкторов.

Проблема OP delete (void*) решается тем фактом, что конструктор shared_ptr является шаблоном, который запоминает тип данных, который требуется для уничтожения . Этот механизм решает проблему виртуального деструктора точно таким же образом.

Таким образом, даже если фактический тип объекта не обязательно фиксируется в типе самого shared_ptr (поскольку T не обязательно должен быть того же типа, что и Y ), тем не менее, shared_ptr запоминает, какой тип объекта он держит, и выполняет приведение к этому типу (или делает что-то эквивалентное этому), когда приходит время удалить объект.

...