c++ c++11 language-lawyer shared-ptr

c++ - ¿Por qué los eliminadores de shared_ptr deben ser CopyConstructible?



c++11 language-lawyer (3)

Debido a que shared_ptr está destinado a ser copiado, y cualquiera de esas copias podría tener que eliminar el objeto, entonces todas deben tener acceso a un eliminador. Mantener solo un eliminador requeriría, bueno, volver a contar el propio eliminador. Si realmente quieres que eso suceda, podrías usar un std::shared_ptr como el eliminador, pero eso suena un poco exagerado.

En C ++ 11 std::shared_ptr tiene cuatro constructores a los que se pueden pasar los objetos de eliminación d del tipo D Las firmas de estos constructores son las siguientes:

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);

El estándar requiere en [util.smartptr.shared.const] tipo D que sea CopyConstructible. ¿Por qué es esto necesario? Si shared_ptr hace copias de d , ¿a cuál de estos deleters se le podría llamar? ¿No sería posible que shared_ptr solo mantuviera un solo eliminador alrededor? ¿Qué significa para shared_ptr poseer un eliminador si d se puede copiar?

¿Cuál es la razón de ser del requisito de CopyConstructible?

PD: este requisito podría complicar la escritura de eliminadores para shared_ptr . unique_ptr parece tener requisitos mucho mejores para su eliminador.


Esta pregunta me desconcertaba tanto que boost::shared_ptr correo electrónico a Peter Dimov (implementador de boost::shared_ptr e involucrado en la estandarización de std::shared_ptr )

Aquí está la esencia de lo que dijo (reimpreso con su permiso):

Mi suposición es que el Deleter tenía que ser CopyConstructible realmente solo como una reliquia de C ++ 03 donde la semántica de movimiento no existía.

Tu conjetura es correcta. Cuando se especificó shared_ptr, las referencias de valor aún no existían. Hoy en día deberíamos ser capaces de salir adelante con la exigencia de no mover movimiento-construible.

Hay una sutileza en eso cuando

pi_ = new sp_counted_impl_pd<P, D>(p, d);

throws, d debe dejarse intacto para que la limpieza d(p) funcione, pero creo que esto no sería un problema (aunque en realidad no he intentado que la implementación sea amigable).
[...]
Creo que no habrá ningún problema para que la implementación lo defina de modo que cuando los new lanzamientos, d se dejen en su estado original.

Si vamos más allá y permitimos que D tenga un constructor de movimientos arrojadizos, las cosas se vuelven más complicadas. Pero no lo haremos :-)


La diferencia entre los modificadores en std::shared_ptr y std::unique_ptr es que el shared_ptr shared_ptr está borrado por tipo, mientras que en el tipo de unique_ptr unique_ptr es parte de la plantilla.

Aquí está Stephan T. Lavavej explicando cómo el borrado de tipos conduce al requisito CopyConstructible en std::function .

En cuanto a la razón detrás de esta diferencia en los tipos de punteros, se ha abordado en SO varias veces, por ejemplo here .

Una cita de lo que dijo STL:

Muy sorprendente "gotcha", diría yo, es que la std::function requiere objetos de función CopyConstructible, y esto es algo inusual en el STL.

Usualmente el STL es flojo en el sentido de que no necesita las cosas por adelantado: si tengo algo así como una std::list de tipo T , T no necesita ser menos que comparable; solo si llama a la list<T>::sort funciones miembro list<T>::sort entonces realmente necesita ser menos que comparable.

La regla del lenguaje central que potencia esto es que las definiciones de las funciones miembro de una plantilla de clase no se instancian hasta que realmente se necesitan y los cuerpos no existen en algún sentido hasta que realmente se lo llama.

Esto generalmente es genial, esto significa que solo paga por lo que necesita, pero la std::function es especial debido a la borradura de tipo, porque cuando construye la std::function partir de un objeto llamable F , necesita generar todo lo que pueda necesitar. de ese objeto F porque va a borrar su tipo. Requiere todas las operaciones que posiblemente podría necesitar, independientemente de si se usan.

Por lo tanto, si construye una std::function desde algún objeto llamable F , F es absolutamente necesario en tiempo de compilación para ser CopyConstructible. Esto es cierto incluso aunque F se mueva a la std::function , incluso si le da un valor r e incluso si nunca copia las std::function en cualquier lugar de su programa, F todavía debe ser CopyConstructible.

Obtendrá un error de compilación al decirlo, tal vez horrible, tal vez agradable, dependiendo de lo que obtenga.

Simplemente no puede almacenar objetos de función solo móviles. Esta es una limitación de diseño causada en parte por el hecho de que std::function se remonta a boost / TR1, antes de las referencias de valor r, y en cierto sentido nunca se puede arreglar con la interfaz de std::function tal como está.

Se están investigando alternativas, tal vez podamos tener una "función móvil" diferente, por lo que probablemente obtengamos algún tipo de envoltorio borrado por tipo que pueda almacenar funciones solo móviles en el futuro, pero std::function tal como está en c ++ 17 en este momento no puede hacer eso, así que ten en cuenta.