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

22

Digamos que tengo un shared_ptrcon un asignador personalizado y un eliminador personalizado.

No puedo encontrar nada en el estándar que habla sobre dónde se debe almacenar 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?

Carreras de ligereza en órbita
fuente

Respuestas:

11

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 deben 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. Basado en el estándar actual y en los informes de defectos relevantes, la asignación no es obligatoria sino asumida por el comité.

  • Aunque la interfaz de shared_ptrpermite a una aplicación donde nunca hay un bloque de control y todo shared_ptry weak_ptrse ponen en una lista enlazada, no hay tal aplicación en la práctica. Además, la redacción se ha modificado suponiendo, por ejemplo, que use_countse comparte.

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

Uno puede imaginar una implementación que coloca al eliminador en un diseño especial shared_ptry lo mueve cuando se shared_ptrelimina el especial . Si bien la implementación parece conforme, también es extraño, especialmente porque puede ser necesario un bloque de control para el recuento de uso (quizás sea posible pero incluso más extraño hacer lo mismo con el recuento 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 obligan de alguna manera), 2802 (que requiere que el eliminador solo se mueva de manera constructiva y, por lo tanto, impide la implementación donde El eliminador se copia entre varios shared_ptr.

Un programador
fuente
2
"para asignar memoria para uso interno" ¿Qué sucede si la implementación no va a asignar memoria para uso interno para empezar? Puede usar un miembro.
LF
1
@LF No puede, la interfaz no lo permite.
Programador
Teóricamente, todavía puede usar algún tipo de "pequeña optimización de eliminación", ¿verdad?
LF
Lo extraño es que no puedo encontrar nada sobre el uso del mismo asignador (copia de a) para desasignar esa memoria. Lo que implicaría algún almacenamiento de esa copia de a. No hay información al respecto en [util.smartptr.shared.dest].
Daniel Langr el
1
@DanielsaysreinstateMonica, me pregunto si en util.smartptr.shared / 1: "La plantilla de clase shared_ptr almacena un puntero, generalmente obtenido a través de new. Shared_ptr implementa semántica de propiedad compartida; el último propietario restante del puntero es responsable de destruir el objeto, o liberando los recursos asociados con el puntero almacenado ". la liberación de los recursos asociados con el puntero almacenado no está destinada para eso. Pero el bloque de control también debería sobrevivir hasta que se elimine el último puntero débil.
Programador
4

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 un 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 deletercon su Alloc.

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

1 Los requisitos comunes que se aplican a todos make_shared, allocate_shared, make_shared_default_init, y allocate_shared_default_initsobrecargas, 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. —Nota final]

[El énfasis es todo mío]

Entonces, el estándar dice que std::allocate_shared debería usarse Allocpara el bloque de control.

Paul Evans
fuente
1
Lo siento por cppreference no es un texto normativo. Es un gran recurso, pero no necesariamente para preguntas de abogados de idiomas .
StoryTeller - Unslander Monica
@ StoryTeller-UnslanderMonica Totalmente de acuerdo: revisé el último estándar y no pude encontrar nada, así que fui con cppreference.
Paul Evans el
@PaulEvans, eel.is/c++draft/util.smartptr.shared.create
Programador del
n4810Respuesta encontrada y actualizada.
Paul Evans el
1
Sin embargo, esto está hablando make_shared, no los constructores en sí. Aún así, puedo usar un miembro para pequeños borradores.
LF
3

Creo que esto no está especificado.

Aquí está la especificación de 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 shared_­ptrobjeto que posee el objeto py el eliminador d. Cuando Tno es un tipo de matriz, los constructores primero y segundo se habilitan shared_­from_­thiscon p. Los constructores segundo y cuarto deberán usar una copia de apara asignar memoria para uso interno . Si se lanza una excepción, d(p)se llama.

Ahora, mi interpretación es que cuando la implementación necesita memoria para uso interno, lo hace mediante el uso 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, std::move(__d));
        else
            // use 'a' to allocate storage for the deleter
    }
// ...
};

¿Esta implementación "utiliza una copia de apara asignar memoria para uso interno"? Si lo hace. Nunca asigna memoria excepto mediante el uso 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_ptrse 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 prueba por sí sola que no pueda existir teóricamente. No estoy diciendo que tal implementación se pueda encontrar realmente en el mundo real, solo que el estándar no parece estar prohibiéndola activamente.

LF
fuente
IMO your shared_ptrpara tipos pequeños asigna memoria en la pila. Y así no cumple con los requisitos estándar
bartop
1
@bartop No "asigna" ninguna memoria en la pila. _Smaller_deleter es incondicionalmente una parte de la representación de un shared_ptr. Llamar a un constructor en este espacio no significa asignar nada. De lo contrario, incluso mantener un puntero en el bloque de control cuenta como "asignar memoria", ¿verdad? :-)
LF
Pero no se requiere que el borrador sea copiable, entonces, ¿cómo funcionaría esto?
Nicol Bolas
@NicolBolas Umm ... Use std::move(__d), y recurra a allocatecuando se requiere copia.
LF