Vocês, desenvolvedores de C ++, poderiam nos dar uma boa descrição do que é RAII, por que é importante e se pode ou não ter alguma relevância para outras linguagens?
I fazer saber um pouco. Eu acredito que significa "Aquisição de recursos é inicialização". No entanto, esse nome não combina com meu entendimento (possivelmente incorreto) do que é RAII: tenho a impressão de que RAII é uma forma de inicializar objetos na pilha de forma que, quando essas variáveis saem do escopo, os destruidores irão automaticamente ser chamado fazendo com que os recursos sejam limpos.
Então, por que isso não é chamado de "usar a pilha para acionar a limpeza" (UTSTTC :)? Como você vai de lá para "RAII"?
E como você pode fazer algo na pilha que causará a limpeza de algo que vive na pilha? Além disso, há casos em que você não pode usar RAII? Você já se pegou desejando a coleta de lixo? Pelo menos um coletor de lixo que você poderia usar para alguns objetos enquanto permite que outros sejam gerenciados?
Obrigado.
fonte
:(
Eu estava lendo o tópico inteiro, naquela época, e nem me considerava um novato em C ++!Respostas:
RAII está lhe dizendo o que fazer: Adquira seu recurso em um construtor! Eu acrescentaria: um recurso, um construtor. UTSTTC é apenas uma aplicação disso, RAII é muito mais.
Gerenciamento de recursos é uma merda. Aqui, recurso é tudo o que precisa de limpeza após o uso. Estudos de projetos em muitas plataformas mostram que a maioria dos bugs está relacionada ao gerenciamento de recursos - e isso é particularmente ruim no Windows (devido aos muitos tipos de objetos e alocadores).
Em C ++, o gerenciamento de recursos é particularmente complicado devido à combinação de exceções e modelos (estilo C ++). Para uma espiada sob o capô, consulte GOTW8 ).
C ++ garante que o destruidor seja chamado se, e somente se, o construtor for bem-sucedido. Contando com isso, o RAII pode resolver muitos problemas desagradáveis dos quais o programador médio pode nem estar ciente. Aqui estão alguns exemplos além de "minhas variáveis locais serão destruídas sempre que eu retornar".
Vamos começar com uma
FileHandle
classe excessivamente simplista que emprega RAII:Se a construção falhar (com uma exceção), nenhuma outra função de membro - nem mesmo o destruidor - será chamada.
RAII evita usar objetos em um estado inválido. já torna a vida mais fácil antes mesmo de usarmos o objeto.
Agora, vamos dar uma olhada nos objetos temporários:
Existem três casos de erro a tratar: nenhum arquivo pode ser aberto, apenas um arquivo pode ser aberto, ambos os arquivos podem ser abertos, mas a cópia dos arquivos falhou. Em uma implementação não RAII,
Foo
teria que tratar todos os três casos explicitamente.RAII libera recursos que foram adquiridos, mesmo quando vários recursos são adquiridos em uma instrução.
Agora, vamos agregar alguns objetos:
O construtor de
Logger
falhará seoriginal
o construtor de falhar (porquefilename1
não pôde ser aberto),duplex
o construtor de falhar (porquefilename2
não pôde ser aberto) ou gravar nos arquivos dentroLogger
do corpo do construtor falhar. Em qualquer um desses casos,Logger
o destruidor de não será chamado - portanto, não podemos contar comLogger
o destruidor de para liberar os arquivos. Mas seoriginal
foi construído, seu destruidor será chamado durante a limpeza doLogger
construtor.RAII simplifica a limpeza após a construção parcial.
Pontos negativos:
Pontos negativos? Todos os problemas podem ser resolvidos com RAII e smart pointers ;-)
O RAII às vezes é difícil de controlar quando você precisa de aquisição atrasada, colocando objetos agregados no heap.
Imagine que o Logger precisa de um
SetTargetFile(const char* target)
. Nesse caso, o identificador, que ainda precisa ser um membroLogger
, precisa residir na pilha (por exemplo, em um ponteiro inteligente, para acionar a destruição do identificador de forma adequada).Nunca desejei realmente a coleta de lixo. Quando faço C #, às vezes sinto um momento de felicidade que simplesmente não preciso me importar, mas muito mais sinto falta de todos os brinquedos legais que podem ser criados por meio da destruição determinística. (usar
IDisposable
simplesmente não resolve.)Eu tive uma estrutura particularmente complexa que pode ter se beneficiado do GC, onde ponteiros inteligentes "simples" causariam referências circulares em várias classes. Fizemos confusão equilibrando cuidadosamente os indicadores fortes e fracos, mas sempre que quisermos mudar algo, temos que estudar um grande gráfico de relacionamento. O GC pode ter sido melhor, mas alguns dos componentes continham recursos que deveriam ser liberados o mais rápido possível.
Uma observação sobre o exemplo do FileHandle: Não era para ser completo, apenas um exemplo - mas acabou incorreto. Obrigado Johannes Schaub por apontar e FredOverflow por transformá-lo em uma solução C ++ 0x correta. Com o tempo, me conformei com a abordagem documentada aqui .
fonte
Foo
proprietárioBar
eBoz
sofrer mutação, ...Existem excelentes respostas por aí, então eu apenas adiciono algumas coisas esquecidas.
0. RAII é sobre escopos
RAII é sobre ambos:
Outros já responderam sobre isso, então não vou entrar em detalhes.
1. Ao codificar em Java ou C #, você já usa RAII ...
Como Monsieur Jourdain fazia com a prosa, o pessoal de C # e até mesmo de Java já usa RAII, mas de maneiras ocultas. Por exemplo, o seguinte código Java (que é escrito da mesma forma em C #, substituindo
synchronized
porlock
):... já está usando RAII: A aquisição mutex é feita na palavra-chave (
synchronized
oulock
), e a desaquisição será feita ao sair do escopo.É tão natural em sua notação que quase não requer explicação, mesmo para pessoas que nunca ouviram falar de RAII.
A vantagem de C ++ sobre Java e C # aqui é que qualquer coisa pode ser feita usando RAII. Por exemplo, não há equivalente embutido direto de
synchronized
nemlock
em C ++, mas ainda podemos tê-los.Em C ++, seria escrito:
que pode ser facilmente escrito da maneira Java / C # (usando macros C ++):
2. RAII tem usos alternativos
Você sabe quando o construtor será chamado (na declaração do objeto) e quando seu destruidor correspondente será chamado (na saída do escopo), então você pode escrever um código quase mágico com apenas uma linha. Bem-vindo ao país das maravilhas do C ++ (pelo menos, do ponto de vista de um desenvolvedor C ++).
Por exemplo, você pode escrever um objeto contador (deixo isso como um exercício) e usá-lo apenas declarando sua variável, como o objeto de bloqueio acima foi usado:
que, obviamente, pode ser escrito, novamente, da maneira Java / C # usando uma macro:
3. Por que falta C ++
finally
?A
finally
cláusula é usada em C # / Java para manipular o descarte de recursos em caso de saída do escopo (por meio de umareturn
exceção ou lançada).Leitores de especificações astutos terão notado que C ++ não tem cláusula finally. E isso não é um erro, porque C ++ não precisa, pois o RAII já trata do descarte de recursos. (E acredite em mim, escrever um destruidor em C ++ é muito mais fácil do que escrever a cláusula final do Java certa, ou mesmo o método Dispose correto do C #).
Ainda assim, às vezes, uma
finally
cláusula seria legal. Podemos fazer em C ++? Sim, nós podemos! E novamente com um uso alternativo de RAII.Conclusão: RAII é mais do que filosofia em C ++: é C ++
Quando você atinge algum nível de experiência em C ++, começa a pensar em termos de RAII , em termos de execução automatizada de construtores e destruidores .
Você começa a pensar em termos de escopos , e os caracteres
{
e}
se tornam os mais importantes em seu código.E quase tudo se encaixa bem em termos de RAII: segurança de exceção, mutexes, conexões de banco de dados, solicitações de banco de dados, conexão de servidor, relógios, identificadores de sistema operacional, etc. e, por último, mas não menos importante, memória.
A parte do banco de dados não é desprezível, pois, se você aceitar pagar o preço, pode até escrever no estilo " programação transacional ", executando linhas e linhas de código até decidir, no final, se deseja cometer todas as alterações , ou, se não for possível, reverter todas as alterações (desde que cada linha satisfaça pelo menos a Garantia de Exceção Forte). (veja a segunda parte deste artigo do Herb's Sutter para a programação transacional).
E como um quebra-cabeça, tudo se encaixa.
RAII faz parte do C ++, C ++ não poderia ser C ++ sem ele.
Isso explica por que os desenvolvedores C ++ experientes são tão apaixonados por RAII e por que RAII é a primeira coisa que eles procuram quando tentam outra linguagem.
E explica por que o Coletor de Lixo, embora seja uma peça magnífica de tecnologia em si, não é tão impressionante do ponto de vista de um desenvolvedor de C ++:
fonte
Por favor, veja:
Os programadores de outras linguagens, além de C ++, usam, conhecem ou entendem RAII?
RAII e ponteiros inteligentes em C ++
C ++ oferece suporte a blocos 'finalmente'? (E o que é esse 'RAII' que eu sempre ouço falar?)
RAII vs. exceções
etc ..
fonte
RAII está usando a semântica de destruidores C ++ para gerenciar recursos. Por exemplo, considere um ponteiro inteligente. Você tem um construtor parametrizado do ponteiro que inicializa este ponteiro com o endereço do objeto. Você aloca um ponteiro na pilha:
Quando o ponteiro inteligente sai do escopo, o destruidor da classe do ponteiro exclui o objeto conectado. O ponteiro é alocado na pilha e o objeto - alocado na pilha.
Existem certos casos em que RAII não ajuda. Por exemplo, se você usar ponteiros inteligentes de contagem de referência (como boost :: shared_ptr) e criar uma estrutura semelhante a um gráfico com um ciclo, você corre o risco de enfrentar um vazamento de memória porque os objetos em um ciclo evitarão que um ao outro seja liberado. A coleta de lixo ajudaria contra isso.
fonte
Eu gostaria de colocar um pouco mais forte do que as respostas anteriores.
RAII, Resource Acquisition Is Initialization significa que todos os recursos adquiridos devem ser adquiridos no contexto da inicialização de um objeto. Isso proíbe a aquisição de recursos "nua". A justificativa é que a limpeza em C ++ funciona com base no objeto, não com base na chamada de função. Portanto, toda a limpeza deve ser feita por objetos, não por chamadas de função. Nesse sentido, C ++ é mais orientado a objetos do que, por exemplo, Java. A limpeza do Java é baseada em chamadas de função em
finally
cláusulas.fonte
Eu concordo com cpitis. Mas gostaria de acrescentar que os recursos podem ser qualquer coisa, não apenas memória. O recurso pode ser um arquivo, uma seção crítica, um thread ou uma conexão de banco de dados.
É chamado de Aquisição de Recursos é Inicialização porque o recurso é adquirido quando o objeto que controla o recurso é construído. Se o construtor falhou (isto é, devido a uma exceção), o recurso não é adquirido. Então, quando o objeto sai do escopo, o recurso é liberado. c ++ garante que todos os objetos na pilha que foram construídos com sucesso serão destruídos (isso inclui construtores de classes base e membros, mesmo se o construtor de superclasse falhar).
O raciocínio por trás do RAII é tornar segura a exceção de aquisição de recursos. Que todos os recursos adquiridos sejam devidamente liberados, independentemente de onde ocorra uma exceção. No entanto, isso depende da qualidade da classe que adquire o recurso (isso deve ser excepcionalmente seguro e isso é difícil).
fonte
O problema com a coleta de lixo é que você perde a destruição determinística que é crucial para RAII. Quando uma variável sai do escopo, cabe ao coletor de lixo quando o objeto será recuperado. O recurso mantido pelo objeto continuará sendo mantido até que o destruidor seja chamado.
fonte
RAII vem de Alocação de recursos é inicialização. Basicamente, significa que quando um construtor termina a execução, o objeto construído está totalmente inicializado e pronto para uso. Também implica que o destruidor irá liberar quaisquer recursos (por exemplo, memória, recursos do sistema operacional) pertencentes ao objeto.
Comparado com linguagens / tecnologias de coleta de lixo (por exemplo, Java, .NET), C ++ permite o controle total da vida de um objeto. Para um objeto alocado na pilha, você saberá quando o destruidor do objeto será chamado (quando a execução sai do escopo), coisa que realmente não é controlada no caso de coleta de lixo. Mesmo usando ponteiros inteligentes em C ++ (por exemplo, boost :: shared_ptr), você saberá que, quando não houver referência ao objeto apontado, o destruidor desse objeto será chamado.
fonte
Quando uma instância de int_buffer passa a existir, ela deve ter um tamanho e alocará a memória necessária. Quando sai do escopo, seu destruidor é chamado. Isso é muito útil para coisas como objetos de sincronização. Considerar
Não, na verdade não.
Nunca. A coleta de lixo resolve apenas um subconjunto muito pequeno de gerenciamento dinâmico de recursos.
fonte
Já existem muitas respostas boas aqui, mas gostaria apenas de acrescentar:
Uma explicação simples de RAII é que, em C ++, um objeto alocado na pilha é destruído sempre que sai do escopo. Isso significa que um destruidor de objetos será chamado e poderá fazer toda a limpeza necessária.
Isso significa que, se um objeto for criado sem "novo", não será necessário "excluir". E essa também é a ideia por trás dos "ponteiros inteligentes" - eles residem na pilha e, essencialmente, envolvem um objeto baseado em heap.
fonte
RAII é um acrônimo para Resource Acquisition Is Initialization.
Esta técnica é muito única para C ++ por causa de seu suporte para Construtores e Destruidores e quase automaticamente os construtores que correspondem aos argumentos sendo passados ou, no pior caso, o construtor padrão é chamado de & destruidores se explicitamente fornecido for chamado de outra forma, o padrão que é adicionado pelo compilador C ++ é chamado se você não escreveu um destruidor explicitamente para uma classe C ++. Isso acontece apenas para objetos C ++ que são gerenciados automaticamente - o que significa que não estão usando o armazenamento gratuito (memória alocada / desalocada usando novos, novos [] / delete, delete [] operadores C ++).
A técnica RAII faz uso deste recurso de objeto autogerenciado para manipular os objetos que são criados no heap / free-store solicitando explicitamente por mais memória usando new / new [], que deve ser destruído explicitamente chamando delete / delete [] . A classe do objeto autogerenciado envolverá este outro objeto criado na memória heap / free-store. Portanto, quando o construtor do objeto autogerenciado é executado, o objeto empacotado é criado no heap / memória de armazenamento livre e quando o identificador do objeto autogerenciado sai do escopo, o destruidor desse objeto autogerenciado é chamado automaticamente no qual o objeto empacotado objeto é destruído usando delete. Com os conceitos OOP, se você agrupar tais objetos dentro de outra classe em escopo privado, você não terá acesso aos membros e métodos das classes agrupadas e esta é a razão pela qual os ponteiros inteligentes (também conhecidos como classes de identificador) são projetados. Esses ponteiros inteligentes expõem o objeto empacotado como objeto digitado para o mundo externo e lá, permitindo invocar quaisquer membros / métodos dos quais o objeto de memória exposto é feito. Observe que os ponteiros inteligentes têm vários sabores com base em necessidades diferentes. Você deve consultar a programação C ++ moderna de Andrei Alexandrescu ou a implementação / documentação da biblioteca boost (www.boostorg) shared_ptr.hpp para aprender mais sobre ela. Espero que isso ajude você a entender RAII. Você deve consultar a programação C ++ moderna de Andrei Alexandrescu ou a implementação / documentação da biblioteca boost (www.boostorg) shared_ptr.hpp para aprender mais sobre ela. Espero que isso ajude você a entender RAII. Você deve consultar a programação C ++ moderna de Andrei Alexandrescu ou a implementação / documentação da biblioteca boost (www.boostorg) shared_ptr.hpp para aprender mais sobre ela. Espero que isso ajude você a entender RAII.
fonte