¿Es una buena práctica utilizar siempre punteros inteligentes?

80

Encuentro que los punteros inteligentes son mucho más cómodos que los punteros en bruto. Entonces, ¿es una buena idea usar siempre punteros inteligentes? (Tenga en cuenta que soy de origen Java y, por lo tanto, no me gusta mucho la idea de la gestión de memoria explícita. Por lo tanto, a menos que haya algunos problemas de rendimiento graves con los punteros inteligentes, me gustaría seguir con ellos).

Nota: Aunque tengo experiencia en Java, entiendo bastante bien la implementación de punteros inteligentes y los conceptos de RAII. Por lo tanto, puede dar por sentado este conocimiento de mi parte al publicar una respuesta. Utilizo la asignación estática en casi todas partes y uso punteros solo cuando es necesario. Mi pregunta es simplemente: ¿Puedo usar siempre punteros inteligentes en lugar de punteros sin procesar?

Dony Borris
fuente
9
usar la palabra "siempre" nunca es algo bueno cuando se habla de buenas prácticas, ya que hay circunstancias en las que el uso de un patrón o pauta no será útil por numerosas razones. M.
Max
@Neil Sí, solo quise decir eso.
Dony Borris
Lo digo sin ofender, pero está claro que necesita un buen libro y empezar desde el principio. Su terminología es incorrecta, y me temo que su código es muy "no C ++".
GManNickG
2
maldición, nada más que programadores salados de C ++ aquí
Bruno

Respuestas:

79

Dadas las diversas ediciones, tengo la impresión de que sería útil un resumen completo.

1. Cuándo no

Hay dos situaciones en las que no debería utilizar punteros inteligentes.

La primera es exactamente la misma situación en la que no debería usar una C++clase de hecho. IE: límite de DLL si no ofrece el código fuente al cliente. Digamos anecdótico.

El segundo ocurre con mucha más frecuencia: un administrador inteligente significa propiedad . Puede usar punteros para apuntar a recursos existentes sin administrar su vida útil, por ejemplo:

void notowner(const std::string& name)
{
  Class* pointer(0);
  if (name == "cat")
    pointer = getCat();
  else if (name == "dog")
    pointer = getDog();

  if (pointer) doSomething(*pointer);
}

Este ejemplo está limitado. Pero un puntero es semánticamente diferente de una referencia en que puede apuntar a una ubicación no válida (el puntero nulo). En este caso, está perfectamente bien no utilizar un puntero inteligente en su lugar, porque no desea administrar la vida útil del objeto.

2. Gerentes inteligentes

A menos que esté escribiendo una clase de administrador inteligente, si usa la palabra clave delete , está haciendo algo mal.

Es un punto de vista controvertido, pero después de haber revisado tantos ejemplos de código defectuoso, ya no corro riesgos. Entonces, si escribe new, necesita un administrador inteligente para la memoria recién asignada. Y lo necesitas ahora mismo.

¡No significa que seas menos programador! Por el contrario, reutilizar el código que se ha demostrado que funciona en lugar de reinventar la rueda una y otra vez es una habilidad clave.

Ahora comienza la verdadera dificultad: ¿qué administrador inteligente?

3. Punteros inteligentes

Hay varios indicadores inteligentes, con varias características.

Omitir std::auto_ptrlo que generalmente debe evitar (su semántica de copia está arruinada).

  • scoped_ptr: sin gastos generales, no se puede copiar ni mover.
  • unique_ptr: sin gastos generales, no se puede copiar, se puede mover.
  • shared_ptr/ weak_ptr: algunos gastos generales (recuento de referencias), se pueden copiar.

Por lo general, intente utilizar scoped_ptro unique_ptr. Si necesita varios propietarios, intente cambiar el diseño. Si no puede cambiar el diseño y realmente necesita varios propietarios, use a shared_ptr, pero tenga cuidado con los ciclos de referencias que deberían romperse con un weak_ptren algún lugar intermedio.

4. Contenedores inteligentes

Muchos punteros inteligentes no están destinados a ser copiados, por lo que su uso con los contenedores STL está algo comprometido.

En lugar de recurrir a shared_ptry sus gastos generales, use contenedores inteligentes del Boost Pointer Container . Emulan la interfaz de los contenedores STL clásicos, pero almacenan los punteros que poseen.

5. Rodando el tuyo

Hay situaciones en las que es posible que desee lanzar su propio administrador inteligente. Verifique que no se haya perdido alguna característica en las bibliotecas que está utilizando de antemano.

Escribir un administrador inteligente en presencia de excepciones es bastante difícil. Por lo general, no puede asumir que la memoria está disponible ( newpuede fallar) o que Copy Constructortiene la no throwgarantía.

Puede ser aceptable, de alguna manera, ignorar la std::bad_allocexcepción e imponer que los Copy Constructors de varios ayudantes no fallan ... después de todo, eso es lo que boost::shared_ptrhace con su Dparámetro de plantilla de eliminación .

Pero no lo recomendaría, especialmente para un principiante. Es un problema complicado y no es probable que note los errores en este momento.

6. Ejemplos

// For the sake of short code, avoid in real code ;)
using namespace boost;

// Example classes
//   Yes, clone returns a raw pointer...
// it puts the burden on the caller as for how to wrap it
//   It is to obey the `Cloneable` concept as described in 
// the Boost Pointer Container library linked above
struct Cloneable
{
  virtual ~Cloneable() {}
  virtual Cloneable* clone() const = 0;
};

struct Derived: Cloneable
{
  virtual Derived* clone() const { new Derived(*this); }
};

void scoped()
{
  scoped_ptr<Cloneable> c(new Derived);
} // memory freed here

// illustration of the moved semantics
unique_ptr<Cloneable> unique()
{
  return unique_ptr<Cloneable>(new Derived);
}

void shared()
{
  shared_ptr<Cloneable> n1(new Derived);
  weak_ptr<Cloneable> w = n1;

  {
    shared_ptr<Cloneable> n2 = n1;          // copy

    n1.reset();

    assert(n1.get() == 0);
    assert(n2.get() != 0);
    assert(!w.expired() && w.get() != 0);
  } // n2 goes out of scope, the memory is released

  assert(w.expired()); // no object any longer
}

void container()
{
  ptr_vector<Cloneable> vec;
  vec.push_back(new Derived);
  vec.push_back(new Derived);

  vec.push_back(
    vec.front().clone()         // Interesting semantic, it is dereferenced!
  );
} // when vec goes out of scope, it clears up everything ;)
Matthieu M.
fuente
4
¡Gran respuesta! :) Supongo que el Sr. Butterworth podría aprender algo de esto.
Dony Borris
2
Personalmente, me gustan las respuestas de Neil (en general, y esta en particular), solo pensé que el tema requería una explicación más profunda dado que la gestión de la memoria es complicada y lo "relativamente" nuevas que son las bibliotecas (estoy pensando en Pointer Container aquí , de 2007).
Matthieu M.
¿Qué quieres decir con "Smart Managers"? Esa sección de la respuesta no tiene sentido para mí. Además, la semántica de std::auto_ptres diferente de lo que la mayoría de la gente espera, pero tiene sentido y conduce a evitar problemas de diseño en el código, decir "generalmente evitarlo" no tiene sentido.
Frunsi
1
@frunsi: si decir "en general evitarlo" no tiene sentido, debe dirigirse rápidamente al comité de normas ISO y explicarles por qué. Están a punto de cometer un terrible error al desaprobarauto_ptr en C ++ 0x, recomendando el uso de unique_ptr;-). Para ser justos, se unique_ptrbasa en la construcción / asignación de movimientos para reemplazar auto_ptr, si desea una propiedad estrictamente transferible en C ++ 03, no tiene muchas opciones.
Steve Jessop
@Steve: ¡Está bien! Parece que unique_ptr es el camino a seguir cuando la semántica de movimiento está disponible. Hasta entonces, auto_ptr sigue siendo útil, pero se eliminará en 100 años aproximadamente;) Si todos comenzamos a escribir código C ++ 0x el próximo año (eso espero) deberíamos evitar auto_ptr ahora, pero lo sospecho, así que .. :) Es broma, tienes razón, debería evitarse ahora.
Frunsi
18

Punteros inteligentes hacen realizar la gestión de la memoria explícita, y si usted no entiende cómo lo están haciendo, que están en un mundo de problemas en la programación con C ++. Y recuerde que la memoria no es el único recurso que administran.

Pero para responder a su pregunta, debería preferir los punteros inteligentes como primera aproximación a una solución, pero posiblemente esté preparado para deshacerse de ellos cuando sea necesario. Nunca debe usar punteros (o cualquier tipo) o asignación dinámica cuando se pueda evitar. Por ejemplo:

string * s1 = new string( "foo" );      // bad
string s2( "bar" );    // good

Editar: Para responder a su pregunta complementaria "¿Puedo usar siempre punteros inteligentes en lugar de punteros sin procesar? Entonces, no, no puede. Si (por ejemplo) necesita implementar su propia versión del operador nuevo, tendría que haz que devuelva un puntero, no un puntero inteligente.


fuente
10
Respuesta completamente inútil. Ojalá tuviera suficiente reputación para rechazar esto.
Dony Borris
8
@Dony La calidad de la respuesta a menudo refleja la de la pregunta.
12
@Dony En general, rechazo las respuestas incorrectas, en lugar de simplemente respuestas inútiles. Después de todo, puede ser difícil saber exactamente qué necesita aprender el interlocutor para iluminarse.
Philip Potter
4
Desafortunadamente, las respuestas de Neil a menudo vienen acompañadas de un lado de condescendencia. Debería dejar de responder preguntas que lo frustran porque el mundo no es tan inteligente o experimentado como él.
3
@Neil: recibiste varios golpes en el nivel de competencia del OP en tus comentarios. Aunque no me malinterpretes. Estoy por ello totalmente. Creo que cualquiera que no haya leído el estándar de lenguaje de principio a fin al menos 5 veces se encuentra en un "mundo de problemas cuando programa con C ++".
13

Por lo general, no debe usar punteros (inteligentes o de otro tipo) si no los necesita. Es mejor hacer que las variables locales, los miembros de la clase, los elementos vectoriales y los elementos similares sean objetos normales en lugar de punteros a objetos. (Como vienes de Java, probablemente tengas la tentación de asignar todo new, lo cual no se recomienda).

Este enfoque (" RAII ") le evita tener que preocuparse por los punteros la mayor parte del tiempo.

Cuando tenga que usar punteros, depende de la situación y de por qué exactamente los necesita, pero generalmente se pueden usar punteros inteligentes. Puede que no sea siempre (en negrita) la mejor opción, pero esto depende de la situación específica.

algo
fuente
4
Entonces, ¿cuáles son esas situaciones, de las que "depende"? ¿Por qué nadie lo cuenta?
P Shved el
1
Puedo pensar en un par de situaciones: los requisitos de rendimiento pueden hacer que los punteros compartidos no sean adecuados ( boost::scoped_ptraún podrían usarse en ese momento, pero tal vez no desee depender de boost entonces) - o necesita interactuar con una API C , en cuyo caso los punteros sin procesar son más consistentes. Si necesita iterar sobre una matriz, sus iteradores probablemente también serán punteros sin procesar.
jalf
1
Si está creando un objeto que puede sobrevivir a la instancia que lo creó, claramente no puede simplemente incrustarlo dentro del otro objeto.
Ben Voigt
1
@Ben Voigt: en el caso general, su ejemplo no es válido. Puede devolver un auto_ptr, unique_ptro shared_ptrsimplemente como pasaría un puntero sin procesar fuera de su alcance transfiriendo la propiedad. scoped_ptres el único puntero inteligente del conjunto que no podrá transferir / compartir la propiedad
David Rodríguez - dribeas
1
@Ben: los punteros compartidos boost tienen un mecanismo para abordar esto, vea esta pregunta que hice: stackoverflow.com/questions/1403465/… . Básicamente, puede tener un puntero compartido que contiene una referencia (en el sentido de recuento) a un objeto, pero cuando se hace referencia, devuelve un puntero diferente (en este caso, un miembro del objeto al que se hace referencia).
Evan Teran
9

Un buen momento para no usar punteros inteligentes es en el límite de la interfaz de una DLL. No sabe si otros ejecutables se compilarán con el mismo compilador / bibliotecas. La convención de llamada de DLL de su sistema no especificará qué aspecto tienen las clases estándar o TR1, incluidos los punteros inteligentes.

Dentro de un ejecutable o biblioteca, si desea representar la propiedad del puntero, los punteros inteligentes son en promedio la mejor manera de hacerlo. Por lo que está bien querer usarlos siempre con preferencia a los crudos. Si realmente puede usarlos siempre es otro asunto.

Para un ejemplo concreto de cuándo no hacerlo, suponga que está escribiendo una representación de un gráfico genérico, con vértices representados por objetos y bordes representados por punteros entre los objetos. Los punteros inteligentes habituales no le ayudarán: los gráficos pueden ser cíclicos y ningún nodo en particular puede ser responsable de la gestión de la memoria de otros nodos, por lo que los punteros compartidos y débiles son insuficientes. Por ejemplo, puede poner todo en un vector y usar índices en lugar de punteros, o poner todo en una deque y usar punteros sin formato. Puede usarlo shared_ptrsi lo desea, pero no agregará nada excepto gastos generales. O puede buscar GC de barrido de marcas.

Un caso más marginal: prefiero ver que las funciones toman un parámetro por puntero o referencia, y prometen no retener un puntero o una referencia a él , en lugar de tomar un shared_ptry dejarlo preguntándose si tal vez conservan una referencia después de regresar, tal vez si modificas la referencia y nunca más romperás algo, etc. No retener referencias es algo que a menudo no se documenta explícitamente, es evidente. Quizás no debería, pero lo hace. Los indicadores inteligentes implican algo acerca de la propiedad y lo dan a entender falsamente que puede ser confuso. Entonces, si su función toma una shared_ptr, asegúrese de documentar si puede retener una referencia o no.

Steve Jessop
fuente
6

En muchas situaciones, creo que definitivamente son el camino a seguir (código de limpieza menos desordenado, menor riesgo de fugas, etc.). Sin embargo, existe un gasto adicional muy leve. Si estuviera escribiendo un código que tuviera que ser lo más rápido posible (digamos un bucle estrecho que tuviera que hacer una asignación y una libre), probablemente no usaría un puntero inteligente con la esperanza de ganar un poco más de velocidad. Pero dudo que suponga una diferencia apreciable en la mayoría de situaciones.

Mark Wilkins
fuente
6
Probablemente usaría un scoped_ptr virtualmente cero sobrecarga si estuviera asignando, usando y destruyendo recursos en un ciclo estrecho (lo que podría no ser una buena idea en primer lugar). Elija el tipo de puntero inteligente adecuado para la tarea.
visitante
Si necesita eliminar el objeto, no hay una penalización de rendimiento real con algunos punteros inteligentes (en particular scoped_ptr)
David Rodríguez - dribeas
@David: Ese es un buen punto ... aunque estaba pensando (posiblemente por ignorancia) que todavía habría una prueba adicional en esa situación. Tendré que deshacerme de un poco de montaje y enseñarme yo mismo.
Mark Wilkins
2
shared_ptrtener sobrecarga tanto en la asignación del bloque de información compartida como para comprobar si se debe desasignar en función de esa información compartida. Pero con los punteros inteligentes de propiedad única, el destructor no necesita realizar ninguna prueba: simplemente elimine el puntero interno. Ejemplos: libstdc ++:, ~auto_ptr() { delete _M_ptr; }boost 1.37: ~scoped_ptr() { checked_delete(ptr); }donde checked_deletees una verificación de tiempo de compilación para ver si el tipo está completo y una única llamada a delete, que probablemente estará en línea.
David Rodríguez - dribeas
@David: Genial, gracias por compartir eso. Aprendí algo.
Mark Wilkins
4

En general, no, no siempre puedes usar punteros inteligentes. Por ejemplo, cuando usa otros marcos que no usan puntero inteligente (como Qt), también debe usar punteros sin formato.

jopa
fuente
2

Si está manejando un recurso, siempre debe usar técnicas RAII, en el caso de la memoria significa usar una forma u otra de un puntero inteligente (nota: inteligente, no shared_ptr, elija el puntero inteligente que sea más apropiado para su caso de uso específico ). Es la única forma de evitar fugas en presencia de excepciones.

Todavía hay casos en los que se necesitan punteros sin procesar, cuando la gestión de recursos no se maneja a través del puntero. En particular, son la única forma de tener una referencia reiniciable. Piense en mantener una referencia en un objeto cuya vida útil no se puede manejar explícitamente (atributo de miembro, objeto en la pila). Pero ese es un caso muy específico que solo he visto una vez en código real. En la mayoría de los casos, usar un shared_ptres un mejor enfoque para compartir un objeto.

David Rodríguez - dribeas
fuente
2

Mi opinión sobre los punteros inteligentes: GENIAL cuando es difícil saber cuándo podría ocurrir la desasignación (digamos dentro de un bloque try / catch, o dentro de una función que llama a una función (¡o incluso a un constructor!) Que podría sacarlo de su función actual) o agregar una mejor administración de memoria a una función que tiene retornos en todas partes del código. O poner punteros en contenedores.

Sin embargo, los punteros inteligentes tienen un costo que quizás no desee pagar en todo su programa. Si la gestión de la memoria es fácil de hacer a mano ("Hmm, sé que cuando finaliza esta función necesito eliminar estos tres punteros, y sé que esta función se ejecutará hasta su finalización"), entonces ¿por qué desperdiciar los ciclos que tiene la computadora? ¿eso?

RyanWilcox
fuente
3
"Hmm, sé que cuando esta función finaliza, necesito eliminar estos tres punteros, y sé que esta función se ejecutará hasta su finalización", para eso auto_ptres, o scoped_ptr. Sería raro para ellos crear una sobrecarga medible y, mientras tanto, facilitan la obtención del código correcto. Por ejemplo, si adquirir el segundo de esos triples arroja una excepción, ¿libera el primero? ¿Cuánto código tienes que escribir para hacerlo, en comparación con el uso de punteros inteligentes? ¿Con qué frecuencia usted realmente adquirir recursos que se necesita para liberar, pero donde su adquisición no puede fallar?
Steve Jessop
Ese es otro buen ejemplo de estar dentro de una función que podría sacarlo de su función actual, y buen punto :)
RyanWilcox
1

Sí, PERO he realizado varios proyectos sin el uso de un puntero inteligente o punteros. Es una buena práctica usar contenedores como deque, list, map, etc. Alternativamente, uso referencias cuando sea posible. En lugar de pasar un puntero, paso una referencia o una referencia constante y casi siempre es ilógico eliminar / liberar una referencia, por lo que nunca tengo un problema allí (por lo general, los creo en la pila escribiendo{ Class class; func(class, ref2, ref3); }


fuente
0

Es. El puntero inteligente es una de las piedras angulares del antiguo ecosistema Cocoa (Touch). Creo que sigue impactando lo nuevo.

Trombe
fuente