c++ - punteros - smart pointer c programming
¿Cuándo usar shared_ptr y cuándo usar punteros en bruto? (10)
class B;
class A
{
public:
A ()
: m_b(new B())
{
}
shared_ptr<B> GimmeB ()
{
return m_b;
}
private:
shared_ptr<B> m_b;
};
Digamos que B es una clase que semánticamente no debería existir fuera de la vida útil de A, es decir, no tiene ningún sentido que B exista por sí misma. ¿Debería GimmeB
devolver un shared_ptr<B>
o un B*
?
En general, ¿es una buena práctica evitar por completo el uso de punteros en bruto en código C ++, en lugar de punteros inteligentes?
Soy de la opinión de que shared_ptr
solo debe usarse cuando hay una transferencia o un intercambio de propiedad explícitos, lo que creo que es bastante raro fuera de los casos en que una función asigna algo de memoria, la llena con algunos datos y la devuelve, y hay entendiendo entre la persona que llama y la persona que llama que el primero es ahora "responsable" de esos datos.
- Asignas B a la construcción de A.
- Tú dices que B no debería persistir afuera como en la vida
Ambos apuntan a que B es un miembro de A y que simplemente devuelve un acceso de referencia. ¿Estás sobre esto en ingeniería?
Cuando dices: "Digamos que B es una clase que semánticamente no debería existir fuera de la vida útil de A"
Esto me dice que B no debería existir lógicamente sin A, pero ¿qué hay de la existencia física? Si puede estar seguro de que nadie intentará usar un * B después de los indicadores A, quizás un puntero en bruto esté bien. De lo contrario, un puntero más inteligente puede ser apropiado.
Cuando los clientes tienen un puntero directo a A, debe confiar en que lo manejarán adecuadamente; No intentes dtorearlo etc.
Descubrí que las Directrices principales de C ++ ofrecen algunas sugerencias muy útiles para esta pregunta:
Para usar el puntero sin formato (T *) o el puntero más inteligente depende de quién es el propietario del objeto (cuya responsabilidad es liberar la memoria del objeto).
propio :
smart pointer, own<T*>
no propio
T*, T&, span<>
Own <>, span <> está definido en la biblioteca Microsoft GSL
Aquí están las reglas de oro:
1) nunca use el puntero en bruto (o no los tipos propios) para pasar la propiedad
2) el puntero inteligente solo debe usarse cuando se pretende la semántica de propiedad
3) T * o propietario designa un objeto individual (solo)
4) usar vector / array / span para array
5) Para mi indeterminación, shared_ptr se usa generalmente cuando no sabes quién lanzará el objeto, por ejemplo, un objeto es usado por multi-hilo
En general, evitaría usar punteros sin procesar en la medida de lo posible, ya que tienen un significado muy ambiguo: es posible que tenga que desasignar al pointee, pero tal vez no, y solo la documentación escrita y leída por humanos indica cuál es el caso. Y la documentación siempre es mala, desactualizada o mal entendida.
Si la propiedad es un problema, utilice un puntero inteligente. Si no, usaría una referencia si es posible.
Es una buena práctica evitar el uso de punteros sin formato, pero no puede simplemente reemplazar todo con shared_ptr
. En el ejemplo, los usuarios de su clase asumirán que está bien extender la vida útil de B más allá de la de A, y pueden decidir mantener el objeto B devuelto por algún tiempo por sus propias razones. Debería devolver un valor weak_ptr
o, si B no puede existir absolutamente cuando A se destruye, una referencia a B o simplemente un puntero en bruto.
Estoy de acuerdo con su opinión de que shared_ptr
se utiliza mejor cuando se produce el intercambio explícito de recursos, sin embargo, hay otros tipos de punteros inteligentes disponibles.
En su caso preciso: ¿por qué no devolver una referencia?
Un puntero sugiere que los datos pueden ser nulos, sin embargo, aquí siempre habrá una B
en su A
, por lo que nunca será nula. La referencia afirma este comportamiento.
Dicho esto, he visto a personas que abogan por el uso de shared_ptr
incluso en entornos no compartidos, y que weak_ptr
manejadores weak_ptr
, con la idea de "asegurar" la aplicación y evitar los punteros obsoletos. Desafortunadamente, dado que puede recuperar un shared_ptr
de weak_ptr
(y es la única forma de manipular realmente los datos), esto sigue siendo propiedad compartida, incluso si no estaba destinada a ser.
Nota: hay un error sutil con shared_ptr
, una copia de A
compartirá la misma B
que la original de forma predeterminada, a menos que escriba explícitamente un constructor de copia y un operador de asignación de copia. Y, por supuesto, no utilizaría un puntero en bruto en A
para mantener una B
, ¿verdad :)?
Por supuesto, otra pregunta es si realmente necesitas hacerlo. Uno de los principios del buen diseño es la encapsulación . Para lograr la encapsulación:
No debe devolver las asas a sus partes internas (consulte la Ley de Demeter ).
Entonces, tal vez la respuesta real a su pregunta sea que, en lugar de revelar una referencia o un puntero a B
, solo debe modificarse a través de la interfaz de A
La pregunta "¿Cuándo debo usar shared_ptr
y cuándo debo usar punteros en bruto?" tiene una respuesta muy simple:
- Use punteros sin formato cuando no quiera tener ninguna propiedad adjunta al puntero. Este trabajo también puede hacerse a menudo con referencias. Los punteros sin formato también se pueden usar en algunos códigos de bajo nivel (como para implementar punteros inteligentes o contenedores de implementación).
- Utilice
unique_ptr
oscope_ptr
cuando desee una propiedad única del objeto. Esta es la opción más útil, y debe usarse en la mayoría de los casos. La propiedad única también se puede expresar simplemente creando un objeto directamente, en lugar de usar un puntero (esto es incluso mejor que usar ununique_ptr
, si se puede hacer). - Utilice
shared_ptr
ointrusive_ptr
cuando desee compartir la propiedad del puntero. Esto puede ser confuso e ineficiente, y a menudo no es una buena opción. La propiedad compartida puede ser útil en algunos diseños complejos, pero debe evitarse en general, porque conduce a un código que es difícil de entender.
shared_ptr
s realiza una tarea totalmente diferente de los punteros en bruto, y ni shared_ptr
s ni los punteros en bruto son la mejor opción para la mayoría del código.
La siguiente es una buena regla de oro:
- Cuando no hay transferencia o referencias de propiedad compartidas o los punteros simples son suficientes. (Los punteros simples son más flexibles que las referencias).
- Cuando hay transferencia de propiedad pero no propiedad compartida, entonces
std::unique_ptr<>
es una buena opción. A menudo el caso con las funciones de fábrica. - Cuando hay propiedad compartida, entonces es un buen caso de uso para
std::shared_ptr<>
oboost::intrusive_ptr<>
.
Es mejor evitar la propiedad compartida, en parte porque son más caros en términos de copia y std::shared_ptr<>
toma el doble del almacenamiento de un puntero plano, pero, lo más importante, porque son propicios para diseños pobres donde hay no hay propietarios claros, lo que, a su vez, conduce a una bola de objetos que no se pueden destruir porque mantienen los punteros compartidos entre sí.
El mejor diseño es donde se establece una propiedad clara y es jerárquica, de modo que, idealmente, no se requieren punteros inteligentes en absoluto. Por ejemplo, si hay una fábrica que crea objetos únicos o devuelve objetos existentes, tiene sentido que la fábrica sea propietaria de los objetos que crea y simplemente los mantenga por valor en un contenedor asociativo (como std::unordered_map
), para que Puede devolver punteros simples o referencias a sus usuarios. Esta fábrica debe tener una vida útil que comience antes de su primer usuario y finalice después de su último usuario (la propiedad jerárquica), de modo que los usuarios no puedan tener un puntero a un objeto ya destruido.
Si no desea que el beneficiario de GimmeB () pueda prolongar la vida útil del puntero manteniendo una copia del ptr después de que la instancia de A muera, entonces definitivamente no debe devolver un shared_ptr.
Si no se supone que el destinatario de la llamada mantenga el puntero devuelto durante largos períodos de tiempo, es decir, no hay riesgo de que la instancia de A caduque antes de la del puntero, entonces el puntero en bruto sería mejor. Pero incluso una mejor opción es simplemente usar una referencia, a menos que haya una buena razón para usar un puntero en bruto real.
Y finalmente, en el caso de que el puntero devuelto pueda existir después de que haya expirado la vida útil de la instancia A, pero no desea que el puntero extienda la vida útil de la B, entonces puede devolver un valor débil_ptr, que puede usar para probar si todavía existe
La conclusión es que generalmente hay una solución mejor que usar un puntero en bruto.
Tu análisis es bastante correcto, creo. En esta situación, también devolvería una B*
, o incluso una [const] B&
si se garantiza que el objeto nunca será nulo.
Habiendo tenido tiempo para leer punteros inteligentes, llegué a algunas pautas que me dicen qué hacer en muchos casos:
- Si devuelve un objeto cuya vida útil será administrada por el llamante, devuelva
std::unique_ptr
. La persona que llama puede asignarlo a unstd::shared_ptr
si lo desea. - Devolver
std::shared_ptr
es bastante raro, y cuando tiene sentido, generalmente es obvio: le indica al llamante que prolongará la vida útil del objeto apuntado más allá de la vida útil del objeto que originalmente mantenía el recurso . Devolver punteros compartidos desde fábricas no es una excepción: debe hacer esto, por ejemplo. cuando usasstd::enable_shared_from_this
. - Rara vez necesitas
std::weak_ptr
, excepto cuando quieres entender el método delock
. Esto tiene algunos usos, pero son raros. En su ejemplo, si la vida útil del objetoA
no fuera determinista desde el punto de vista del llamante, esto habría sido algo a considerar. - Si devuelve una referencia a un objeto existente cuya vida útil no puede controlar la persona que llama, entonces devuelva un puntero desnudo o una referencia. Al hacerlo, le dice a la persona que llama que existe un objeto y que ella no tiene que cuidar su vida útil. Debe devolver una referencia si no utiliza el valor
nullptr
.