¿Por qué es el tamaño de make shared two pointers?
Como se ilustra en el código aquí, el tamaño del objeto devuelto desde make_shared es de dos punteros.
Sin embargo, ¿por qué make_shared
no funciona de la siguiente manera (supongamos que T es el tipo al que estamos haciendo un puntero compartido):
El resultado de
make_shared
es un puntero en tamaño, que apunta a la memoria asignada de tamañosizeof(int) + sizeof(T)
, donde el int es un recuento de referencia, y esto se incrementa y decrementa en la construcción / destrucción de la puntero.
unique_ptr
s son solo del tamaño de un puntero, así que no estoy seguro de por qué el puntero compartido necesita dos. Por lo que puedo decir, todo lo que necesita un recuento de referencia, que con make_shared
, se puede colocar con el objeto en sí.
Además, ¿hay alguna implementación que se implemente de la manera que sugiero (sin tener que perder el tiempo con intrusive_ptr
s para objetos particulares)? Si no, ¿cuál es la razón por la que se evita la implementación que sugiero?
4 answers
En todas las implementaciones que conozco, shared_ptr
almacena el puntero propiedad y el recuento de referencia en el mismo bloque de memoria. Esto es contrario a lo que otras respuestas están diciendo. Adicionalmente se almacenará una copia del puntero en el objeto shared_ptr
. N1431 describe el diseño de memoria típico.
Es cierto que uno puede construir un puntero contado de referencia con el tamaño de un solo puntero. Pero std::shared_ptr
contiene características que exigen absolutamente un tamaño de dos punteros. Una de esas características es este constructor:
template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept;
Effects: Constructs a shared_ptr instance that stores p
and shares ownership with r.
Postconditions: get() == p && use_count() == r.use_count()
Un puntero en el shared_ptr
va a apuntar al bloque de control propiedad de r
. Este bloque de control va a contener el puntero propiedad, que no tiene que ser p
, y normalmente no es p
. El otro puntero en el shared_ptr
, el devuelto por get()
, va a ser p
.
Esto se conoce como soporte de aliasing y se introdujo en N2351. Usted puede notar que shared_ptr
tenía un tamaño de dos punteros antes de la introducción de esta característica. Antes de la introducción de esta característica, uno podría haber implementado shared_ptr
con un tamaño de un puntero, pero nadie lo hizo porque era poco práctico. Después de N2351 , se hizo imposible.
Una de las razones por las que no era práctico antes de N2351 fue debido al apoyo a:
shared_ptr<B> p(new A);
Aquí, p.get()
devuelve un B*
, y generalmente ha olvidado todo sobre el tipo A
. El único requisito es que A*
sea convertible a B*
. B
puede derivar de A
usando herencia múltiple. Y esto implica que el valor del puntero en sí puede cambiar al convertir de A
a B
y viceversa. En este ejemplo, shared_ptr<B>
necesita recordar dos cosas:
- Cómo devolver un
B*
cuando se llamaget()
. - Cómo eliminar un
A*
cuando sea el momento de hacerlo.
Una muy buena técnica de implementación para lograr esto es almacenar el B*
en el objeto shared_ptr
, y el A*
dentro del bloque de control con el recuento de referencia.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-26 13:36:47
El recuento de referencia no se puede almacenar en un shared_ptr
. shared_ptr
s tienen que compartir el recuento de referencias entre las diversas instancias, por lo tanto el shared_ptr
debe tener un puntero al recuento de referencias. Además, shared_ptr
(el resultado de make_shared
) no tiene para almacenar el recuento de referencia en la misma asignación en la que se asignó el objeto.
El punto de make_shared
es evitar la asignación de dos bloques de memoria para shared_ptr
s. Normalmente, si solo haces shared_ptr<T>(new T())
, tienes que asignar memoria para el recuento de referencia además del asignado T
. make_shared
pone todo esto en un bloque de asignación, usando la colocación nuevo y eliminar para crear el T
. Así que solo obtienes una asignación de memoria y una eliminación.
Pero shared_ptr
todavía debe tener la posibilidad de almacenar el recuento de referencia en un bloque de memoria diferente, ya que no se requiere usarmake_shared
. Por lo tanto, necesita dos indicadores.
Realmente, sin embargo, esto no debería molestarte. Dos punteros no es mucho espacio, incluso en tierra de 64 bits. Todavía está recibiendo la parte importante de la funcionalidad de intrusive_ptr
(es decir, no asignar memoria dos veces).
Su pregunta parece ser "¿por qué make_shared
debe devolver un shared_ptr
en lugar de algún otro tipo?"Hay muchas razones.
shared_ptr
está destinado a ser una especie de puntero inteligente por defecto, catch-all. Puede usar un unique_ptr o scoped_ptr para casos en los que está haciendo algo especial. O simplemente para asignaciones temporales de memoria en el ámbito de la función. Pero shared_ptr
está destinado a ser el tipo de cosa que se utiliza para cualquier trabajo serio de referencia contado.
Debido a eso, shared_ptr
sería parte de una interfaz. Tendría funciones que toman shared_ptr
. Tendría funciones que devuelven shared_ptr
. Y así sucesivamente.
Enter make_shared
. Bajo su idea, esta función devolvería algún nuevo tipo de objeto, a make_shared_ptr
o lo que sea. Tendría su propio equivalente a weak_ptr
, a make_weak_ptr
. Pero a pesar del hecho de que estos dos conjuntos de tipos compartirían la exacta misma interfaz , no se pueden utilizar juntos.
Las funciones que toman un make_shared_ptr
no pueden tomar un shared_ptr
. Podrías hacer make_shared_ptr
convertible a shared_ptr
, pero no podrías ir al revés. Usted no sería capaz de tomar cualquier shared_ptr
y convertirlo en un make_shared_ptr
, porque shared_ptr
necesita tener dos punteros. No puede hacer su trabajo sin dos indicadores.
Así que ahora tienes dos conjuntos de punteros que son medio incompatibles. Usted tiene conversiones unidireccionales; si usted tiene un función que devuelve un shared_ptr
, es mejor que el usuario use un shared_ptr
en lugar de un make_shared_ptr
.
Hacer esto por el bien del valor del espacio de un puntero simplemente no vale la pena. ¿Creando esta incompatibilidad, creando dos conjuntos de punteros solo para 4 bytes? Eso simplemente no vale la pena el problema que se causa.
Ahora, tal vez te preguntarías, "si tienes make_shared_ptr
¿por qué necesitarías shared_ptr
en absoluto?"
Porque make_shared_ptr
es insuficiente. make_shared
no es la única manera de crear un shared_ptr
. Tal vez estoy trabajando con algún código C. Tal vez estoy usando SQLite3. sqlite3_open
devuelve un sqlite3*
, que es una conexión de base de datos.
Ahora mismo, usando el funtor destructor derecho, puedo almacenar eso sqlite3*
en un shared_ptr
. Ese objeto se contará como referencia. Puedo usar weak_ptr
cuando sea necesario. Puedo jugar todos los trucos que normalmente haría con un C++ regular shared_ptr
que obtengo de make_shared
o cualquier otra interfaz. Y funcionaría perfectamente.
Pero si make_shared_ptr
existe, entonces eso no trabajo. Porque yo no puedo crear uno de ellos a partir de eso. El sqlite3*
ya ha sido asignado; no puedo embestirlo a través de make_shared
, porque make_shared
construye un objeto. No funciona con los ya existentes.
Oh, claro, podría hacer algún truco, donde agrupo el sqlite3*
en un tipo C++ que es destructor lo destruirá, luego use make_shared
para crear ese tipo. Pero luego usarlo se vuelve mucho más complicado: tienes que pasar por otro nivel de indirección. Y tienes que pasar por la molestia de hacer un tipo y así sucesivamente; el método destructor anterior al menos puede utilizar una función lambda simple.
La proliferación de tipos de puntero inteligente es algo que debe evitarse. Necesita uno inmóvil, uno móvil y uno compartido copiable. Y una más para romper las referencias circulares de este último. Si empiezas a tener varios de esos tipos, entonces tienes necesidades muy especiales o estás haciendo algo mal.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-26 09:09:45
Tengo un honey::shared_ptr
implementación que optimiza automáticamente a un tamaño de 1 puntero cuando es intrusiva. Es conceptualmente simple types los tipos que heredan de SharedObj
tienen un bloque de control incrustado, por lo que en ese caso shared_ptr<DerivedSharedObj>
es intrusivo y se puede optimizar. Unifica boost::intrusive_ptr
con punteros no intrusivos como std::shared_ptr
y std::weak_ptr
.
Esta optimización solo es posible porque no apoyo el aliasing (ver la respuesta de Howard). El resultado de make_shared
puede tener entonces 1 tamaño de puntero si T
es conocido por ser intrusivo en tiempo de compilación. Pero, ¿qué pasa si T
se sabe que no es intrusivo en tiempo de compilación? En este caso no es práctico tener 1 tamaño de puntero ya que shared_ptr
debe comportarse genéricamente para soportar bloques de control asignados tanto al lado como por separado de sus objetos. Con solo 1 puntero, el comportamiento genérico sería apuntar al bloque de control, por lo que para llegar a T*
primero tendría que desreferenciar el bloque de control, lo que no es práctico.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-18 23:48:27
Otros ya han dicho que shared_ptr
necesita dos punteros porque tiene que apuntar al bloque de memoria de recuento de referencia y al Bloque de memoria Apuntado a Tipos.
Supongo que lo que estás preguntando es esto:
Cuando se usa make_shared
ambos bloques de memoria se fusionan en uno, y debido a que los tamaños y la alineación de los bloques se conocen y se fijan en tiempo de compilación, un puntero se puede calcular desde el otro (porque tienen un desplazamiento fijo). Entonces, ¿por qué el estándar o boost no crean un segundo tipo como small_shared_ptr
que solo contiene un puntero.
¿Es eso correcto?
Bueno, la respuesta es que si lo piensas bien, rápidamente se convierte en una gran molestia para muy poca ganancia. ¿Cómo haces compatibles los punteros? Una dirección, es decir, asignar un small_shared_ptr
a un shared_ptr
sería fácil, al revés extremadamente difícil. Incluso si resuelve este problema de manera eficiente, la pequeña eficiencia que gane probablemente se perderá por las conversiones de ida y vuelta que inevitablemente rociarán en cualquier programa serio. Y el tipo de puntero adicional también hace que el código que lo usa sea más difícil de entender.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-26 09:07:28