Общие указатели пустоты. Почему это работает?

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

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

std::shared_ptr<Root>

Однако:

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

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

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

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

Ответ 1

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

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

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

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

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

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

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

Ответ 2

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

Вы не можете удалить объект с помощью raw 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 запоминает, какой тип объекта он удерживает, и он выполняет приведение к этому типу (или делает что-то эквивалентное этому), когда приходит время для удаления объекта.