O que significa Aquisição de Recursos é Inicialização (RAII)?

Respostas:

374

É um nome realmente terrível para um conceito incrivelmente poderoso, e talvez uma das coisas número 1 que os desenvolvedores de C ++ sentem falta quando mudam para outros idiomas. Houve um movimento para tentar renomear esse conceito como Gerenciamento de recursos vinculados a escopo , embora ainda não pareça ter entendido.

Quando dizemos 'Recurso', não queremos dizer apenas memória - podem ser identificadores de arquivo, soquetes de rede, identificadores de banco de dados, objetos GDI ... Em suma, coisas das quais temos um suprimento finito e, portanto, precisamos ser capazes de controlar seu uso. O aspecto 'Limite do escopo' significa que a vida útil do objeto está vinculada ao escopo de uma variável; portanto, quando a variável sai do escopo, o destruidor libera o recurso. Uma propriedade muito útil disso é que contribui para maior segurança de exceção. Por exemplo, compare isso:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

Com o RAII

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

Neste último caso, quando a exceção é lançada e a pilha é desenrolada, as variáveis ​​locais são destruídas, o que garante que nosso recurso seja limpo e não vaze.

the_mandrill
fonte
2
@the_mandrill: Tentei ideone.com/1Jjzuc neste programa. Mas não há chamada de destruidor. O tomdalling.com/blog/software-design/… diz que o C ++ garante que o destruidor de objetos na pilha será chamado, mesmo se uma exceção for lançada. Então, por que o destruidor não foi executado aqui? Meu recurso vazou ou nunca será liberado ou liberado?
Destructor
8
Uma exceção é lançada, mas você não está capturando, portanto o aplicativo é encerrado. Se você concluir com uma tentativa {} catch () {}, ela funcionará conforme o esperado: ideone.com/xm2GR9
the_mandrill
2
Não tenho certeza se Scope-Boundé a melhor opção de nome aqui, pois os especificadores de classe de armazenamento, juntamente com o escopo, determinam a duração do armazenamento de uma entidade. Estreitando-lo feito a capa de escopo é talvez uma simplificação útil, no entanto, não é 100% preciso
SebNag
125

Este é um idioma de programação que significa brevemente que você

  • encapsular um recurso em uma classe (cujo construtor geralmente - mas não necessariamente ** - adquire o recurso, e seu destruidor sempre o libera)
  • use o recurso através de uma instância local da classe *
  • o recurso é liberado automaticamente quando o objeto fica fora do escopo

Isso garante que, aconteça o que acontecer enquanto o recurso estiver em uso, ele será liberado (devido ao retorno normal, à destruição do objeto que o contém ou a uma exceção lançada).

É uma boa prática amplamente usada em C ++, porque além de ser uma maneira segura de lidar com recursos, também torna seu código muito mais limpo, pois você não precisa misturar código de tratamento de erros com a funcionalidade principal.

* Atualização: "local" pode significar uma variável local ou uma variável de membro não estática de uma classe. No último caso, a variável membro é inicializada e destruída com seu objeto proprietário.

** Atualização2: como o @sbi apontou, o recurso - embora freqüentemente seja alocado dentro do construtor - também pode ser alocado fora e passado como parâmetro.

Péter Török
fonte
1
AFAIK, a sigla não implica que o objeto deva estar em uma variável local (pilha). Pode ser uma variável membro de outro objeto, portanto, quando o objeto 'holding' é destruído, o objeto membro também é destruído e o recurso é liberado. Na verdade, acho que o acrônimo significa especificamente apenas que não há open()/ close()métodos para inicializar e liberar o recurso, apenas o construtor e o destruidor; portanto, a "retenção" do recurso é apenas o tempo de vida do objeto, não importa se esse tempo de vida é manuseado pelo contexto (pilha) ou explicitamente (alloc dinâmico)
Javier
1
Na verdade, nada diz que o recurso deve ser adquirido no construtor. Fluxos de arquivos, sequências de caracteres e outros contêineres fazem isso, mas o recurso também pode ser passado para o construtor, como é geralmente o caso de ponteiros inteligentes. Como a sua é a resposta mais votada, convém corrigir isso.
SBI
Não é um acrônimo, é uma abreviação. A maioria das pessoas do IIRC o pronuncia como "sempre é", para que realmente não se qualifique para um acrônimo como digamos DARPA, que é pronunciado DARPA em vez de escrito. Além disso, eu diria que RAII é um paradigma e não um mero idioma.
DTECH
@ Peter Torok: Eu tentei ideone.com/1Jjzuc este programa. Mas não há chamada de destruidor. O tomdalling.com/blog/software-design/… diz que o C ++ garante que o destruidor de objetos na pilha será chamado, mesmo se uma exceção for lançada. Então, por que o destruidor não foi executado aqui? Meu recurso vazou ou nunca será liberado ou liberado?
Destructor
50

"RAII" significa "Aquisição de Recursos é Inicialização" e é realmente um nome impróprio, pois não se trata de aquisição de recursos (e a inicialização de um objeto), mas a liberação do recurso (por meio da destruição de um objeto). )
Mas RAII é o nome que recebemos e permanece.

No seu âmago, o idioma apresenta recursos de encapsulamento (pedaços de memória, arquivos abertos, mutexes desbloqueados, você escolhe) em objetos locais, automáticos , e com o destruidor desse objeto liberando o recurso quando o objeto é destruído no local. final do escopo ao qual pertence:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

Obviamente, os objetos nem sempre são locais, automáticos. Eles também podem ser membros de uma classe:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

Se esses objetos gerenciam memória, eles são chamados de "ponteiros inteligentes".

Existem muitas variações disso. Por exemplo, no primeiro trecho de código, surge a pergunta sobre o que aconteceria se alguém quisesse copiar obj. A saída mais fácil seria simplesmente proibir a cópia. std::unique_ptr<>, um ponteiro inteligente para fazer parte da biblioteca padrão, conforme apresentado no próximo padrão C ++, faz isso.
Outro ponteiro inteligente, std::shared_ptrapresenta "propriedade compartilhada" do recurso (um objeto alocado dinamicamente) que ele possui. Ou seja, ele pode ser copiado livremente e todas as cópias se referem ao mesmo objeto. O ponteiro inteligente controla quantas cópias se referem ao mesmo objeto e o exclui quando a última estiver sendo destruída.
Uma terceira variante é apresentada porstd::auto_ptr que implementa um tipo de semântica de movimentação: um objeto pertence a apenas um ponteiro, e a tentativa de copiar um objeto resultará (através da invasão de sintaxe) na transferência da propriedade do objeto para o destino da operação de cópia.

sbi
fonte
4
std::auto_ptré uma versão obsoleta do std::unique_ptr. std::auto_ptrO tipo de semântica de movimentação simulada, tanto quanto possível no C ++ 98, std::unique_ptrusa a nova semântica de movimentação do C ++ 11. A nova classe foi criada porque a semântica de movimentação do C ++ 11 é mais explícita (requer std::moveexceção do temporário) enquanto foi padronizada para qualquer cópia de não-const no std::auto_ptr.
Jan Hudec
@JiahaoCai: Uma vez, há muitos anos (na Usenet), o próprio Stroustrup disse isso.
sbi 20/01
21

A vida útil de um objeto é determinada por seu escopo. No entanto, às vezes precisamos, ou é útil, criar um objeto que viva independentemente do escopo em que foi criado. Em C ++, o operadornew é usado para criar esse objeto. E para destruir o objeto, o operador deletepode ser usado. Os objetos criados pelo operador newsão alocados dinamicamente, ou seja, alocados na memória dinâmica (também chamada heap ou free store ). Portanto, um objeto criado por newcontinuará existindo até que seja explicitamente destruído usandodelete .

Alguns erros que podem ocorrer ao usar newe deletesão:

  • Objeto vazado (ou memória): usando newpara alocar um objeto e esquecer deleteo objeto.
  • Exclusão prematura (ou referência pendente ): segurando outro ponteiro para um objeto, deleteo objeto, e use o outro ponteiro.
  • Exclusão dupla : tentando deleteum objeto duas vezes.

Geralmente, as variáveis ​​de escopo são preferidas. No entanto, o RAII pode ser usado como uma alternativa paranew edelete tornar um objeto vivo independentemente de seu escopo. Essa técnica consiste em levar o ponteiro para o objeto que foi alocado no heap e colocá-lo em um objeto manipulador / gerenciador . Este último possui um destruidor que cuidará da destruição do objeto. Isso garantirá que o objeto esteja disponível para qualquer função que queira acessá-lo e que o objeto seja destruído quando a vida útil do objeto de manipulação terminar, sem a necessidade de limpeza explícita.

Exemplos da biblioteca padrão C ++ que usam RAII são std::string e std::vector.

Considere este pedaço de código:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

Quando você cria um vetor e envia elementos a ele, não se preocupa em alocar e desalocar esses elementos. O vetor usa newpara alocar espaço para seus elementos no heap edelete liberar esse espaço. Você, como usuário do vetor, não se importa com os detalhes da implementação e confia no vetor para não vazar. Nesse caso, o vetor é o objeto de manipulação de de seus elementos.

Outros exemplos da biblioteca padrão que usam RAII são std::shared_ptr, std::unique_ptrestd::lock_guard .

Outro nome para essa técnica é SBRM , abreviação de Scope-Bound Resource Management .

elmiomar
fonte
1
"SBRM" faz muito mais sentido para mim. Cheguei a essa pergunta porque achava que tinha compreendido o RAII, mas o nome estava me excitando, ao ouvi-lo descrito como "Gerenciamento de recursos vinculados a escopo", e me fez perceber instantaneamente que eu realmente entendia o conceito.
perfil completo de JShorthouse
Não sei por que isso não foi marcado como resposta à pergunta. É uma resposta muito completa e bem escrita, obrigado @elmiomar
Abdelrahman Shoman
13

O livro C ++ Programming with Design Patterns Revealed descreve RAII como:

  1. Adquirindo todos os recursos
  2. Usando recursos
  3. Liberando recursos

Onde

  • Os recursos são implementados como classes e todos os ponteiros têm wrappers de classe em torno deles (tornando-os ponteiros inteligentes).

  • Os recursos são adquiridos invocando seus construtores e liberados implicitamente (na ordem inversa de aquisição) invocando seus destruidores.

Dennis
fonte
1
@Brandin Eu editei meu post para que os leitores se concentrem no conteúdo que importa, em vez de debater a área cinzenta da lei de direitos autorais sobre o que constitui uso justo.
Dennis
7

Existem três partes para uma classe RAII:

  1. O recurso é abandonado no destruidor
  2. Instâncias da classe são alocadas à pilha
  3. O recurso é adquirido no construtor. Esta parte é opcional, mas comum.

RAII significa "Aquisição de recursos é inicialização". A parte "aquisição de recursos" do RAII é onde você inicia algo que deve ser finalizado posteriormente, como:

  1. Abrindo um arquivo
  2. Alocando um pouco de memória
  3. Adquirindo uma fechadura

A parte "is initialization" significa que a aquisição ocorre dentro do construtor de uma classe.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/

Mohammad Moridi
fonte
5

O gerenciamento manual de memória é um pesadelo que os programadores vêm inventando maneiras de evitar desde a invenção do compilador. Linguagens de programação com coletores de lixo facilitam a vida, mas com o custo do desempenho. Neste artigo - Eliminando o coletor de lixo: o caminho da RAII , o engenheiro da Toptal, Peter Goodspeed-Niklaus, nos dá uma espiada na história dos coletores de lixo e explica como as noções de propriedade e empréstimo podem ajudar a eliminar os coletores de lixo sem comprometer suas garantias de segurança.

Dmitry Pavlov
fonte