Por que o C ++ não possui um coletor de lixo?


270

Não estou fazendo essa pergunta por causa dos méritos da coleta de lixo antes de tudo. Minha principal razão para perguntar isso é que eu sei que Bjarne Stroustrup disse que o C ++ terá um coletor de lixo em algum momento.

Com isso dito, por que não foi adicionado? Já existem alguns coletores de lixo para C ++. Essa é apenas uma daquelas coisas do tipo "mais fácil dizer do que fazer"? Ou há outros motivos para não ter sido adicionado (e não será adicionado no C ++ 11)?

Links cruzados:

Apenas para esclarecer, entendo os motivos pelos quais o C ++ não tinha um coletor de lixo quando foi criado. Gostaria de saber por que o colecionador não pode ser adicionado.


26
Este é um dos dez principais mitos sobre C ++ que os inimigos sempre trazem à tona. A coleta de lixo não é "incorporada", mas existem várias maneiras fáceis de fazer isso em C ++. Postando um comentário porque os outros já responderam melhor do que eu abaixo :)
davr

5
Mas esse é o ponto principal de não ser incorporado, você deve fazer isso sozinho. Realidade de alto a baixo: embutido, biblioteca, caseiro. Eu mesmo uso C ++, e definitivamente não sou odioso, porque é a melhor linguagem do mundo. Mas o gerenciamento dinâmico de memória é uma dor.
QBziZ

4
@ David - Eu não sou um odiador de C ++, nem estou sequer tentando argumentar que C ++ precisa de um coletor de lixo. Estou perguntando, porque sei que Bjarne Stroustrup disse que será adicionado e estava curioso sobre quais eram as razões para não implementá-lo.
Jason Baker

1
Este artigo O Boehm Collector para C e C ++ do Dr. Dobbs descreve um coletor de lixo de código aberto que pode ser usado com C e C ++. Ele discute alguns dos problemas que surgem com o uso de um coletor de lixo com destruidores de C ++ e com a Biblioteca Padrão C.
Richard Chambers

1
@rogerdpack: Mas agora não é tão útil (veja minha resposta ...), por isso é improvável que as implementações invistam em ter uma.
Einpoklum

Respostas:


160

A coleta de lixo implícita poderia ter sido adicionada, mas simplesmente não foi suficiente. Provavelmente devido não apenas a complicações de implementação, mas também porque as pessoas não conseguem chegar a um consenso geral com rapidez suficiente.

Uma citação do próprio Bjarne Stroustrup:

Eu esperava que um coletor de lixo que pudesse ser opcionalmente ativado fizesse parte do C ++ 0x, mas havia problemas técnicos suficientes que eu tenho que resolver com apenas uma especificação detalhada de como esse coletor se integra ao resto do idioma , se fornecido. Como é o caso de essencialmente todos os recursos do C ++ 0x, existe uma implementação experimental.

Há uma boa discussão sobre o tópico aqui .

Visao geral:

O C ++ é muito poderoso e permite que você faça quase tudo. Por esse motivo, não impõe automaticamente muitas coisas que podem afetar o desempenho. A coleta de lixo pode ser facilmente implementada com ponteiros inteligentes (objetos que agrupam ponteiros com uma contagem de referência, que são excluídos automaticamente quando a contagem de referência atinge 0).

O C ++ foi criado com os concorrentes em mente que não tinham coleta de lixo. A eficiência foi a principal preocupação da qual o C ++ teve que afastar as críticas em comparação com o C e outros.

Existem 2 tipos de coleta de lixo ...

Coleta de lixo explícita:

C ++ 0x terá coleta de lixo por meio de ponteiros criados com shared_ptr

Se você quiser, poderá usá-lo; se não quiser, não será obrigado a usá-lo.

Atualmente, você também pode usar o boost: shared_ptr, se não quiser esperar pelo C ++ 0x.

Coleta de lixo implícita:

Porém, ele não possui coleta de lixo transparente. Porém, será um ponto de foco para futuras especificações do C ++.

Por que o Tr1 não possui coleta de lixo implícita?

Há muitas coisas que o tr1 do C ++ 0x deveria ter tido, Bjarne Stroustrup em entrevistas anteriores afirmou que o tr1 não tinha tanto quanto ele gostaria.


71
Eu me tornaria odioso se o C ++ me obrigasse a coletar lixo! Por que as pessoas não podem usar smart_ptr's? Como você faria bifurcação de baixo nível no estilo Unix, com um coletor de lixo no caminho? Outras coisas seriam afetadas, como rosqueamento. O Python possui seu bloqueio global de intérpretes principalmente por causa de sua coleta de lixo (consulte Cython). Mantenha-o fora do C / C ++, obrigado.
precisa

26
@ unixman83: O principal problema com a coleta de lixo contada por referência (ie std::shared_ptr) são as referências cíclicas, que causam vazamento de memória. Portanto, você deve usar std::weak_ptrcom cuidado para interromper os ciclos, o que é confuso. Marcar e varrer o estilo GC não tem esse problema. Não há incompatibilidade inerente entre threading / bifurcação e coleta de lixo. Java e C # têm multithreading preemptivo de alto desempenho e um coletor de lixo. Há problemas relacionados a aplicativos em tempo real e a um coletor de lixo, pois a maioria dos coletores de lixo precisa parar o mundo para rodar.
Andrew Tomazos

9
"O principal problema com a coleta de lixo contada por referência (ie std::shared_ptr) são as referências cíclicas" e o desempenho péssimo, o que é irônico, porque o melhor desempenho geralmente é a justificativa para o uso de C ++ ... flyingfrogblog.blogspot.co.uk/2011/01/…
Jon Harrop

11
"Como você faria bifurcação no estilo Unix de baixo nível". Da mesma forma que idiomas do GC, como OCaml, fazem isso há ~ 20 anos ou mais.
Jon Harrop

9
"O Python tem seu bloqueio global de intérpretes principalmente por causa de sua coleta de lixo". Argumento de Strawman. Java e .NET possuem GCs, mas também não possuem bloqueios globais.
Jon Harrop

149

Para adicionar ao debate aqui.

Existem problemas conhecidos com a coleta de lixo, e entendê-los ajuda a entender por que não há nenhum em C ++.

1. desempenho?

A primeira reclamação geralmente é sobre desempenho, mas a maioria das pessoas não percebe do que está falando. Conforme ilustrado pelo Martin Beckettproblema, pode não ser o desempenho em si, mas a previsibilidade do desempenho.

Atualmente, existem duas famílias de GC amplamente implantadas:

  • Tipo de marcação e varredura
  • Tipo de contagem de referência

A Mark And Sweepé mais rápido (menos impacto no desempenho geral), mas sofre de uma "congelar o mundo" síndrome: ou seja, quando os GC entra em ação, tudo o resto está parado até que o GC tem feito a sua limpeza. Se você deseja construir um servidor que responda em alguns milissegundos ... algumas transações não corresponderão às suas expectativas :)

O problema de Reference Countingé diferente: a contagem de referência aumenta a sobrecarga, especialmente em ambientes com vários segmentos, porque você precisa ter uma contagem atômica. Além disso, há o problema dos ciclos de referência, portanto, você precisa de um algoritmo inteligente para detectar e eliminá-los (geralmente implementado por um "congelamento do mundo" também, embora seja menos frequente). Em geral, a partir de hoje, esse tipo (embora normalmente seja mais responsivo ou melhor, congela com menos frequência) é mais lento que o Mark And Sweep.

Eu vi um artigo de implementadores da Eiffel que estavam tentando implementar um Reference CountingGarbage Collector que teria um desempenho global semelhante Mark And Sweepsem o aspecto "Congelar o mundo". Exigia um thread separado para o GC (típico). O algoritmo era um pouco assustador (no final), mas o artigo fez um bom trabalho ao introduzir os conceitos um de cada vez e mostrar a evolução do algoritmo da versão "simples" para a versão completa. Leitura recomendada se eu pudesse colocar minhas mãos de volta no arquivo PDF ...

2. Aquisição de recursos é inicialização (RAII)

É um idioma comum, C++pois você agrupará a propriedade dos recursos em um objeto para garantir que eles sejam liberados corretamente. É usado principalmente para memória, pois não temos coleta de lixo, mas também é útil para muitas outras situações:

  • bloqueios (multithread, identificador de arquivo, ...)
  • conexões (a um banco de dados, outro servidor, ...)

A idéia é controlar adequadamente a vida útil do objeto:

  • deve estar vivo enquanto você precisar
  • deve ser morto quando você terminar com isso

O problema do GC é que, se ele ajuda com o primeiro e garante que mais tarde ... esse "final" pode não ser suficiente. Se você liberar um bloqueio, realmente gostaria que ele fosse liberado agora, para que não bloqueie mais chamadas!

Os idiomas com GC têm duas soluções alternativas:

  • não use GC quando a alocação de pilha for suficiente: normalmente é para problemas de desempenho, mas, no nosso caso, realmente ajuda, pois o escopo define a vida útil
  • usingconstruir ... mas é RAII explícito (fraco) enquanto estiver em C ++ RAII está implícito, de modo que o usuário NÃO PODE involuntariamente cometer o erro (omitindo a usingpalavra - chave)

3. Ponteiros inteligentes

Ponteiros inteligentes geralmente aparecem como uma bala de prata para lidar com a memória C++. Muitas vezes ouvi falar: afinal, não precisamos de GC, pois temos indicadores inteligentes.

Não se poderia estar mais errado.

Ponteiros inteligentes ajudam: auto_ptre unique_ptrusam conceitos RAII, extremamente úteis. Eles são tão simples que você pode escrevê-los sozinho com bastante facilidade.

Quando é necessário compartilhar a propriedade, porém, fica mais difícil: você pode compartilhar entre vários threads e existem alguns problemas sutis no manuseio da contagem. Portanto, alguém naturalmente vai em direção shared_ptr.

É ótimo, é para isso que serve o Boost, afinal, mas não é uma bala de prata. Na verdade, o principal problema shared_ptré que ele emula um GC implementado por, Reference Countingmas você precisa implementar a detecção de ciclo sozinho.

É claro que existe isso weak_ptr, mas infelizmente eu já vi vazamentos de memória, apesar do uso shared_ptrdevido a esses ciclos ... e quando você está em um ambiente com vários threads, é extremamente difícil de detectar!

4. Qual é a solução?

Não existe uma bala de prata, mas como sempre, é definitivamente viável. Na ausência de GC, é preciso ser claro quanto à propriedade:

  • prefira ter um único proprietário ao mesmo tempo, se possível
  • caso contrário, verifique se o diagrama de classes não possui nenhum ciclo referente à propriedade e quebre-os com a aplicação sutil de weak_ptr

Portanto, seria ótimo ter um GC ... no entanto, não é um problema trivial. E nesse meio tempo, só precisamos arregaçar as mangas.


2
Eu gostaria de poder aceitar duas respostas! Isso é ótimo. Uma coisa a salientar, em relação ao desempenho, o GC executado em um encadeamento separado é realmente bastante comum (é usado em Java e .Net). Concedido, isso pode não ser aceitável em sistemas embarcados.
22610 Jason Baker

14
Apenas dois tipos? Que tal copiar colecionadores? Coletores geracionais? Coletores simultâneos variados (incluindo a esteira dura em tempo real de Baker)? Vários colecionadores híbridos? Cara, a pura ignorância na indústria desse campo me surpreende às vezes.
APENAS MINHA OPINIÃO correta

12
Eu disse que havia apenas 2 tipos? Eu disse que havia dois que foram amplamente implantados. Até onde eu sei, Python, Java e C # agora usam algoritmos Mark e Sweep (o Java costumava ter um algoritmo de contagem de referência). Para ser ainda mais preciso, parece-me que o C # usa o GC geracional para ciclos menores, Mark And Sweep para ciclos principais e Copiando para combater a fragmentação da memória; embora eu argumentasse que o coração do algoritmo é Mark And Sweep. Você conhece alguma linguagem convencional que use outra tecnologia? Fico sempre feliz em aprender.
Matthieu M.

3
Você acabou de nomear um idioma convencional que usava três.
APENAS MINHA OPINIÃO correta

3
A principal diferença é que o GC Geracional e Incremental não precisa parar o mundo para funcionar, e você pode fazê-lo funcionar em sistemas de encadeamento único sem muita sobrecarga, executando ocasionalmente uma iteração do percurso da árvore ao acessar os ponteiros do GC (o fator pode ser determinado pelo número de novos nós, juntamente com uma previsão básica da necessidade de coletar). Você pode levar o GC ainda mais longe, incluindo dados sobre onde ocorreu a criação / modificação do nó no código, o que pode permitir que você melhore suas previsões e obtenha o Escape Analysis gratuitamente com ele.
Keldon Alleyne

56

Que tipo? deve ser otimizado para controladores de máquinas de lavar, telefones celulares, estações de trabalho ou supercomputadores?
Deve priorizar a capacidade de resposta da GUI ou o carregamento do servidor?
deveria usar muita memória ou muita CPU?

C / c ++ é usado em muitas circunstâncias diferentes. Eu suspeito que algo como impulsionar ponteiros inteligentes será suficiente para a maioria dos usuários

Editar - Os coletores de lixo automáticos não são um problema de desempenho (você sempre pode comprar mais servidores); é uma questão de desempenho previsível.
Não saber quando o GC vai entrar é como empregar um piloto de avião narcoléptico, na maioria das vezes eles são ótimos - mas quando você realmente precisa de capacidade de resposta!


6
Definitivamente, entendo seu ponto de vista, mas me sinto compelido a perguntar: o Java não é usado em quase tantos aplicativos?
Jason Baker

35
Não. Java não é adequado para aplicativos de alto desempenho, pelo simples motivo de que ele não possui garantias de desempenho na mesma extensão que o C ++. Então você o encontrará em um telefone celular, mas não em um comutador ou supercomputador.
Zathrus 29/09/08

11
Você sempre pode comprar mais servidor, mas nem sempre pode comprar mais CPU para o telefone celular que já está no bolso do cliente!
Crashworks

8
Java fez muito aumento de desempenho na eficiência da CPU. O problema realmente intratável é o uso de memória, o Java é inerentemente menos eficiente em termos de memória que o C ++. E essa ineficiência se deve ao fato de ser lixo coletado. A coleta de lixo não pode ser rápida e eficiente em termos de memória, fato que se torna óbvio se você examinar a rapidez com que os algoritmos de GC funcionam.
Nate CK

2
O @Zathrus java pode ganhar com a taxa de transferência b / c do jit otimizador, embora não com latência (tempo real em tempo real), e certamente não com pegada de memória.
Gtrak #

34

Uma das maiores razões pelas quais o C ++ não incorporou a coleta de lixo é que fazer com que a coleta seja agradável com os destruidores é muito, muito difícil. Até onde eu sei, ninguém realmente sabe como resolvê-lo completamente ainda. Existem muitos problemas para lidar com:

  • vida útil determinística dos objetos (a contagem de referência fornece isso, mas o GC não. Embora possa não ser tão importante assim).
  • o que acontece se um destruidor lança quando o objeto está sendo coletado de lixo? A maioria dos idiomas ignora essa exceção, pois não há realmente nenhum bloco de captura para transportá-lo, mas provavelmente essa não é uma solução aceitável para C ++.
  • Como ativar / desativar? Naturalmente, provavelmente seria uma decisão em tempo de compilação, mas o código que é escrito para o GC versus o código que é escrito para o NOT GC será muito diferente e provavelmente incompatível. Como você reconcilia isso?

Estes são apenas alguns dos problemas enfrentados.


17
O GC e os destruidores são um problema resolvido, por uma boa saída de Bjarne. Os destruidores não são executados durante o GC, porque esse não é o objetivo do GC. O GC em C ++ existe para criar a noção de memória infinita , não outros recursos infinitos.
MSalters 29/09/08

2
Se os destruidores não executam, isso altera completamente a semântica da linguagem. Eu acho que, no mínimo, você precisaria de uma nova palavra-chave "gcnew" ou algo assim, para permitir explicitamente que esse objeto seja GC'ed (e, portanto, você não deve usá-lo para agrupar recursos além da memória).
Greg Rogers

7
Este é um argumento falso. Como o C ++ possui gerenciamento explícito de memória, você precisa descobrir quando cada objeto deve ser liberado. Com o GC, não é pior; em vez disso, o problema é reduzido a descobrir quando certos objetos são liberados, ou seja, aqueles que requerem considerações especiais ao serem excluídos. A experiência de programação em Java e C # revela que a grande maioria dos objetos não requer considerações especiais e pode ser deixada com segurança para o GC. Como se vê, uma das principais funções dos destruidores no C ++ é liberar objetos filho, que o GC manipula para você automaticamente.
Nate CK

2
@ NateC-K: Uma coisa que é aprimorada em GC vs não-GC (talvez a maior coisa) é a capacidade de um sistema sólido de GC garantir que todas as referências continuem apontando para o mesmo objeto enquanto a referência existir. Chamar Disposeum objeto pode torná-lo impossível, mas as referências que apontaram para o objeto quando ele estava vivo continuarão a fazê-lo depois que ele estiver morto. Por outro lado, em sistemas que não são da GC, os objetos podem ser excluídos enquanto as referências existem, e raramente existe um limite para o caos que pode ser causado se uma dessas referências for usada.
Supercat 01/10

22

Embora essa seja uma pergunta antiga , ainda há um problema que não vejo ninguém abordando: a coleta de lixo é quase impossível de especificar.

Em particular, o padrão C ++ é bastante cuidadoso ao especificar a linguagem em termos de comportamento observável externamente, em vez de como a implementação alcança esse comportamento. No caso da coleta de lixo, no entanto, não é virtualmente nenhum externamente comportamento observável.

A idéia geral da coleta de lixo é que ela faça uma tentativa razoável de garantir que uma alocação de memória seja bem-sucedida. Infelizmente, é essencialmente impossível garantir que qualquer alocação de memória seja bem-sucedida, mesmo se você tiver um coletor de lixo em operação. Isso é verdade até certo ponto em qualquer caso, mas particularmente no caso do C ++, porque (provavelmente) não é possível usar um coletor de cópias (ou qualquer coisa semelhante) que mova objetos na memória durante um ciclo de coleta.

Se você não pode mover objetos, não pode criar um espaço de memória único e contíguo do qual fazer suas alocações - e isso significa que sua pilha (ou armazenamento gratuito ou o que você preferir chamar) pode e provavelmente irá , tornam-se fragmentados ao longo do tempo. Isso, por sua vez, pode impedir que uma alocação seja bem-sucedida, mesmo quando houver mais memória livre do que a quantidade solicitada.

Embora seja possível obter alguma garantia que diga (em essência) que, se você repetir exatamente o mesmo padrão de alocação repetidamente e tiver sido bem-sucedido na primeira vez, continuará tendo êxito nas iterações subsequentes, desde que a memória alocada tornou-se inacessível entre iterações. Essa é uma garantia tão fraca que é essencialmente inútil, mas não vejo nenhuma esperança razoável de fortalecê-la.

Mesmo assim, é mais forte do que o proposto para C ++. A proposta anterior [aviso: PDF] (que foi descartado) não garante nada. Em 28 páginas da proposta, o que você colocou no caminho do comportamento observável externamente foi uma nota única (não normativa) dizendo:

[Nota: Para programas de coleta de lixo, uma implementação hospedada de alta qualidade deve tentar maximizar a quantidade de memória inacessível que ela recupera. - end note]

Pelo menos para mim, isso levanta uma questão séria sobre o retorno do investimento. Vamos quebrar o código existente (ninguém sabe exatamente quanto, mas definitivamente um pouco), colocar novos requisitos em implementações e novas restrições no código, e o que recebemos em troca é possivelmente nada?

Mesmo na melhor das hipóteses, o que obtemos são programas que, com base em testes com Java , provavelmente exigirão cerca de seis vezes mais memória para rodar na mesma velocidade que agora. Pior ainda, a coleta de lixo fazia parte do Java desde o início - o C ++ impõe restrições suficientes ao coletor de lixo, o que quase certamente terá uma relação custo / benefício ainda pior (mesmo se formos além do que a proposta garantia e supomos que haveria) algum benefício).

Eu resumiria a situação matematicamente: essa é uma situação complexa. Como qualquer matemático sabe, um número complexo tem duas partes: real e imaginário. Parece-me que o que temos aqui são custos reais, mas benefícios que são (pelo menos principalmente) imaginários.


Eu diria que, mesmo que alguém especifique que, para uma operação adequada, todos os objetos devem ser excluídos e apenas objetos que foram excluídos seriam elegíveis para coleta, o suporte do compilador para coleta de lixo de rastreamento de referência ainda pode ser útil, pois esse idioma poderia garantir que o uso de um ponteiro excluído (referência) seria garantido para interceptar, em vez de causar Comportamento indefinido.
Supercat 24/07

2
Mesmo em Java, o GC não está realmente especificado para fazer algo útil no AFAIK. Pode chamar freepara você (onde eu quero dizer freeanálogo ao idioma C). Mas o Java nunca garante chamar finalizadores ou algo assim. De fato, o C ++ faz muito mais que o Java para executar gravações de banco de dados de confirmação, liberando identificadores de arquivos e assim por diante. Java alega ter "GC", mas os desenvolvedores de Java precisam ligar meticulosamente close()o tempo todo e precisam estar muito conscientes do gerenciamento de recursos, tomando cuidado para não ligar close()muito cedo ou muito tarde. C ++ nos liberta disso. ... (continuação)
Aaron McDaid 16/10

2
.. meu comentário há pouco não se destina a criticar Java. Estou apenas observando que o termo "coleta de lixo" é um termo muito estranho - significa muito menos do que as pessoas pensam e, portanto, é difícil discuti-lo sem esclarecer o que significa.
Aaron McDaid

@AaronMcDaid É verdade que o GC não ajuda em nada com recursos que não sejam de memória. Felizmente, esses recursos são alocados muito raramente quando comparados à memória. Além disso, mais de 90% deles podem ser liberados no método que os alocou, então o try (Whatever w=...) {...}resolve (e você recebe um aviso quando se esquece). Os restantes também são problemáticos com o RAII. Chamar close()"o tempo todo" significa talvez uma vez por dezenas de mil linhas, portanto não é tão ruim assim, enquanto a memória é alocada quase em todas as linhas Java.
Maaartinus

15

Se você deseja coleta automática de lixo, existem bons coletores de lixo comerciais e de domínio público para C ++. Para aplicativos em que a coleta de lixo é adequada, o C ++ é uma excelente linguagem de coleta de lixo com um desempenho que se compara favoravelmente com outras linguagens de coleta de lixo. Consulte A linguagem de programação C ++ (4ª edição) para obter uma discussão sobre a coleta automática de lixo no C ++. Veja também Hans-J. Site da Boehm para coleta de lixo em C e C ++ ( arquivo morto ).

Além disso, o C ++ suporta técnicas de programação que permitem que o gerenciamento de memória seja seguro e implícito sem um coletor de lixo . Considero a coleta de lixo uma última opção e uma maneira imperfeita de lidar com o gerenciamento de recursos. Isso não significa que nunca seja útil, apenas que existem abordagens melhores em muitas situações.

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

Quanto ao porquê de ele não tê-lo construído em, Se bem me lembro que foi inventado antes de GC foi a coisa , e eu não acredito que a linguagem poderia ter tido GC por várias razões (compatibilidade IE para trás com C)

Espero que isto ajude.


"com um desempenho que se compara favoravelmente com outros idiomas de coleta de lixo". Citação?
Jon Harrop

1
Meu link foi quebrado. Eu escrevi esta resposta há 5 anos.
Rayne

1
Ok, eu esperava alguma verificação independente dessas alegações, ou seja, não por Stroustrup ou Boehm. :-)
Jon Harrop

12

Stroustrup fez alguns bons comentários sobre isso na conferência Going Native de 2013.

Basta pular para cerca de 25m50s neste vídeo . (Eu recomendo assistir o vídeo inteiro, na verdade, mas isso pula para as coisas sobre coleta de lixo.)

Quando você tem uma linguagem realmente boa que facilita (e é seguro, previsível, fácil de ler e fácil de ensinar) lidar com objetos e valores de maneira direta, evitando o uso (explícito) do pilha, então você nem quer coleta de lixo.

Com o C ++ moderno e o material que temos no C ++ 11, a coleta de lixo não é mais desejável, exceto em circunstâncias limitadas. De fato, mesmo que um bom coletor de lixo seja incorporado a um dos principais compiladores de C ++, acho que ele não será usado com muita frequência. Será mais fácil , e não mais difícil, evitar o GC.

Ele mostra este exemplo:

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

Isso não é seguro em C ++. Mas também não é seguro em Java! No C ++, se a função retornar mais cedo, deletenunca será chamada. Mas se você teve uma coleta de lixo completa, como em Java, você apenas recebe uma sugestão de que o objeto será destruído "em algum momento no futuro" ( atualização: é ainda pior que isso. Java nãoprometa ligar para o finalizador de todos os tempos - talvez nunca seja chamado). Isso não é suficiente se o Gadget tiver um identificador de arquivo aberto, uma conexão com um banco de dados ou dados que você armazenou em buffer para gravação em um banco de dados posteriormente. Queremos que o Gadget seja destruído assim que terminar, para liberar esses recursos o mais rápido possível. Você não quer que seu servidor de banco de dados esteja enfrentando milhares de conexões de banco de dados que não são mais necessárias - ele não sabe que seu programa terminou de funcionar.

Então, qual é a solução? Existem algumas abordagens. A abordagem óbvia, que você usará para a grande maioria dos seus objetos é:

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;
}

Isso leva menos caracteres para digitar. Não está newatrapalhando. Não requer que você digite Gadgetduas vezes. O objeto é destruído no final da função. Se é isso que você deseja, isso é muito intuitivo. Gadgets se comportam da mesma forma que intou double. Previsível, fácil de ler e fácil de ensinar. Tudo é um "valor". Às vezes, um grande valor, mas os valores são mais fáceis de ensinar, porque você não tem essa coisa de "ação à distância" que obtém com ponteiros (ou referências).

A maioria dos objetos que você cria destina-se a ser usada apenas na função que os criou e, talvez, passada como entrada para funções filho. O programador não deveria ter que pensar em 'gerenciamento de memória' ao retornar objetos ou compartilhar objetos em partes amplamente separadas do software.

O escopo e a vida útil são importantes. Na maioria das vezes, é mais fácil se a vida útil for igual à do escopo. É mais fácil de entender e mais fácil de ensinar. Quando você deseja uma vida útil diferente, deve ser óbvio ler o código em que está fazendo isso, usando, shared_ptrpor exemplo. (Ou retornando objetos (grandes) por valor, aproveitando a semântica de movimento ou unique_ptr.

Isso pode parecer um problema de eficiência. E se eu quiser devolver um gadget foo()? A semântica de movimentação do C ++ 11 facilita o retorno de objetos grandes. Basta escrever Gadget foo() { ... }e ele funcionará e funcionará rapidamente. Você não precisa mexer com &&você mesmo, basta devolver as coisas por valor, e o idioma geralmente poderá fazer as otimizações necessárias. (Mesmo antes do C ++ 03, os compiladores faziam um trabalho notavelmente bom em evitar cópias desnecessárias.)

Como Stroustrup disse em outro lugar do vídeo (parafraseando): "Somente um cientista da computação insistiria em copiar um objeto e depois destruir o original. (A platéia ri). Por que não apenas mover o objeto diretamente para o novo local? É isso que os humanos (não cientistas da computação) esperam ".

Quando você pode garantir que apenas uma cópia de um objeto é necessária, é muito mais fácil entender a vida útil do objeto. Você pode escolher qual política de vida útil deseja e a coleta de lixo estará disponível, se desejar. Mas quando você entender os benefícios de outras abordagens, verá que a coleta de lixo está no final da sua lista de preferências.

Se isso não funcionar para você, você pode usar unique_ptrou, na sua falta shared_ptr,. O C ++ 11 bem escrito é mais curto, mais fácil de ler e mais fácil de ensinar do que muitos outros idiomas quando se trata de gerenciamento de memória.


1
O GC deve ser usado apenas para objetos que não adquirem recursos (por exemplo, peça a outras entidades que façam coisas em seu nome "até novo aviso"). Se Gadgetnão pedir mais nada para fazer algo em seu nome, o código original seria perfeitamente seguro em Java se a deleteinstrução sem sentido (para Java) fosse removida.
Supercat

@ supercat, objetos com destruidores chatos são interessantes. (Não defini 'chato', mas basicamente destruidores que nunca precisam ser chamados, exceto a liberação de memória). Pode ser possível para um compilador individual tratar shared_ptr<T>especialmente quando Té 'chato'. Ele poderia decidir não gerenciar um contador ref para esse tipo e, em vez disso, usar o GC. Isso permitiria que o GC fosse usado sem que o desenvolvedor precisasse notá-lo. A shared_ptrpoderia simplesmente ser visto como um ponteiro de GC, para adequado T. Mas há limitações nisso, e isso tornaria muitos programas mais lentos.
Aaron McDaid

Um sistema de tipo bom deve ter tipos diferentes para objetos de heap gerenciados por GC e RAII, pois alguns padrões de uso funcionam muito bem com um e muito mal com o outro. No .NET ou Java, uma instrução string1=string2;será executada muito rapidamente, independentemente do comprimento da string (literalmente nada mais é do que uma carga e armazenamento de registros) e não requer nenhum bloqueio para garantir que, se a instrução acima for executada enquanto string2estiver sendo gravado, string1conterá o valor antigo ou o novo valor, sem comportamento indefinido).
supercat

Em C ++, a atribuição de a shared_ptr<String>requer muita sincronização nos bastidores, e a atribuição de a Stringpode se comportar de maneira estranha se uma variável for lida e gravada simultaneamente. Casos em que alguém gostaria de escrever e ler Stringsimultaneamente não são muito comuns, mas podem surgir se, por exemplo, algum código desejar disponibilizar relatórios de status em andamento para outros threads. No .NET e Java, essas coisas simplesmente "funcionam".
supercat

1
@curiousguy nada mudou, a menos que você tome as precauções corretas, o Java ainda permite que o finalizador seja chamado assim que o construtor terminar. Aqui está um exemplo da vida real: “ finalize () chamou objetos fortemente alcançáveis ​​no Java 8 ”. A conclusão é nunca usar esse recurso, que quase todo mundo concorda em ser um erro de design histórico da linguagem. Quando seguimos esse conselho, a linguagem fornece o determinismo que amamos.
Holger

11

Porque o C ++ moderno não precisa de coleta de lixo.

A resposta das perguntas frequentes de Bjarne Stroustrup sobre esse assunto diz :

Eu não gosto de lixo Eu não gosto de jogar lixo. Meu ideal é eliminar a necessidade de um coletor de lixo, não produzindo lixo. Isso agora é possível.


A situação, para o código escrito atualmente (C ++ 17 e seguindo as Diretrizes Principais oficiais ), é a seguinte:

  • A maioria dos códigos relacionados à propriedade da memória está em bibliotecas (especialmente aquelas que fornecem contêineres).
  • A maior parte do uso de código envolvendo propriedade de memória segue o padrão RAII , portanto, a alocação é feita na construção e a desalocação na destruição, o que acontece ao sair do escopo em que algo foi alocado.
  • Você não aloca ou desaloca explicitamente a memória diretamente .
  • Os ponteiros não processados não possuem memória (se você seguiu as diretrizes); portanto, você não pode vazar passando-os por aí.
  • Se você está se perguntando como vai passar os endereços iniciais de seqüências de valores na memória - estará fazendo isso com uma extensão ; nenhum ponteiro bruto é necessário.
  • Se você realmente precisa de um "ponteiro" proprietário, use os ponteiros inteligentes da biblioteca padrão do C ++ - eles não podem vazar e são decentemente eficientes (embora a ABI possa atrapalhar isso). Como alternativa, você pode passar a propriedade através dos limites do escopo com "ponteiros do proprietário" . Isso é incomum e deve ser usado explicitamente; mas quando adotadas - elas permitem uma verificação estática agradável contra vazamentos.

"Oh sim? Mas e quanto a ...

... se eu apenas escrever código da maneira que costumávamos escrever C ++ nos velhos tempos? "

Na verdade, você pode simplesmente desconsiderar todas as diretrizes e escrever um código de aplicativo com vazamento - e ele será compilado e executado (e vazado), como sempre.

Mas não é uma situação "simplesmente não faça isso", onde se espera que o desenvolvedor seja virtuoso e exerça muito autocontrole; não é mais simples escrever código não conforme, nem é mais rápido escrever, nem tem melhor desempenho. Gradualmente, também será mais difícil escrever, pois você enfrentaria uma crescente "incompatibilidade de impedância" com o que o código em conformidade fornece e espera.

... se eu reintrepret_cast? Ou o ponteiro complexo é aritmético? Ou outros desses hacks? "

De fato, se você se dedicar a isso, poderá escrever um código que atrapalhe as coisas, apesar de ser agradável com as diretrizes. Mas:

  1. Você faria isso raramente (em termos de locais no código, não necessariamente em termos de fração do tempo de execução)
  2. Você faria isso intencionalmente, não acidentalmente.
  3. Fazer isso se destacará em uma base de código em conformidade com as diretrizes.
  4. É o tipo de código no qual você ignoraria o GC em outro idioma de qualquer maneira.

... desenvolvimento de biblioteca? "

Se você é um desenvolvedor de bibliotecas C ++, escreve um código inseguro envolvendo ponteiros brutos e é obrigado a codificar com cuidado e responsabilidade - mas esses são trechos de código independentes escritos por especialistas (e mais importante, revisados ​​por especialistas).


Então, é exatamente como Bjarne disse: Não há realmente motivação para coletar lixo em geral, pois todos menos se certificam de não produzir lixo. O GC está se tornando um problema com o C ++.

Isso não quer dizer que o GC não seja um problema interessante para certas aplicações específicas, quando você deseja empregar estratégias personalizadas de alocação e desalocação. Para aqueles que você deseja alocação e desalocação personalizadas, não um GC no nível do idioma.


Bem, é necessário (precisa do GC) se você estiver moendo cordas. Imagine que você tem grandes matrizes de cordas (pense em centenas de megabytes) que está construindo aos poucos, processando e reconstruindo em diferentes comprimentos, excluindo os não utilizados, combinando outros etc. sei porque tive que mudar para idiomas de alto nível para lidar. (É claro que você também pode criar seu próprio GC).
www-0av-Com

2
@ user1863152: Esse é um caso em que um alocador personalizado seria útil. Ainda não necessitam de um GC linguagem-integral ...
einpoklum

para einpoklum: verdadeiro. É apenas um cavalo para os cursos. Minha exigência era processar galões dinâmicos de informações de transporte de passageiros. Assunto fascinante. Realmente se resume à filosofia do software.
www-0av-Com

O GC, como o mundo Java e .NET descobriu, finalmente apresenta um enorme problema - ele não é escalável. Quando você tem bilhões de objetos ativos na memória, como fazemos hoje em dia com qualquer software não trivial, você precisa começar a escrever código para ocultar coisas do GC. É um fardo ter GC em Java e .NET.
Zach Saw

10

A idéia por trás do C ++ era que você não pagaria nenhum impacto no desempenho pelos recursos que não usa. Portanto, adicionar coleta de lixo significaria ter alguns programas executados diretamente no hardware, como C e outros em algum tipo de máquina virtual de tempo de execução.

Nada impede que você use algum tipo de ponteiro inteligente vinculado a algum mecanismo de coleta de lixo de terceiros. Eu me lembro da Microsoft fazendo algo parecido com o COM e não deu certo.


2
Eu não acho que o GC requer uma VM. O compilador pode adicionar código a todas as operações do ponteiro para atualizar um estado global, enquanto um thread separado é executado em segundo plano, excluindo objetos conforme necessário.
User83255

3
Concordo. Você não precisa de uma máquina virtual, mas no segundo em que você começar a ter algo para gerenciar sua memória, você sente que, em segundo plano, sinto que você deixou os "fios elétricos" reais e passou por uma situação de VM.
511 Uri


4

Um dos princípios fundamentais por trás da linguagem C original é que a memória é composta por uma sequência de bytes, e o código precisa se preocupar apenas com o que esses bytes significam no momento exato em que estão sendo usados. O C moderno permite que os compiladores imponham restrições adicionais, mas o C inclui - e o C ++ retém - a capacidade de decompor um ponteiro em uma sequência de bytes, montar qualquer sequência de bytes que contenha os mesmos valores em um ponteiro e, em seguida, usá-lo para acessar o objeto anterior.

Embora essa capacidade possa ser útil - ou mesmo indispensável - em alguns tipos de aplicativos, um idioma que inclua essa capacidade será muito limitado em oferecer suporte a qualquer tipo de coleta de lixo útil e confiável. Se um compilador não souber tudo o que foi feito com os bits que compõem um ponteiro, ele não terá como saber se informações suficientes para reconstruir o ponteiro podem existir em algum lugar do universo. Como seria possível que essas informações fossem armazenadas de maneira que o computador não pudesse acessar, mesmo que soubesse delas (por exemplo, os bytes que compõem o ponteiro podem ter sido mostrados na tela por tempo suficiente para alguém escrever em um pedaço de papel), pode ser literalmente impossível para um computador saber se um ponteiro pode ser usado no futuro.

Uma peculiaridade interessante de muitas estruturas de coleta de lixo é que uma referência de objeto não é definida pelos padrões de bits contidos nela, mas pelo relacionamento entre os bits contidos na referência de objeto e outras informações mantidas em outro local. Em C e C ++, se o padrão de bits armazenado em um ponteiro identificar um objeto, esse padrão de bit identificará esse objeto até que o objeto seja destruído explicitamente. Em um sistema GC típico, um objeto pode ser representado por um padrão de bits 0x1234ABCD em um momento, mas o próximo ciclo do GC pode substituir todas as referências a 0x1234ABCD por referências a 0x4321BABE, e o objeto será representado pelo último padrão. Mesmo se alguém exibisse o padrão de bits associado a uma referência de objeto e depois o lesse novamente no teclado,


Esse é um ponto realmente bom: eu recentemente roubei alguns bits dos meus ponteiros porque, caso contrário, haveria uma quantidade estúpida de erros de cache.
Passer Por

@PasserBy: Gostaria de saber quantos aplicativos que usam ponteiros de 64 bits se beneficiariam mais usando ponteiros de 32 bits em escala como referências a objetos, ou mantendo quase tudo em 4GiB de espaço de endereço e usando objetos especiais para armazenar / recuperar dados de alta armazenamento de alta velocidade além? As máquinas têm RAM suficiente para que o consumo de RAM dos ponteiros de 64 bits possa não importar, exceto que eles consomem o dobro do cache que os ponteiros de 32 bits.
supercat

3

Toda a conversa técnica está complicando demais o conceito.

Se você colocar o GC no C ++ para toda a memória automaticamente, considere algo como um navegador da web. O navegador da web deve carregar um documento da web completo E executar scripts da web. Você pode armazenar variáveis ​​de script da web na árvore de documentos. Em um documento BIG em um navegador com muitas guias abertas, isso significa que toda vez que o GC deve fazer uma coleção completa, também deve digitalizar todos os elementos do documento.

Na maioria dos computadores, isso significa que PAGE FAULTS ocorrerá. Portanto, a principal razão para responder à pergunta é que ocorrerão falhas na página. Você saberá isso como quando o seu PC começar a fazer muito acesso ao disco. Isso ocorre porque o GC deve tocar em muita memória para provar indicadores inválidos. Quando você tem um aplicativo de boa-fé usando muita memória, a necessidade de verificar todos os objetos em todas as coleções é prejudicada por causa dos PAGE FAULTS. Uma falha de página ocorre quando a memória virtual precisa ser lida novamente na RAM do disco.

Portanto, a solução correta é dividir um aplicativo nas partes que precisam de GC e nas que não precisam. No caso do exemplo do navegador da web acima, se a árvore de documentos foi alocada com malloc, mas o javascript foi executado com o GC, toda vez que o GC entra nele, ele apenas varre uma pequena parte da memória e todos os elementos PAGED OUT da memória por a árvore de documentos não precisa ser paginada novamente.

Para entender melhor esse problema, procure na memória virtual e como ela é implementada nos computadores. É tudo sobre o fato de que 2 GB estão disponíveis para o programa quando não há realmente muita memória RAM. Em computadores modernos com 2 GB de RAM para um sistema 32BIt, esse não é um problema, desde que apenas um programa esteja sendo executado.

Como um exemplo adicional, considere uma coleção completa que deve rastrear todos os objetos. Primeiro, você deve verificar todos os objetos acessíveis através de raízes. Segundo, verifique todos os objetos visíveis na etapa 1. Em seguida, verifique os destruidores em espera. Então vá para todas as páginas novamente e desligue todos os objetos invisíveis. Isso significa que muitas páginas podem ser trocadas e retornadas várias vezes.

Portanto, minha resposta para resumir é que o número de PAGE FAULTS que ocorrem como resultado de tocar em toda a memória faz com que o GC completo para todos os objetos em um programa seja inviável e, portanto, o programador deve ver o GC como uma ajuda para coisas como scripts e banco de dados funcionam, mas fazem coisas normais com o gerenciamento manual de memória.

E a outra razão muito importante, é claro, são as variáveis ​​globais. Para que o coletor saiba que um ponteiro de variável global está no GC, seriam necessárias palavras-chave específicas e, portanto, o código C ++ existente não funcionaria.


3

RESPOSTA CURTA: Não sabemos como fazer a coleta de lixo de maneira eficiente (com pouco tempo e espaço sobrando) e corretamente o tempo todo (em todos os casos possíveis).

RESPOSTA LONGA: Assim como C, C ++ é uma linguagem de sistemas; isso significa que é usado quando você está escrevendo o código do sistema, por exemplo, sistema operacional. Em outras palavras, o C ++ é projetado, assim como o C, com o melhor desempenho possível como o principal alvo. O padrão do idioma não adicionará nenhum recurso que possa prejudicar o objetivo de desempenho.

Isso interrompe a pergunta: por que a coleta de lixo dificulta o desempenho? O principal motivo é que, quando se trata de implementação, nós [cientistas da computação] não sabemos como fazer a coleta de lixo com um mínimo de sobrecarga, para todos os casos. Portanto, é impossível para o compilador C ++ e o sistema de tempo de execução executar a coleta de lixo com eficiência o tempo todo. Por outro lado, um programador de C ++ deve conhecer seu design / implementação e é a melhor pessoa para decidir a melhor forma de fazer a coleta de lixo.

Por último, se controle (hardware, detalhes, etc.) e desempenho (tempo, espaço, energia etc.) não são as principais restrições, o C ++ não é a ferramenta de gravação. Outro idioma pode servir melhor e oferecer mais gerenciamento de tempo de execução [oculto], com a sobrecarga necessária.


3

Quando comparamos C ++ com Java, vemos que o C ++ não foi projetado com a Garbage Collection implícita em mente, enquanto o Java era.

Ter coisas como ponteiros arbitrários no estilo C não é ruim apenas para implementações de GC, mas também destruiria a compatibilidade com versões anteriores de uma grande quantidade de C ++ - código legado.

Além disso, o C ++ é uma linguagem que deve ser executada como executável independente, em vez de ter um ambiente de tempo de execução complexo.

Em suma: Sim, pode ser possível adicionar a Coleta de Lixo ao C ++, mas por uma questão de continuidade, é melhor não fazer isso.


1
Liberar memória e destruir destruidores são questões muito completamente separadas. (O Java não possui destruidores, que é uma PITA.) O GC libera memória, não executa dtors.
precisa saber é o seguinte

0

Principalmente por duas razões:

  1. Porque ele não precisa de um (IMHO)
  2. Porque é praticamente incompatível com o RAII, que é a pedra angular do C ++

O C ++ já oferece gerenciamento manual de memória, alocação de pilha, RAII, contêineres, ponteiros automáticos, ponteiros inteligentes ... Isso deve ser o suficiente. Os coletores de lixo são para programadores preguiçosos que não querem gastar 5 minutos pensando em quem deve possuir quais objetos ou quando os recursos devem ser liberados. Não é assim que fazemos as coisas em C ++.


Existem inúmeros algoritmos (mais recentes) que são inerentemente difíceis de implementar sem a coleta de lixo. O tempo mudou. A inovação também vem de novas idéias que combinam bem com os idiomas de alto nível (de coleta de lixo). Tente fazer o backport de qualquer um deles para C ++ livre de GC; você perceberá os solavancos na estrada. (Eu sei que devo dar exemplos, mas estou com pressa agora. Desculpe. Um em que posso pensar agora gira em torno de estruturas de dados persistentes, nas quais a contagem de referências não funciona.).
BitTickler

0

Impor a coleta de lixo é realmente uma mudança de paradigma de baixo para alto nível.

Se você observar como as strings são tratadas em um idioma com coleta de lixo, você descobrirá que elas SOMENTE permitem funções de manipulação de strings de alto nível e não permitem acesso binário às strings. Simplificando, todas as funções de string verificam primeiro os ponteiros para ver onde está a string, mesmo se você estiver apenas desenhando um byte. Portanto, se você estiver executando um loop que processa cada byte em uma string em um idioma com coleta de lixo, ele deve calcular o local base mais o deslocamento para cada iteração, porque não pode saber quando a string foi movida. Então você tem que pensar em pilhas, pilhas, threads, etc., etc.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.