¿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ño sizeof(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_ptrs 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?

Author: Clinton, 2011-07-26

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:

  1. Cómo devolver un B* cuando se llama get().
  2. 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.

 34
Author: Howard Hinnant,
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_ptrs. 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.

 2
Author: Nicol Bolas,
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.

 2
Author: Qarterd,
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.

 0
Author: Fabio Fracassi,
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