¿Por qué los progtwigdores de C ++ deberían minimizar el uso de ‘nuevo’?

Me encontré con Stack Overflow pregunta Memory leak con std :: string cuando usé std :: list , y uno de los comentarios dice esto:

Deja de usar tanto new . No veo razón alguna por la que hayas usado algo nuevo en ningún lado. Puede crear objetos por valor en C ++ y es una de las grandes ventajas de usar el lenguaje. No tiene que asignar todo en el montón. Deja de pensar como un progtwigdor de Java.

No estoy muy seguro de lo que quiere decir con eso. ¿Por qué se deben crear objetos por valor en C ++ tan a menudo como sea posible, y qué diferencia hace internamente? ¿He malinterpretado la respuesta?

Hay dos técnicas de asignación de memoria ampliamente utilizadas: asignación automática y asignación dinámica. Comúnmente, hay una región correspondiente de memoria para cada uno: la stack y el montón.

Astackr

La stack siempre asigna memoria de forma secuencial. Puede hacerlo porque requiere que libere la memoria en el orden inverso (First-In, Last-Out: FILO). Esta es la técnica de asignación de memoria para variables locales en muchos lenguajes de progtwigción. Es muy, muy rápido porque requiere una contabilidad mínima y la siguiente dirección para asignar es implícita.

En C ++, esto se denomina almacenamiento automático porque el almacenamiento se reclama automáticamente al final del scope. Tan pronto como se completa la ejecución del bloque de código actual (delimitado mediante {} ), la memoria de todas las variables en ese bloque se recostack automáticamente. Este es también el momento en que se invocan los destructores para limpiar los recursos.

Montón

El montón permite un modo de asignación de memoria más flexible. La contabilidad es más compleja y la asignación es más lenta. Como no hay un punto de lanzamiento implícito, debe liberar la memoria manualmente, utilizando delete o delete[] ( free en C). Sin embargo, la ausencia de un punto de lanzamiento implícito es la clave de la flexibilidad del montón.

Razones para usar la asignación dinámica

Incluso si el uso del almacenamiento dynamic es más lento y puede generar memory leaks o fragmentación de la memoria, existen casos de uso perfectamente adecuados para la asignación dinámica, ya que es menos limitado.

Dos razones clave para usar la asignación dinámica:

  • No sabe cuánta memoria necesita en tiempo de comstackción. Por ejemplo, cuando lee un archivo de texto en una cadena, generalmente no sabe qué tamaño tiene el archivo, por lo que no puede decidir cuánta memoria asignar hasta que ejecute el progtwig.

  • Desea asignar memoria que persistirá después de abandonar el bloque actual. Por ejemplo, es posible que desee escribir un string readfile(string path) función string readfile(string path) que devuelve el contenido de un archivo. En este caso, incluso si la stack pudiera contener todo el contenido del archivo, no podría regresar de una función y mantener el bloque de memoria asignado.

Por qué la asignación dinámica a menudo es innecesaria

En C ++ hay una construcción ordenada llamada destructor . Este mecanismo le permite administrar recursos alineando la vida útil del recurso con la vida útil de una variable. Esta técnica se llama RAII y es el punto distintivo de C ++. “Envuelve” recursos en objetos. std::string es un ejemplo perfecto. Este fragmento:

 int main ( int argc, char* argv[] ) { std::string program(argv[0]); } 

en realidad asigna una cantidad variable de memoria. El objeto std::string asigna memoria utilizando el montón y lo libera en su destructor. En este caso, no necesitó administrar manualmente ningún recurso y aún obtuvo los beneficios de la asignación de memoria dinámica.

En particular, implica que en este fragmento:

 int main ( int argc, char* argv[] ) { std::string * program = new std::string(argv[0]); // Bad! delete program; } 

hay una asignación de memoria dinámica innecesaria. El progtwig requiere más tipeo (!) E introduce el riesgo de olvidarse de desasignar la memoria. Lo hace sin beneficio aparente.

Por qué debería usar el almacenamiento automático tan a menudo como sea posible

Básicamente, el último párrafo lo resume. El uso de almacenamiento automático tan a menudo como sea posible hace que sus progtwigs:

  • más rápido para escribir;
  • más rápido cuando se ejecuta;
  • menos propenso a memory leaks / recursos.

Puntos extra

En la pregunta a la que se hace referencia, hay preocupaciones adicionales. En particular, la siguiente clase:

 class Line { public: Line(); ~Line(); std::string* mString; }; Line::Line() { mString = new std::string("foo_bar"); } Line::~Line() { delete mString; } 

En realidad, es mucho más riesgoso de usar que el siguiente:

 class Line { public: Line(); std::string mString; }; Line::Line() { mString = "foo_bar"; // note: there is a cleaner way to write this. } 

La razón es que std::string define correctamente un constructor de copia. Considere el siguiente progtwig:

 int main () { Line l1; Line l2 = l1; } 

Usando la versión original, es probable que este progtwig falle, ya que usa delete en la misma cadena dos veces. Con la versión modificada, cada instancia de Line tendrá su propia instancia de cadena, cada una con su propia memoria y ambas se lanzarán al final del progtwig.

Otras notas

El uso extensivo de RAII se considera una mejor práctica en C ++ debido a todas las razones anteriores. Sin embargo, hay un beneficio adicional que no es inmediatamente obvio. Básicamente, es mejor que la sum de sus partes. Todo el mecanismo se compone . Se escala.

Si usa la clase Line como un bloque de construcción:

  class Table { Line borders[4]; }; 

Entonces

  int main () { Table table; } 

asigna cuatro instancias std::string , cuatro instancias de Line , una instancia de Table y todos los contenidos de la cadena y todo se libera automágicamente .

Porque la stack es rápida y a prueba de tontos

En C ++, se necesita una sola instrucción para asignar espacio, en la stack, para cada objeto de ámbito local en una función dada, y es imposible filtrar cualquiera de esa memoria. Ese comentario pretendía (o debería haber tenido la intención) decir algo como “usa la stack y no el montón”.

Es complicado.

Primero, C ++ no es basura recolectada. Por lo tanto, para cada nuevo, debe haber una eliminación correspondiente. Si no puedes poner esta eliminación, entonces tienes una pérdida de memoria. Ahora, para un caso simple como este:

 std::string *someString = new std::string(...); //Do stuff delete someString; 

Esto es simple. Pero, ¿qué ocurre si “Hacer cosas” arroja una excepción? Oops: pérdida de memoria. ¿Qué sucede si los problemas de “Hacer cosas” return temprano? Oops: pérdida de memoria.

Y esto es para el caso más simple . Si le devuelve esa cadena a alguien, ahora tienen que eliminarla. Y si lo pasan como argumento, ¿la persona que lo recibe debe eliminarlo? ¿Cuándo deberían eliminarlo?

O bien, puedes hacer esto:

 std::string someString(...); //Do stuff 

Sin delete El objeto fue creado en la “stack”, y se destruirá una vez que salga del scope. Incluso puede devolver el objeto, transfiriendo así su contenido a la función de llamada. Puede pasar el objeto a funciones (generalmente como referencia o referencia constante: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis) . Y así sucesivamente.

Todo sin new y delete No hay duda de a quién pertenece la memoria o quién es el responsable de borrarla. Si lo haces:

 std::string someString(...); std::string otherString; otherString = someString; 

Se entiende que otherString tiene una copia de los datos de someString . No es un puntero; es un objeto separado Es posible que tengan los mismos contenidos, pero puede cambiar uno sin afectar al otro:

 someString += "More text."; if(otherString == someString) { /*Will never get here */ } 

Ver la idea?

Los objetos creados por new deben ser eventualmente delete para evitar fugas. No se llamará al destructor, la memoria no se liberará, todo el bit. Como C ++ no tiene recolección de basura, es un problema.

Los objetos creados por valor (es decir, en la stack) mueren automáticamente cuando salen del scope. La llamada al destructor es insertada por el comstackdor, y la memoria se libera automáticamente al regresar la función.

Los punteros inteligentes como auto_ptr , shared_ptr resuelven el problema de referencia colgante, pero requieren disciplina de encoding y otros problemas (capacidad de copiado, bucles de referencia, etc.).

Además, en escenarios con muchos subprocesos múltiples, new es un punto de disputa entre hilos; puede haber un impacto en el rendimiento por usar demasiado new . La creación de objetos de stack es, por definición, local de subprocesos, ya que cada subproceso tiene su propia stack.

La desventaja de los objetos de valor es que mueren una vez que regresa la función de host; no se puede pasar una referencia a los que regresan a la persona que llama, solo copiando o devolviendo por valor.

  • C ++ no emplea ningún administrador de memoria por sí mismo. Otros lenguajes como C #, Java tiene un recolector de basura para manejar la memoria
  • C ++ utilizando las rutinas del sistema operativo para asignar la memoria y demasiado nuevo / eliminar podría fragmentar la memoria disponible
  • Con cualquier aplicación, si la memoria se usa con frecuencia, es aconsejable asignarla previamente y liberarla cuando no sea necesario.
  • La administración incorrecta de la memoria puede provocar pérdidas de memoria y es muy difícil realizar un seguimiento. Entonces, usar objetos de stack dentro del scope de la función es una técnica probada
  • La desventaja del uso de objetos de stack es que crea copias múltiples de objetos al regresar, pasando a funciones, etc. Sin embargo, los comstackdores inteligentes son muy conscientes de estas situaciones y se han optimizado bien para el rendimiento.
  • Es realmente tedioso en C ++ si la memoria se asigna y se libera en dos lugares diferentes. La responsabilidad del lanzamiento es siempre una cuestión y, en su mayoría, confiamos en algunos punteros comúnmente accesibles, objetos astackdos (máximo posible) y técnicas como auto_ptr (objetos RAII).
  • Lo mejor es que tienes control sobre la memoria y lo peor es que no tendrás ningún control sobre la memoria si empleamos una gestión de memoria incorrecta para la aplicación. Los lockings causados ​​por la corrupción de la memoria son los más desagradables y difíciles de rastrear.

En gran medida, eso es alguien elevando sus propias debilidades a una regla general. No hay nada de malo en crear objetos utilizando el new operador. Lo que hay que argumentar es que debes hacerlo con cierta disciplina: si creas un objeto, debes asegurarte de que se destruirá.

La forma más fácil de hacerlo es crear el objeto en el almacenamiento automático, por lo que C ++ sabe destruirlo cuando se sale del scope:

  { File foo = File("foo.dat"); // do things } 

Ahora, observe que cuando se cae de ese bloque después de la llave, foo está fuera del scope. C ++ llamará automáticamente a su dtor. A diferencia de Java, no necesita esperar a que el GC lo encuentre.

Si hubieras escrito

  { File * foo = new File("foo.dat"); 

querrías unirlo explícitamente con

  delete foo; } 

o mejor aún, asigne su File * como un “puntero inteligente”. Si no tiene cuidado acerca de eso, puede provocar fugas.

La respuesta en sí supone erróneamente que, si no utiliza una new , no asigna en el montón; de hecho, en C ++ no lo sabes. A lo sumo, sabrá que una pequeña cantidad de memoria, por ejemplo, un puntero, ciertamente está asignada en la stack. Sin embargo, considere si la implementación de File es algo así como

  class File { private: FileImpl * fd; public: File(String fn){ fd = new FileImpl(fn);} 

entonces FileImpl todavía se asignará en la stack.

Y sí, es mejor que te asegures de tener

  ~File(){ delete fd ; } 

en la clase también; sin él, perderá memoria del montón incluso si aparentemente no asignó el montón.

Veo que faltan algunas razones importantes para hacer tan pocas novedades como sea posible:

Operator new tiene un tiempo de ejecución no determinista

Llamar a new puede o no causar que el sistema operativo asigne una nueva página física a su proceso. Esto puede ser bastante lento si lo hace a menudo. O puede que ya tenga una ubicación de memoria adecuada lista, no lo sabemos. Si su progtwig necesita tener un tiempo de ejecución consistente y predecible (como en un sistema en tiempo real o una simulación de juego / física), debe evitar new en sus ciclos críticos de tiempo.

Operator new es una sincronización de hilos implícita

Sí, me escuchó, su sistema operativo necesita asegurarse de que las tablas de sus páginas sean coherentes y, como tal, llamar a alguien new hará que su cadena adquiera un locking mutex implícito. Si consistentemente llama new desde muchos hilos, en realidad está serializando sus hilos (he hecho esto con 32 CPUs, cada una golpeando new para obtener unos cientos de bytes cada una, ¡ay! Eso fue un pita real para depurar)

El rest, como la lentitud, la fragmentación, la propensión a errores, etc. ya han sido mencionados por otras respuestas.

Cuando utiliza nuevo, los objetos se asignan al montón. Generalmente se usa cuando anticipas la expansión. Cuando declara un objeto como,

 Class var; 

se coloca en la stack.

Siempre tendrá que invocar destruir en el objeto que colocó en el montón con nuevo. Esto abre la posibilidad de memory leaks. ¡Los objetos colocados en la stack no son propensos a la pérdida de memoria!

new() no debe usarse lo menos posible. Se debe usar con el mayor cuidado posible. Y debe usarse tantas veces como sea necesario según lo dictado por el pragmatismo.

La asignación de objetos en la stack, confiando en su destrucción implícita, es un modelo simple. Si el scope requerido de un objeto se ajusta a ese modelo, entonces no hay necesidad de usar new() , con la delete() asociada delete() y la verificación de punteros NULL. En el caso de que tenga muchos objetos de vida corta, la asignación en la stack debería reducir los problemas de fragmentación del montón.

Sin embargo, si la duración de su objeto necesita extenderse más allá del scope actual, entonces new() es la respuesta correcta. Solo asegúrese de prestar atención a cuándo y cómo llama a delete() y las posibilidades de punteros NULL, utilizando objetos eliminados y todos los otros errores que vienen con el uso de punteros.

Pre-C ++ 17:

Porque es propenso a fugas sutiles, incluso si ajusta el resultado en un puntero inteligente .

Considere un usuario “cuidadoso” que recuerda envolver objetos en punteros inteligentes:

 foo(shared_ptr(new T1()), shared_ptr(new T2())); 

Este código es peligroso porque no hay garantía de que shared_ptr esté construido antes de T1 o T2 . Por lo tanto, si uno de los new T1() o new T2() falla luego de que el otro tiene éxito, entonces el primer objeto se shared_ptr porque no existe shared_ptr para destruirlo y desasignarlo.

Solución: use make_shared .

Post-C ++ 17:

Esto ya no es un problema: C ++ 17 impone una restricción en el orden de estas operaciones, en este caso asegurando que cada llamada a new() debe ser seguida inmediatamente por la construcción del puntero inteligente correspondiente, sin ninguna otra operación en Entre. Esto implica que, en el momento en que se llame al segundo new() , se garantiza que el primer objeto ya haya sido envuelto en su puntero inteligente, evitando así cualquier fuga en caso de que se produzca una excepción.

Una explicación más detallada del nuevo orden de evaluación introducido por C ++ 17 fue proporcionada por Barry en otra respuesta .

Creo que el póster quería decir You do not have to allocate everything on the heap lugar de la stack .

Básicamente, los objetos se asignan en la stack (si el tamaño del objeto lo permite, por supuesto) debido al bajo costo de asignación de la stack, en lugar de la asignación basada en el montón que implica bastante trabajo por parte del asignador, y agrega verbosidad porque entonces tienes que administrar los datos asignados en el montón.

Tiendo a estar en desacuerdo con la idea de usar nuevos “demasiado”. Aunque el uso del original del cartel de nuevo con las clases del sistema es un poco ridículo. ( int *i; i = new int[9999]; really? int i[9999]; es mucho más claro.) Creo que eso es lo que estaba obteniendo la chiva del comentarista.

Cuando trabajas con objetos del sistema, es muy raro que necesites más de una referencia al mismo objeto. Siempre que el valor sea el mismo, eso es todo lo que importa. Y los objetos del sistema normalmente no ocupan mucho espacio en la memoria. (un byte por personaje, en una cadena). Y si lo hacen, las bibliotecas deberían estar diseñadas para tener en cuenta esa administración de memoria (si están bien escritas). En estos casos, (todas menos una o dos de las noticias en su código), lo nuevo es prácticamente inútil y solo sirve para introducir confusiones y posibles errores.

Sin embargo, cuando trabajas con tus propias clases / objetos (por ejemplo, la clase Line del póster original), tienes que empezar a pensar en cuestiones como la memoria, la persistencia de los datos, etc., por ti mismo. En este punto, permitir múltiples referencias al mismo valor es invaluable: permite construcciones como listas vinculadas, diccionarios y gráficos, donde múltiples variables no solo tienen el mismo valor, sino que hacen referencia exactamente al mismo objeto en la memoria. Sin embargo, la clase Line no tiene ninguno de esos requisitos. Por lo tanto, el código del póster original en realidad no necesita absolutamente nada new .

Una razón notable para evitar el uso excesivo del montón es el rendimiento, que implica específicamente el rendimiento del mecanismo de gestión de memoria predeterminado utilizado por C ++. Si bien la asignación puede ser bastante rápida en el caso trivial, hacer muchas cosas new y delete en objetos de tamaño no uniforme sin un orden estricto conduce no solo a la fragmentación de la memoria, sino que también complica el algoritmo de asignación y puede destruir el rendimiento en ciertos casos .

Ese es el problema que las agrupaciones de memoria crearon para resolver, lo que permite mitigar las desventajas inherentes de las implementaciones de heap tradicionales, a la vez que le permite utilizar el almacenamiento dynamic según sea necesario.

Mejor aún, sin embargo, para evitar el problema por completo. Si puedes ponerlo en la stack, hazlo.

Dos razones:

  1. No es necesario en este caso. Estás haciendo tu código innecesariamente más complicado.
  2. Asigna espacio en el montón, lo que significa que debe recordar delete más tarde o provocará una pérdida de memoria.

La razón principal es que los objetos en el montón siempre son difíciles de usar y administrar que los valores simples. Escribir código que sea fácil de leer y mantener es siempre la primera prioridad de cualquier progtwigdor serio.

Otro escenario es que la biblioteca que estamos usando proporciona semántica de valores y hace innecesaria la asignación dinámica. Std::string es un buen ejemplo.

Sin embargo, para el código orientado a objetos, usar un puntero, que significa usar new para crearlo de antemano, es obligatorio. Para simplificar la complejidad de la administración de recursos, tenemos docenas de herramientas para hacerlo lo más simple posible, como punteros inteligentes. El paradigma basado en objetos o el paradigma genérico asume una semántica de valores y requiere menos o nada de new , al igual que los carteles en otros lugares.

Los patrones de diseño tradicionales, especialmente los mencionados en el libro GoF , usan mucho new , ya que son códigos OO típicos.

new es el nuevo goto .

Recuerde por qué goto es tan vilipendiado: si bien es una herramienta poderosa y de bajo nivel para el control de flujo, las personas a menudo lo usaban de formas innecesariamente complicadas que dificultaban el seguimiento del código. Además, los patrones más útiles y fáciles de leer se codificaron en enunciados de progtwigción estructurados (por ejemplo, for o while ); el efecto final es que el código donde goto es la forma adecuada de hacerlo es bastante raro, si estás tentado a escribir goto , probablemente estés haciendo las cosas mal (a menos que realmente sepas lo que estás haciendo).

new es similar: a menudo se usa para hacer las cosas innecesariamente complicadas y difíciles de leer, y los patrones de uso más útiles que se pueden codificar se han codificado en varias clases. Furthermore, if you need to use any new usage patterns for which there aren’t already standard classes, you can write your own classes that encode them!

I would even argue that new is worse than goto , due to the need to pair new and delete statements.

Like goto , if you ever think you need to use new , you are probably doing things badly — especially if you are doing so outside of the implementation of a class whose purpose in life is to encapsulate whatever dynamic allocations you need to do.

new allocates objects on the heap. Otherwise, objects are allocated on the stack. Look up the difference between the two .