c++ language-lawyer c++17 shared-ptr allocator

c++ - ¿El eliminador de shared_ptr está almacenado en la memoria asignada por el asignador personalizado?



language-lawyer c++17 (3)

Digamos que tengo un shared_ptr con un asignador personalizado y un eliminador personalizado.

No puedo encontrar nada en el estándar que explique dónde debe almacenarse el eliminador: no dice que el asignador personalizado se usará para la memoria del eliminador, y no dice que no lo será.

¿Esto no está especificado o solo me falta algo?


Creo que esto no está especificado.

Así es como se especifican los constructores relevantes: [util.smartptr.shared.const]/10

template<class Y, class D> shared_ptr(Y* p, D d); template<class Y, class D, class A> shared_ptr(Y* p, D d, A a); template <class D> shared_ptr(nullptr_t p, D d); template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

Efectos: construye un objeto shared_ptr que posee el objeto p y el eliminador d . Cuando T no es un tipo de matriz, los constructores primero y segundo habilitan shared_from_this con p . Los constructores segundo y cuarto deberán usar una copia de a para asignar memoria para uso interno . Si se produce una excepción, se llama d(p) .

Ahora, mi interpretación es que cuando la implementación necesita memoria para uso interno, lo hace usando a . No significa que la implementación tenga que usar esta memoria para colocar todo. Por ejemplo, supongamos que existe esta implementación extraña:

template <typename T> class shared_ptr : /* ... */ { // ... std::aligned_storage<16> _Small_deleter; // ... public: // ... template <class _D, class _A> shared_ptr(nullptr_t, _D __d, _A __a) // for example : _Allocator_base{__a} { if constexpr (sizeof(_D) <= 16) _Construct_at(&_Small_deleter, __d); else // use ''a'' to allocate storage for the deleter } };

¿Esta implementación "utiliza una copia de a para asignar memoria para uso interno"? Sí lo hace. Nunca asigna memoria, excepto mediante el uso de a . Hay muchos problemas con esta implementación ingenua, pero digamos que cambia al uso de asignadores en todos los casos, excepto en el caso más simple, en el que shared_ptr se construye directamente desde un puntero y nunca se copia, mueve o hace referencia de otro modo y no hay otras complicaciones. El punto es que el hecho de que no podamos imaginar una implementación válida no significa que pueda existir teóricamente. No estoy diciendo que tal implementación realmente se pueda encontrar, solo que el estándar no parece estar activamente prohibiéndola.


Desde std::shared_ptr tenemos:

El bloque de control es un objeto asignado dinámicamente que contiene:

  • ya sea un puntero al objeto administrado o el objeto administrado en sí mismo;
  • el borrador (tipo borrado);
  • el asignador (tipo borrado);
  • el número de shared_ptrs que poseen el objeto administrado;
  • El número de puntos débiles que hacen referencia al objeto gestionado.

Y de std::allocate_shared obtenemos:

template< class T, class Alloc, class... Args > shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );

Construye un objeto de tipo T y lo envuelve en std :: shared_ptr [...] para usar una asignación tanto para el bloque de control del puntero compartido como para el objeto T.

Por lo tanto, parece que std::allocate_shared debería asignar el deleter con su Alloc .

EDITAR: Y desde n4810 §20.11.3.6 Creación [util.smartptr.shared.create]

1 Los requisitos comunes que se aplican a todas las make_shared , allocate_shared , make_shared_default_init y allocate_shared_default_init , a menos que se especifique lo contrario, se describen a continuación.

[...]

7 Observaciones: (7.1) - Las implementaciones no deben realizar más de una asignación de memoria. [Nota: Esto proporciona una eficiencia equivalente a un puntero inteligente intrusivo. —Final nota]

[El énfasis es todo mío]

Entonces, el estándar dice que std::allocate_shared debería usar Alloc para el bloque de control.


util.smartptr.shared.const / 9 en C ++ 11:

Efectos: construye un objeto shared_ptr que posee el objeto p y el eliminador d. Los constructores segundo y cuarto deberán usar una copia de a para asignar memoria para uso interno.

El segundo y cuarto constructores tienen estos prototipos:

template<class Y, class D, class A> shared_ptr(Y* p, D d, A a); template<class D, class A> shared_ptr(nullptr_t p, D d, A a);

En el último borrador, util.smartptr.shared.const / 10 es equivalente para nuestro propósito:

Efectos: construye un objeto shared_ptr que posee el objeto p y el eliminador d. Cuando T no es un tipo de matriz, los constructores primero y segundo habilitan shared_from_this con p. Los constructores segundo y cuarto deberán usar una copia de a para asignar memoria para uso interno. Si se produce una excepción, se llama d (p).

Por lo tanto, el asignador se usa si es necesario asignarlo en la memoria asignada. Mirando el estado actual de la norma y los informes de defectos relevantes, eso no es obligatorio pero parece asumido por el comité.

  • aunque la interfaz de shared_ptr se diseñó para permitir una implementación donde nunca hay un bloque de control, pero todos shared_ptr y weak_ptr se colocaron en una lista vinculada, en la práctica no existe dicha implementación y la redacción se modificó suponiendo, por ejemplo, que use_count es algo compartido

  • se requiere que el eliminador solo se mueva de forma constructiva. Por lo tanto, no es posible tener varias copias en shared_ptr .

Sería posible imaginar una implementación que coloque al eliminador en un shared_ptr diseñado especial y lo mueva cuando se shared_ptr el shared_ptr especial parece conforme, pero sería realmente extraño, especialmente porque probablemente se necesita un bloque de control para el conteo de uso ( quizás sea posible, pero aún más extraño hacer lo mismo con el conteo de uso).

DR relevantes que encontré: 545 , 575 , 2434 (que reconocen que todas las implementaciones están usando un bloque de control y parecen implicar que las restricciones de subprocesos múltiples lo exigen de alguna manera), 2802 (que hace que el requisito sobre el eliminador solo se mueva de manera constructiva y, por lo tanto, evite implementación donde el eliminador se copia entre varios shared_ptr ).