¿Por qué C ++ no tiene un recolector de basura?


270

No estoy haciendo esta pregunta debido a los méritos de la recolección de basura en primer lugar. Mi razón principal para preguntar esto es que sé que Bjarne Stroustrup ha dicho que C ++ tendrá un recolector de basura en algún momento.

Dicho esto, ¿por qué no se ha agregado? Ya hay algunos recolectores de basura para C ++. ¿Es esta una de esas cosas del tipo "más fácil decirlo que hacerlo"? ¿O hay otras razones por las que no se ha agregado (y no se agregará en C ++ 11)?

Enlaces cruzados:

Solo para aclarar, entiendo las razones por las que C ++ no tenía un recolector de basura cuando se creó por primera vez. Me pregunto por qué no se puede agregar el colector.


26
Este es uno de los diez principales mitos sobre C ++ que los enemigos siempre mencionan. La recolección de basura no está "integrada", pero hay varias formas fáciles de hacerlo en C ++. Publicar un comentario porque otros ya respondieron mejor de lo que podría a continuación :)
davr

55
Pero de eso se trata no estar integrado, tienes que hacerlo tú mismo. Realidad de mayor a menor: incorporado, biblioteca, hecho en casa. Yo mismo uso C ++, y definitivamente no odio porque es el mejor lenguaje del mundo. Pero el manejo dinámico de la memoria es un dolor.
QBziZ

44
@Davr: no soy un enemigo de C ++ ni siquiera estoy tratando de argumentar que C ++ necesita un recolector de basura. Lo pregunto porque sé que Bjarne Stroustrup ha dicho que se agregará y tenía curiosidad por saber cuáles fueron las razones para no implementarlo.
Jason Baker,

1
Este artículo The Boehm Collector para C y C ++ del Dr. Dobbs describe un recolector de basura de código abierto que se puede utilizar con C y C ++. Discute algunos de los problemas que surgen con el uso de un recolector de basura con destructores C ++, así como la Biblioteca estándar de C.
Richard Chambers el

1
@rogerdpack: Pero ahora no es tan útil (vea mi respuesta ...), por lo que es poco probable que las implementaciones inviertan en tener una.
einpoklum

Respuestas:


160

La recolección de basura implícita podría haberse agregado, pero simplemente no fue suficiente. Probablemente no solo por complicaciones de implementación, sino también porque las personas no pueden llegar a un consenso general lo suficientemente rápido.

Una cita del propio Bjarne Stroustrup:

Tenía la esperanza de que un recolector de basura que podría habilitarse opcionalmente fuera parte de C ++ 0x, pero hubo suficientes problemas técnicos que tuve que resolver con solo una especificación detallada de cómo se integra dicho recolector con el resto del lenguaje , si se proporciona Como es el caso con esencialmente todas las características de C ++ 0x, existe una implementación experimental.

Hay una buena discusión sobre el tema aquí .

Visión general:

C ++ es muy poderoso y te permite hacer casi cualquier cosa. Por esta razón, no le empuja automáticamente muchas cosas que podrían afectar el rendimiento. La recolección de basura se puede implementar fácilmente con punteros inteligentes (objetos que envuelven punteros con un recuento de referencia, que se eliminan automáticamente cuando el recuento de referencia llega a 0).

C ++ fue construido con competidores en mente que no tenían recolección de basura. La eficiencia fue la principal preocupación de que C ++ tuvo que defenderse de las críticas en comparación con C y otros.

Hay 2 tipos de recolección de basura ...

Recolección de basura explícita:

C ++ 0x tendrá recolección de basura mediante punteros creados con shared_ptr

Si lo desea, puede usarlo, si no lo quiere, no está obligado a usarlo.

Actualmente, también puedes usar boost: shared_ptr si no quieres esperar a C ++ 0x.

Recolección de basura implícita:

Sin embargo, no tiene recolección de basura transparente. Sin embargo, será un punto de enfoque para futuras especificaciones de C ++.

¿Por qué Tr1 no tiene recolección de basura implícita?

Hay muchas cosas que debería haber tenido tr1 de C ++ 0x, Bjarne Stroustrup en entrevistas anteriores declaró que tr1 no tenía tanto como le hubiera gustado.


71
Me gustaría llegar a ser un enemigo si C ++ obligado recolección de basura sobre mí! ¿Por qué la gente no puede usar smart_ptr's? ¿Cómo haría una bifurcación de estilo Unix de bajo nivel, con un recolector de basura en el camino? Otras cosas se verían afectadas, como el enhebrado. Python tiene su bloqueo de intérprete global principalmente debido a su recolección de basura (ver Cython). Manténgalo fuera de C / C ++, gracias.
unixman83

26
@ unixman83: El principal problema con la recolección de basura contada por referencia (es decir std::shared_ptr) son las referencias cíclicas, que causan una pérdida de memoria. Por lo tanto, debe usar std::weak_ptrcon cuidado para romper los ciclos, lo cual es desordenado. El estilo de marca y barrido GC no tiene este problema. No hay incompatibilidad inherente entre subprocesos / bifurcaciones y recolección de basura. Java y C # tienen subprocesamiento múltiple preventivo de alto rendimiento y un recolector de basura. Hay problemas que hacer con las aplicaciones en tiempo real y un recolector de basura, ya que la mayoría de los recolectores de basura tienen que detener el mundo para funcionar.
Andrew Tomazos

9
"El principal problema con la recolección de basura contada por referencia (es decir std::shared_ptr) son las referencias cíclicas" y el rendimiento horrible, lo cual es irónico porque un mejor rendimiento suele ser la justificación para usar C ++ ... flyingfrogblog.blogspot.co.uk/2011/01/…
Jon Harrop

11
"¿Cómo haría una bifurcación de estilo Unix de bajo nivel". De la misma manera que los lenguajes GC'd como OCaml lo han estado haciendo durante ~ 20 años o más.
Jon Harrop

9
"Python tiene su bloqueo global de intérprete principalmente debido a su recolección de basura". El argumento de Strawman. Java y .NET tienen GC pero ninguno tiene bloqueos globales.
Jon Harrop

149

Para agregar al debate aquí.

Existen problemas conocidos con la recolección de basura, y comprenderlos ayuda a comprender por qué no hay ninguno en C ++.

1. ¿Rendimiento?

La primera queja es a menudo sobre el rendimiento, pero la mayoría de las personas realmente no se dan cuenta de lo que están hablando. Como lo ilustra Martin Beckettel problema, puede no ser el rendimiento per se, sino la previsibilidad del rendimiento.

Actualmente hay 2 familias de GC que se implementan ampliamente:

  • Tipo de marca y barrido
  • Tipo de recuento de referencias

El Mark And Sweepes más rápido (menos impacto en el rendimiento general) pero sufre de un síndrome de "congelar el mundo": es decir, cuando se activa el GC, todo lo demás se detiene hasta que el GC haya realizado su limpieza. Si desea crear un servidor que responda en unos pocos milisegundos ... algunas transacciones no cumplirán con sus expectativas :)

El problema Reference Countinges diferente: el recuento de referencias agrega una sobrecarga, especialmente en entornos de subprocesos múltiples porque necesita tener un conteo atómico. Además, existe el problema de los ciclos de referencia, por lo que necesita un algoritmo inteligente para detectar esos ciclos y eliminarlos (generalmente también se implementa mediante un "congelamiento del mundo", aunque menos frecuente). En general, a partir de hoy, este tipo (aunque normalmente responde mejor o se congela con menos frecuencia) es más lento que el Mark And Sweep.

He visto un artículo de los implementadores de Eiffel que intentaban implementar un Reference Countingrecolector de basura que tendría un rendimiento global similar al Mark And Sweepdel aspecto "Congelar el mundo". Se requiere un hilo separado para el GC (típico). El algoritmo fue un poco aterrador (al final), pero el trabajo hizo un buen trabajo al introducir los conceptos uno por uno y mostrar la evolución del algoritmo desde la versión "simple" a la versión completa. Lectura recomendada si solo pudiera volver a poner mis manos sobre el archivo PDF ...

2. La adquisición de recursos es la inicialización (RAII)

Es un idioma común en el sentido de C++que envolverá la propiedad de los recursos dentro de un objeto para asegurarse de que se liberen correctamente. Se usa principalmente para la memoria ya que no tenemos recolección de basura, pero también es útil para muchas otras situaciones:

  • bloqueos (multihilo, identificador de archivo, ...)
  • conexiones (a una base de datos, otro servidor, ...)

La idea es controlar adecuadamente la vida útil del objeto:

  • debería estar vivo mientras lo necesites
  • debería ser asesinado cuando termines

El problema de GC es que si ayuda con el primero y, en última instancia, garantiza que más tarde ... este "último" puede no ser suficiente. Si liberas un bloqueo, ¡realmente te gustaría que se libere ahora, para que no bloquee más llamadas!

Los idiomas con GC tienen dos soluciones:

  • no use GC cuando la asignación de la pila es suficiente: normalmente es por problemas de rendimiento, pero en nuestro caso realmente ayuda, ya que el alcance define la vida útil
  • usingconstruir ... pero es RAII explícito (débil) mientras que en C ++ RAII está implícito para que el usuario NO PUEDA cometer el error sin darse cuenta (al omitir la usingpalabra clave)

3. Punteros inteligentes

Los punteros inteligentes a menudo aparecen como una bala de plata para manejar la memoria C++. Muchas veces he escuchado: no necesitamos GC después de todo, ya que tenemos punteros inteligentes.

Uno no podría estar más equivocado.

Los punteros inteligentes ayudan: auto_ptry unique_ptrusan conceptos RAII, extremadamente útiles. Son tan simples que puede escribirlos usted mismo con bastante facilidad.

Sin embargo, cuando se necesita compartir la propiedad, se vuelve más difícil: puede compartir entre varios subprocesos y hay algunos problemas sutiles con el manejo del recuento. Por lo tanto, uno naturalmente va hacia shared_ptr.

Es genial, para eso es Boost después de todo, pero no es una bala de plata. De hecho, el problema principal shared_ptres que emula un GC implementado por, Reference Countingpero debe implementar la detección del ciclo usted mismo ... Urg

Por supuesto, existe esta weak_ptrcosita, pero desafortunadamente ya he visto pérdidas de memoria a pesar del uso shared_ptrdebido a esos ciclos ... y cuando estás en un entorno de múltiples subprocesos, ¡es extremadamente difícil de detectar!

4. ¿Cuál es la solución?

No hay una bala de plata, pero como siempre, definitivamente es factible. En ausencia de GC, uno debe ser claro sobre la propiedad:

  • prefiera tener un solo propietario en un momento dado, si es posible
  • si no, asegúrese de que su diagrama de clase no tenga ningún ciclo relacionado con la propiedad y rómpalos con una aplicación sutil de weak_ptr

De hecho, sería genial tener un GC ... sin embargo, no es un problema trivial. Y mientras tanto, solo tenemos que arremangarnos.


2
¡Ojalá pudiera aceptar dos respuestas! Esto es simplemente genial. Una cosa para señalar, en lo que respecta al rendimiento, el GC que se ejecuta en un hilo separado es bastante común (se usa en Java y .Net). Por supuesto, eso podría no ser aceptable en los sistemas integrados.
Jason Baker

14
¿Solo dos tipos? ¿Qué tal copiar coleccionistas? Coleccionistas generacionales? ¿Colectores concurrentes variados (incluida la cinta de correr en tiempo real de Baker)? Varios colectores híbridos? Hombre, la pura ignorancia en la industria de este campo me sorprende a veces.
SOLO MI OPINIÓN correcta

12
¿Dije que solo había 2 tipos? Dije que había 2 que se desplegaron ampliamente. Hasta donde yo sé, Python, Java y C # usan todos los algoritmos Mark y Sweep ahora (Java solía tener un algoritmo de conteo de referencia). Para ser aún más preciso, me parece que C # usa GC generacional para ciclos menores, Mark And Sweep para ciclos mayores y Copiar para combatir la fragmentación de la memoria; aunque yo diría que el corazón del algoritmo es Mark And Sweep. ¿Conoces algún lenguaje convencional que use otra tecnología? Siempre estoy feliz de aprender.
Matthieu M.

3
Acabas de nombrar un lenguaje convencional que usa tres.
SOLO MI OPINIÓN correcta

3
La principal diferencia es que el GC generacional e incremental no necesita detener el mundo para que funcione, y puede hacer que funcionen en sistemas de un solo subproceso sin demasiada sobrecarga realizando ocasionalmente iteraciones del recorrido del árbol al acceder a los punteros del GC (el factor puede determinarse por el número de nuevos nodos, junto con una predicción básica de la necesidad de recopilar). Puede llevar GC aún más lejos al incluir datos sobre dónde se produjo la creación / modificación del nodo en el código, lo que podría permitirle mejorar sus predicciones, y obtendrá el Escape Analysis de forma gratuita con él.
Keldon Alleyne

56

¿Que tipo? ¿Debería estar optimizado para controladores de lavadora integrados, teléfonos celulares, estaciones de trabajo o supercomputadoras?
¿Debería priorizar la capacidad de respuesta de la interfaz gráfica de usuario o la carga del servidor?
¿debería usar mucha memoria o mucha CPU?

C / c ++ se usa en demasiadas circunstancias diferentes. Sospecho que algo como impulsar los punteros inteligentes será suficiente para la mayoría de los usuarios

Editar: los recolectores de basura automáticos no son tanto un problema de rendimiento (siempre se puede comprar más servidor) es una cuestión de rendimiento predecible.
No saber cuándo va a entrar en funcionamiento el GC es como emplear a un piloto de aerolínea narcoléptico, la mayoría de las veces son geniales, ¡pero cuando realmente necesita respuesta!


66
Definitivamente entiendo su punto, pero me siento obligado a preguntar: ¿no se utiliza Java en casi tantas aplicaciones?
Jason Baker,

35
No. Java no es adecuado para aplicaciones de alto rendimiento, por la sencilla razón de que no tiene garantías de rendimiento en la misma medida que C ++. Por lo tanto, lo encontrará en un teléfono celular, pero no lo encontrará en un interruptor celular o una supercomputadora.
Zathrus

11
¡Siempre puede comprar más servidores, pero no siempre puede comprar más CPU para el teléfono celular que ya está en el bolsillo del cliente!
Crashworks

8
Java ha logrado una gran recuperación del rendimiento en la eficiencia de la CPU. El problema realmente insoluble es el uso de memoria, Java es inherentemente menos eficiente en memoria que C ++. Y esa ineficiencia se debe al hecho de que es basura recolectada. La recolección de basura no puede ser rápida y eficiente en cuanto a memoria, un hecho que se vuelve obvio si observa qué tan rápido funcionan los algoritmos de GC.
Nate CK el

2
@Zathrus java puede ganar en rendimiento b / c del jit de optimización, aunque no latencia (abucheo en tiempo real), y ciertamente no huella de memoria.
gtrak

34

Una de las principales razones por las que C ++ no ha incorporado la recolección de basura es que hacer que la recolección de basura juegue bien con los destructores es muy, muy difícil. Hasta donde yo sé, nadie realmente sabe cómo resolverlo completamente todavía. Hay muchos problemas con los que lidiar:

  • vidas de objetos deterministas (el recuento de referencias le da esto, pero GC no. Aunque puede que no sea tan importante).
  • ¿Qué sucede si un destructor tira cuando el objeto se está recolectando basura? La mayoría de los idiomas ignoran esta excepción, ya que en realidad no hay ningún bloque de captura para poder transportarlo, pero probablemente esta no sea una solución aceptable para C ++.
  • ¿Cómo habilitarlo / deshabilitarlo? Naturalmente, probablemente sería una decisión de tiempo de compilación, pero el código escrito para GC versus el código escrito para NOT GC será muy diferente y probablemente incompatible. ¿Cómo reconcilias esto?

Estos son solo algunos de los problemas enfrentados.


17
GC y destructores es un problema resuelto, a un lado de Bjarne. Los destructores no corren durante GC, porque ese no es el punto de GC. GC en C ++ existe para crear la noción de memoria infinita , no otros recursos infinitos.
MSalters

2
Si no se ejecutan los destructores, eso cambia completamente la semántica del lenguaje. Supongo que al menos necesitaría una nueva palabra clave "gcnew" o algo así para permitir explícitamente que este objeto sea GC'ed (y, por lo tanto, no debe usarlo para envolver recursos además de la memoria).
Greg Rogers

77
Este es un argumento falso. Dado que C ++ tiene administración de memoria explícita, debe averiguar cuándo se debe liberar cada objeto. Con GC, no es peor; más bien, el problema se reduce a descubrir cuándo se liberan ciertos objetos, es decir, aquellos objetos que requieren consideraciones especiales al eliminarlos. La experiencia en programación en Java y C # revela que la gran mayoría de los objetos no requieren consideraciones especiales y pueden dejarse en forma segura en el GC. Como resultado, una de las funciones principales de los destructores en C ++ es liberar objetos secundarios, que GC maneja automáticamente.
Nate CK el

2
@ NateC-K: Una cosa que se mejora en GC frente a no GC (quizás lo más importante) es la capacidad de un sistema GC sólido para garantizar que cada referencia continuará apuntando al mismo objeto mientras exista la referencia. Llamando Disposesobre un objeto puede hacer que sea unsable, pero las referencias que apuntaban al objeto cuando estaba vivo continuará haciéndolo después de que esté muerto. Por el contrario, en los sistemas que no son GC, los objetos se pueden eliminar mientras existan referencias, y rara vez hay algún límite para los estragos que pueden generarse si se utiliza una de esas referencias.
supercat

22

Aunque esta es una vieja pregunta, todavía hay un problema que no veo que nadie haya abordado en absoluto: la recolección de basura es casi imposible de especificar.

En particular, el estándar C ++ es bastante cuidadoso al especificar el lenguaje en términos de comportamiento externo observable, en lugar de cómo la implementación logra ese comportamiento. En el caso de la recolección de basura, sin embargo, no es virtualmente ningún comportamiento observable externamente.

La idea general de la recolección de basura es que debe hacer un intento razonable de asegurar que una asignación de memoria tenga éxito. Desafortunadamente, es esencialmente imposible garantizar que cualquier asignación de memoria tenga éxito, incluso si tiene un recolector de basura en funcionamiento. Esto es cierto hasta cierto punto en cualquier caso, pero particularmente en el caso de C ++, porque (probablemente) no es posible utilizar un recopilador de copia (o algo similar) que mueva objetos en la memoria durante un ciclo de recopilación.

Si no puede mover objetos, no puede crear un espacio de memoria único y contiguo desde el cual hacer sus asignaciones, y eso significa que su montón (o almacén gratuito, o como prefiera llamarlo) puede, y probablemente lo hará , fragmentarse con el tiempo. Esto, a su vez, puede evitar que una asignación tenga éxito, incluso cuando hay más memoria libre que la cantidad solicitada.

Si bien es posible encontrar alguna garantía que diga (en esencia) que si repite exactamente el mismo patrón de asignación repetidamente, y tuvo éxito la primera vez, continuará teniendo éxito en las iteraciones posteriores, siempre que la memoria asignada se volvió inaccesible entre iteraciones. Esa es una garantía tan débil que es esencialmente inútil, pero no veo ninguna esperanza razonable de fortalecerla.

Aun así, es más fuerte de lo que se ha propuesto para C ++. La propuesta anterior [advertencia: PDF] (que se descartó) no garantizaba nada en absoluto. En 28 páginas de propuesta, lo que obtuvo en el camino del comportamiento observable externamente fue una nota única (no normativa) que decía:

[Nota: Para los programas de recolección de basura, una implementación alojada de alta calidad debería intentar maximizar la cantidad de memoria inalcanzable que reclama. —Nota final]

Al menos para mí, esto plantea una pregunta seria sobre el retorno de la inversión. Vamos a romper el código existente (nadie sabe exactamente cuánto, pero definitivamente un poco), establecer nuevos requisitos en las implementaciones y nuevas restricciones en el código, y lo que obtenemos a cambio es, posiblemente, ¿nada?

Incluso en el mejor de los casos, lo que obtenemos son programas que, basados ​​en pruebas con Java , probablemente requerirán alrededor de seis veces más memoria para ejecutarse a la misma velocidad que lo hacen ahora. Peor aún, la recolección de basura formó parte de Java desde el principio: C ++ impone suficientes restricciones en el recolector de basura que seguramente tendrá una relación costo / beneficio aún peor (incluso si vamos más allá de lo que garantiza la propuesta y suponemos que habrá algún beneficio).

Resumiría la situación matemáticamente: esta es una situación compleja. Como cualquier matemático sabe, un número complejo tiene dos partes: real e imaginaria. Me parece que lo que tenemos aquí son costos reales, pero beneficios que son (al menos en su mayoría) imaginarios.


Yo diría que incluso si uno especifica que para una operación adecuada todos los objetos deben ser eliminados, y solo los objetos que fueron eliminados serían elegibles para la recolección, el soporte del compilador para la recolección de basura con seguimiento de referencia aún podría ser útil, ya que dicho lenguaje podría garantizar se garantizaría que el uso de un puntero eliminado (referencia) atrapa, en lugar de causar un comportamiento indefinido.
supercat

2
Incluso en Java, el GC no está realmente especificado para hacer nada útil AFAIK. Podría llamarte free(donde quiero decir freeanálogo al lenguaje C). Pero Java nunca garantiza llamar a finalizadores ni nada por el estilo. De hecho, C ++ hace mucho más que Java para ejecutar escrituras de base de datos de confirmación, enjuagar identificadores de archivos, etc. Java afirma tener "GC", pero los desarrolladores de Java deben llamar meticulosamente close()todo el tiempo y deben ser muy conscientes de la gestión de recursos, teniendo cuidado de no llamar close()demasiado pronto o demasiado tarde. C ++ nos libera de eso. ... (continuación)
Aaron McDaid el

2
.. mi comentario hace un momento no pretende criticar a Java. Solo estoy observando que el término "recolección de basura" es un término muy extraño: significa mucho menos de lo que la gente piensa y, por lo tanto, es difícil discutirlo sin tener claro qué significa.
Aaron McDaid el

@AaronMcDaid Es cierto que GC no ayuda en absoluto con los recursos que no son de memoria. Afortunadamente, estos recursos se asignan muy raramente en comparación con la memoria. Además, más del 90% de ellos se pueden liberar en el método que los asignó, por try (Whatever w=...) {...}lo que lo resuelve (y recibirá una advertencia cuando lo olvide). Los restantes también son problemáticos con RAII. Llamar close()"todo el tiempo" significa tal vez una vez por decenas de miles de líneas, por lo que no es tan malo, mientras que la memoria se asigna casi en cada línea de Java.
maaartinus

15

Si desea la recolección automática de basura, existen buenos recolectores de basura comerciales y de dominio público para C ++. Para aplicaciones donde la recolección de basura es adecuada, C ++ es un excelente lenguaje de recolección de basura con un rendimiento que se compara favorablemente con otros lenguajes recolectados de basura. Consulte El lenguaje de programación C ++ (4ª edición) para obtener información sobre la recolección de basura automática en C ++. Ver también, Hans-J. Sitio de Boehm para recolección de basura C y C ++ ( archivo ).

Además, C ++ admite técnicas de programación que permiten que la administración de memoria sea segura e implícita sin un recolector de basura . Considero que la recolección de basura es una última opción y una forma imperfecta de manejo para la gestión de recursos. Eso no significa que nunca sea útil, solo que hay mejores enfoques en muchas situaciones.

Fuente: http://www.stroustrup.com/bs_faq.html#garbage-collection

En cuanto a por qué no lo tiene incorporado, si recuerdo correctamente, fue inventado antes de que GC fuera la cosa , y no creo que el lenguaje pudiera haber tenido GC por varias razones (compatibilidad con IE con versiones anteriores de C)

Espero que esto ayude.


"con un rendimiento que se compara favorablemente con otros idiomas recolectados de basura". ¿Citación?
Jon Harrop

1
Mi enlace estaba roto Escribí esta respuesta hace 5 años.
Rayne

1
Ok, esperaba una verificación independiente de estas afirmaciones, es decir, no por Stroustrup o Boehm. :-)
Jon Harrop

12

Stroustrup hizo algunos buenos comentarios al respecto en la conferencia Going Native 2013.

Simplemente pase a unos 25m50s en este video . (Recomiendo ver el video completo en realidad, pero esto salta a las cosas sobre la recolección de basura).

Cuando tiene un lenguaje realmente excelente que hace que sea fácil (y seguro, predecible, fácil de leer y fácil de enseñar) manejar objetos y valores de forma directa, evitando el uso (explícito) del montón, entonces ni siquiera quieres la recolección de basura.

Con C ++ moderno, y las cosas que tenemos en C ++ 11, la recolección de basura ya no es deseable, excepto en circunstancias limitadas. De hecho, incluso si un buen recolector de basura está integrado en uno de los principales compiladores de C ++, creo que no se usará con mucha frecuencia. Será más fácil , no más difícil, evitar el GC.

Él muestra este ejemplo:

void f(int n, int x) {
    Gadget *p = new Gadget{n};
    if(x<100) throw SomeException{};
    if(x<200) return;
    delete p;
}

Esto no es seguro en C ++. ¡Pero tampoco es seguro en Java! En C ++, si la función regresa temprano, deletenunca se llamará. Pero si tuvo una recolección de basura completa, como en Java, simplemente recibe una sugerencia de que el objeto se destruirá "en algún momento en el futuro" ( Actualización: es aún peor que esto. Java nopromete llamar al finalizador alguna vez, tal vez nunca se llame). Esto no es lo suficientemente bueno si Gadget tiene un identificador de archivo abierto, o una conexión a una base de datos, o datos que ha guardado para escribir en una base de datos en un momento posterior. Queremos que el Gadget se destruya tan pronto como esté terminado, para liberar estos recursos lo antes posible. No desea que su servidor de bases de datos tenga problemas con miles de conexiones de bases de datos que ya no se necesitan, no sabe que su programa ha terminado de funcionar.

Entonces, ¿cuál es la solución? Hay algunos enfoques. El enfoque obvio, que utilizará para la gran mayoría de sus objetos es:

void f(int n, int x) {
    Gadget p = {n};  // Just leave it on the stack (where it belongs!)
    if(x<100) throw SomeException{};
    if(x<200) return;
}

Esto requiere menos caracteres para escribir. No tiene que newinterponerse en el camino. No requiere que escriba Gadgetdos veces. El objeto se destruye al final de la función. Si esto es lo que quieres, esto es muy intuitivo. Gadgets se comportan igual que into double. Predecible, fácil de leer, fácil de enseñar. Todo es un "valor". A veces es un gran valor, pero los valores son más fáciles de enseñar porque no tienes esta cosa de 'acción a distancia' que obtienes con punteros (o referencias).

La mayoría de los objetos que crea son para usar solo en la función que los creó, y tal vez pasen como entradas a las funciones secundarias. El programador no debería tener que pensar en la "administración de memoria" al devolver objetos, o de otra manera compartir objetos en partes muy separadas del software.

El alcance y la vida útil son importantes. La mayoría de las veces, es más fácil si la vida útil es la misma que el alcance. Es más fácil de entender y más fácil de enseñar. Cuando desee una vida útil diferente, debería ser obvio leer el código que está haciendo esto, por shared_ptrejemplo , utilizando . (O devolviendo objetos (grandes) por valor, aprovechando la semántica de movimiento o unique_ptr.

Esto puede parecer un problema de eficiencia. ¿Qué sucede si quiero devolver un gadget foo()? La semántica de movimiento de C ++ 11 facilita la devolución de objetos grandes. Simplemente escriba Gadget foo() { ... }y simplemente funcionará, y funcionará rápidamente. No necesita meterse consigo &&mismo, simplemente devuelva las cosas por valor y el lenguaje a menudo podrá hacer las optimizaciones necesarias. (Incluso antes de C ++ 03, los compiladores hicieron un trabajo notablemente bueno al evitar copias innecesarias).

Como dijo Stroustrup en otra parte del video (parafraseando): "Solo un informático insistiría en copiar un objeto y luego destruir el original. (La audiencia se ríe). ¿Por qué no mover el objeto directamente a la nueva ubicación? Esto es lo que los humanos (no los informáticos) esperan ".

Cuando puede garantizar que solo se necesita una copia de un objeto, es mucho más fácil comprender la vida útil del objeto. Puede elegir qué política de por vida desea, y la recolección de basura está ahí si lo desea. Pero cuando comprenda los beneficios de los otros enfoques, encontrará que la recolección de basura se encuentra al final de su lista de preferencias.

Si eso no funciona para usted, usted puede utilizar unique_ptr, o en su defecto, shared_ptr. Bien escrito C ++ 11 es más corto, más fácil de leer y más fácil de enseñar que muchos otros lenguajes cuando se trata de la gestión de la memoria.


1
GC solo debe usarse para objetos que no adquieren recursos (es decir, pedir a otras entidades que hagan cosas en su nombre "hasta nuevo aviso"). Si Gadgetno pide nada más para hacer algo en su nombre, el código original estaría perfectamente seguro en Java si deletese eliminara la declaración sin sentido (para Java) .
supercat

@supercat, los objetos con destructores aburridos son interesantes. (No he definido 'aburrido', pero básicamente destructores que nunca necesitan ser llamados, excepto por la liberación de memoria). Es posible que un compilador individual trate shared_ptr<T>especialmente cuando Tes "aburrido". Podría decidir no administrar realmente un contador de referencia para ese tipo y, en su lugar, usar GC. Esto permitiría que GC se use sin que el desarrollador tenga que darse cuenta. A shared_ptrpodría verse simplemente como un puntero GC, por adecuado T. Pero hay limitaciones en esto, y haría que muchos programas fueran más lentos.
Aaron McDaid el

Un buen sistema de tipos debe tener diferentes tipos para objetos de almacenamiento dinámico administrados por GC y RAII, ya que algunos patrones de uso funcionan muy bien con uno y muy mal con el otro. En .NET o Java, una instrucción string1=string2;se ejecutará muy rápidamente independientemente de la longitud de la cadena (literalmente no es más que una carga de registro y un almacén de registros), y no requiere ningún bloqueo para garantizar que si la instrucción anterior se ejecuta mientras string2está siendo escrito, string1retendrá el valor antiguo o el nuevo valor, sin Comportamiento Indefinido).
supercat

En C ++, la asignación de a shared_ptr<String>requiere mucha sincronización detrás de escena, y la asignación de a Stringpuede comportarse de manera extraña si una variable se lee y se escribe simultáneamente. Los casos en los que uno quisiera escribir y leer Stringsimultáneamente no son terriblemente comunes, pero pueden surgir si, por ejemplo, algún código desea poner a disposición de otros hilos informes de estado continuos. En .NET y Java, tales cosas simplemente "funcionan".
supercat

1
@curiousguy nada ha cambiado, a menos que tome las precauciones correctas, Java aún permite que se llame al finalizador tan pronto como el constructor haya terminado. Aquí un ejemplo de la vida real: " finalize () solicitó objetos muy accesibles en Java 8 ". La conclusión es que nunca debe usar esta función, ya que casi todos aceptan ser un error de diseño histórico del lenguaje. Cuando seguimos ese consejo, el lenguaje proporciona el determinismo que amamos.
Holger

11

Porque el C ++ moderno no necesita recolección de basura.

La respuesta a las preguntas frecuentes de Bjarne Stroustrup sobre este asunto dice :

No me gusta la basura No me gusta tirar basura. Mi ideal es eliminar la necesidad de un recolector de basura al no producir basura. Eso ahora es posible.


La situación, para el código escrito en estos días (C ++ 17 y siguiendo las Pautas básicas oficiales ) es la siguiente:

  • La mayoría del código relacionado con la propiedad de la memoria está en bibliotecas (especialmente las que proporcionan contenedores).
  • La mayor parte del uso del código que implica la propiedad de la memoria sigue el patrón RAII , por lo que la asignación se realiza en la construcción y la desasignación en la destrucción, que ocurre al salir del alcance en el que se asignó algo.
  • No asigna ni desasigna explícitamente la memoria directamente .
  • Los punteros sin formato no poseen memoria (si ha seguido las pautas), por lo que no puede filtrarlos.
  • Si se pregunta cómo va a pasar las direcciones iniciales de las secuencias de valores en la memoria, lo hará con un lapso ; No se necesita un puntero sin formato.
  • Si realmente necesita un "puntero" propio, utilice los punteros inteligentes de la biblioteca estándar de C ++ : no pueden filtrarse y son bastante eficientes (aunque el ABI puede interferir en eso). Alternativamente, puede pasar la propiedad a través de los límites del alcance con "punteros de propietario" . Estos son poco comunes y deben usarse explícitamente; pero cuando se adoptan, permiten una buena comprobación estática de fugas.

"¿Ah sí? Pero ¿qué pasa con ...

... si solo escribo código de la forma en que solíamos escribir C ++ en los viejos tiempos? "

De hecho, podría ignorar todas las pautas y escribir código de aplicación con fugas, y se compilará y ejecutará (y se filtrará), como siempre.

Pero no es una situación de "simplemente no hagas eso", donde se espera que el desarrollador sea virtuoso y ejerza mucho autocontrol; simplemente no es más simple escribir código no conforme, ni es más rápido de escribir, ni tiene mejor rendimiento. Gradualmente, también se volverá más difícil de escribir, ya que se enfrentaría a un "desajuste de impedancia" cada vez mayor con lo que el código conforme proporciona y espera.

... si yo reintrepret_cast? O hacer aritmética puntero complejo? ¿O otros trucos similares?

De hecho, si te lo propones, puedes escribir código que arruine las cosas a pesar de jugar bien con las pautas. Pero:

  1. Raramente haría esto (en términos de lugares en el código, no necesariamente en términos de fracción de tiempo de ejecución)
  2. Solo harías esto intencionalmente, no accidentalmente.
  3. Hacerlo se destacará en una base de código conforme a las pautas.
  4. Es el tipo de código en el que omitirías el GC en otro idioma de todos modos.

... desarrollo de la biblioteca?

Si usted es un desarrollador de bibliotecas C ++, entonces escribe código inseguro que involucra punteros sin procesar, y debe codificar con cuidado y responsabilidad, pero estos son fragmentos de código independientes escritos por expertos (y lo más importante, revisados ​​por expertos).


Entonces, es como dijo Bjarne: en general, no hay motivación para recolectar basura, ya que todos se aseguran de no producir basura. GC se está convirtiendo en un problema con C ++.

Eso no quiere decir que GC no sea un problema interesante para ciertas aplicaciones específicas, cuando se desea emplear estrategias de asignación y desasignación personalizadas. Para aquellos que deseen asignación personalizada y desasignación, no un GC a nivel de idioma.


Bueno, sí (necesita GC) si está moliendo cadenas. Imagine que tiene grandes conjuntos de cadenas (piense en cientos de megabytes) que está construyendo poco a poco, luego procesando y reconstruyendo en diferentes longitudes, eliminando las que no se utilizan, combinando otras, etc. sé porque he tenido que cambiar a idiomas de alto nivel para hacer frente. (Por supuesto, también podría construir su propio GC).
www-0av-Com

2
@ user1863152: Ese es un caso en el que sería útil un asignador personalizado. Todavía no necesita un GC integral del lenguaje ...
einpoklum

einpoklum: cierto. Es solo caballo para cursos. Mi requisito era procesar dinámicamente el cambio de galones de información de pasajeros de transporte. Tema fascinante. Realmente se reduce a la filosofía del software.
www-0av-Com

GC, como han descubierto el mundo de Java y .NET, finalmente tiene un problema enorme: no se escala. Cuando tenga miles de millones de objetos vivos en la memoria, como lo hacemos hoy en día con cualquier software no trivial, tendrá que comenzar a escribir código para ocultar cosas del GC. Es una carga tener GC en Java y .NET.
Zach vio el

10

La idea detrás de C ++ era que no pagaría ningún impacto en el rendimiento de las funciones que no utiliza. Por lo tanto, agregar la recolección de basura habría significado que algunos programas se ejecuten directamente en el hardware como lo hace C y algunos dentro de algún tipo de máquina virtual en tiempo de ejecución.

Nada le impide utilizar algún tipo de punteros inteligentes que están vinculados a algún mecanismo de recolección de basura de terceros. Me parece recordar que Microsoft hizo algo así con COM y no funcionó bien.


2
No creo que GC requiera una VM. El compilador podría agregar código a todas las operaciones de puntero para actualizar un estado global, mientras que un subproceso separado se ejecuta en segundo plano eliminando objetos según sea necesario.
user83255

3
Estoy de acuerdo. No necesitas una máquina virtual, pero en el momento en que comienzas a tener algo que gestiona tu memoria de esa manera en el fondo, creo que has dejado los "cables eléctricos" reales y tienes una especie de situación de VM.
Uri,


4

Uno de los principios fundamentales detrás del lenguaje C original es que la memoria está compuesta de una secuencia de bytes, y el código solo necesita preocuparse por lo que esos bytes significan en el momento exacto en que se están utilizando. Modern C permite a los compiladores imponer restricciones adicionales, pero C incluye, y C ++ conserva, la capacidad de descomponer un puntero en una secuencia de bytes, ensamblar cualquier secuencia de bytes que contengan los mismos valores en un puntero y luego usar ese puntero para acceder al objeto anterior.

Si bien esa capacidad puede ser útil, o incluso indispensable, en algunos tipos de aplicaciones, un lenguaje que incluya esa capacidad será muy limitado en su capacidad para admitir cualquier tipo de recolección de basura útil y confiable. Si un compilador no sabe todo lo que se ha hecho con los bits que formaron un puntero, no tendrá forma de saber si podría existir información suficiente para reconstruir el puntero en algún lugar del universo. Dado que sería posible almacenar esa información de manera que la computadora no pudiera acceder incluso si supiera de ellos (por ejemplo, los bytes que componen el puntero podrían haberse mostrado en la pantalla el tiempo suficiente para que alguien escriba en una hoja de papel), puede ser literalmente imposible para una computadora saber si un puntero podría usarse en el futuro.

Una peculiaridad interesante de muchos marcos de recolección de basura es que una referencia de objeto no está definida por los patrones de bits contenidos en ella, sino por la relación entre los bits contenidos en la referencia de objeto y otra información contenida en otro lugar. En C y C ++, si el patrón de bits almacenado en un puntero identifica un objeto, ese patrón de bits identificará ese objeto hasta que el objeto se destruya explícitamente. En un sistema GC típico, un objeto puede estar representado por un patrón de bits 0x1234ABCD en un momento dado, pero el siguiente ciclo GC puede reemplazar todas las referencias a 0x1234ABCD con referencias a 0x4321BABE, con lo cual el objeto estaría representado por el último patrón. Incluso si uno mostrara el patrón de bits asociado con una referencia de objeto y luego lo leyera desde el teclado,


Ese es un punto realmente bueno, recientemente robé algunos bits de mis punteros porque de lo contrario habría cantidades estúpidas de errores de caché.
Pasador A más tardar el

@PasserBy: Me pregunto cuántas aplicaciones que usan punteros de 64 bits se beneficiarían más si usan punteros escalados de 32 bits como referencias de objetos, o si mantienen casi todo en 4GiB de espacio de direcciones y usan objetos especiales para almacenar / recuperar datos de alta de almacenamiento de alta velocidad más allá? Las máquinas tienen suficiente RAM para que el consumo de RAM de los punteros de 64 bits no importe, excepto que engullen el doble de caché que los punteros de 32 bits.
supercat

3

Toda la conversación técnica está complicando demasiado el concepto.

Si coloca GC en C ++ para toda la memoria automáticamente, considere algo como un navegador web. El navegador web debe cargar un documento web completo Y ejecutar scripts web. Puede almacenar variables de script web en el árbol de documentos. En un documento GRANDE en un navegador con muchas pestañas abiertas, significa que cada vez que el GC debe hacer una colección completa, también debe escanear todos los elementos del documento.

En la mayoría de las computadoras esto significa que ocurrirán FALLAS DE PÁGINA. Entonces, la razón principal para responder la pregunta es que ocurrirán FALLAS DE PÁGINA. Lo sabrá cuando su PC comience a hacer mucho acceso al disco. Esto se debe a que el GC debe tocar mucha memoria para probar punteros no válidos. Cuando tiene una aplicación de buena fe que utiliza mucha memoria, tener que escanear todos los objetos de cada colección es un caos debido a los FALLOS DE PÁGINA. Un error de página es cuando la memoria virtual necesita volver a leerse en la RAM desde el disco.

Entonces, la solución correcta es dividir una aplicación en las partes que necesitan GC y las partes que no. En el caso del ejemplo del navegador web anterior, si el árbol de documentos se asignó con malloc, pero el JavaScript se ejecutó con GC, cada vez que el GC se activa solo escanea una pequeña porción de memoria y todos los elementos de la memoria PAGADOS. no es necesario volver a paginar el árbol de documentos.

Para comprender mejor este problema, busque en la memoria virtual y cómo se implementa en las computadoras. Se trata del hecho de que 2GB están disponibles para el programa cuando en realidad no hay tanta RAM. En las computadoras modernas con 2GB de RAM para un sistema de 32Bt, no es un problema, siempre que se esté ejecutando un solo programa.

Como ejemplo adicional, considere una colección completa que debe rastrear todos los objetos. Primero debe escanear todos los objetos accesibles a través de las raíces. Segundo escanee todos los objetos visibles en el paso 1. Luego escanee los destructores en espera. Luego, vaya a todas las páginas nuevamente y apague todos los objetos invisibles. Esto significa que muchas páginas pueden intercambiarse y volverse varias veces.

Entonces, mi respuesta para resumir es que el número de FALLAS DE PÁGINA que ocurren como resultado de tocar toda la memoria hace que el GC completo para todos los objetos en un programa sea inviable y, por lo tanto, el programador debe ver el GC como una ayuda para cosas como scripts y el trabajo de la base de datos, pero haga cosas normales con la gestión manual de la memoria.

Y la otra razón muy importante, por supuesto, son las variables globales. Para que el recopilador sepa que hay un puntero de variable global en el GC, necesitaría palabras clave específicas y, por lo tanto, el código C ++ existente no funcionaría.


3

RESPUESTA CORTA: No sabemos cómo recolectar basura de manera eficiente (con poco tiempo y sobrecarga de espacio) y correctamente todo el tiempo (en todos los casos posibles).

RESPUESTA LARGA: Al igual que C, C ++ es un lenguaje de sistemas; esto significa que se usa cuando está escribiendo código del sistema, por ejemplo, sistema operativo. En otras palabras, C ++ está diseñado, al igual que C, con el mejor rendimiento posible como objetivo principal. El lenguaje estándar no agregará ninguna característica que pueda obstaculizar el objetivo de rendimiento.

Esto detiene la pregunta: ¿por qué la recolección de basura dificulta el rendimiento? La razón principal es que, cuando se trata de implementación, nosotros [los informáticos] no sabemos cómo recolectar basura con una sobrecarga mínima, en todos los casos. Por lo tanto, es imposible para el compilador de C ++ y el sistema de tiempo de ejecución realizar una recolección de basura de manera eficiente todo el tiempo. Por otro lado, un programador de C ++, debe conocer su diseño / implementación y es la mejor persona para decidir cómo hacer la mejor recolección de basura.

Por último, si el control (hardware, detalles, etc.) y el rendimiento (tiempo, espacio, potencia, etc.) no son las restricciones principales, entonces C ++ no es la herramienta de escritura. Otro lenguaje podría servir mejor y ofrecer más administración de tiempo de ejecución [oculto], con la sobrecarga necesaria.


3

Cuando comparamos C ++ con Java, vemos que C ++ no fue diseñado teniendo en cuenta la recolección implícita de basura, mientras que Java sí.

Tener cosas como punteros arbitrarios en C-Style no solo es malo para las implementaciones de GC, sino que también destruiría la compatibilidad con versiones anteriores para una gran cantidad de C ++: código heredado.

Además de eso, C ++ es un lenguaje destinado a ejecutarse como ejecutable independiente en lugar de tener un entorno complejo de tiempo de ejecución.

Con todo: Sí, podría ser posible agregar Garbage Collection a C ++, pero en aras de la continuidad, es mejor no hacerlo.


1
Liberar memoria y ejecutar destructores son problemas completamente separados. (Java no tiene destructores, que es un PITA). GC libera memoria, no ejecuta dtors.
curioso

0

Principalmente por dos razones:

  1. Porque no necesita uno (en mi humilde opinión)
  2. Porque es bastante incompatible con RAII, que es la piedra angular de C ++

C ++ ya ofrece administración manual de memoria, asignación de pila, RAII, contenedores, punteros automáticos, punteros inteligentes ... Eso debería ser suficiente. Los recolectores de basura son para programadores perezosos que no quieren pasar 5 minutos pensando en quién debería poseer qué objetos o cuándo deberían liberarse los recursos. Así no es como hacemos las cosas en C ++.


Existen numerosos algoritmos (más nuevos) que son inherentemente difíciles de implementar sin recolección de basura. El tiempo pasó. La innovación también proviene de nuevos conocimientos que coinciden bien con los lenguajes de alto nivel (recolección de basura). Intente hacer una copia de seguridad de cualquiera de estos a C ++ libre de GC, notará los baches en el camino. (Sé que debería dar ejemplos, pero estoy un poco apurado en este momento. Lo siento. Uno en lo que puedo pensar ahora gira en torno a estructuras de datos persistentes, donde el recuento de referencias no funcionará).
BitTickler

0

Imponer la recolección de basura es realmente un cambio de paradigma de bajo nivel a alto nivel.

Si observa la forma en que las cadenas se manejan en un lenguaje con recolección de basura, encontrará que SOLO permiten funciones de manipulación de cadenas de alto nivel y no permiten el acceso binario a las cadenas. En pocas palabras, todas las funciones de cadena primero verifican los punteros para ver dónde está la cadena, incluso si solo está extrayendo un byte. Entonces, si está haciendo un ciclo que procesa cada byte en una cadena en un idioma con recolección de basura, debe calcular la ubicación base más el desplazamiento para cada iteración, porque no puede saber cuándo se movió la cadena. Luego tienes que pensar en montones, pilas, hilos, etc.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.