RAII e pointers inteligentes em C ++

Na prática, com C ++, o que é RAII , quais são os pointers inteligentes , como eles são implementados em um programa e quais são os benefícios de usar o RAII com pointers inteligentes?

Um exemplo simples (e talvez excessivamente usado) de RAII é uma class File. Sem RAII, o código pode ser algo como isto:

File file("/path/to/file"); // Do stuff with file file.close(); 

Em outras palavras, devemos nos certificar de que fechamos o arquivo assim que terminarmos. Isto tem duas desvantagens – em primeiro lugar, sempre que usarmos File, teremos que chamar File :: close () – se nos esquecermos de fazer isso, estaremos segurando o arquivo por mais tempo do que precisamos. O segundo problema é o que acontece se uma exceção é lançada antes de fechar o arquivo?

Java resolve o segundo problema usando uma cláusula finally:

 try { File file = new File("/path/to/file"); // Do stuff with file } finally { file.close(); } 

C ++ resolve os dois problemas usando RAII – isto é, fechando o arquivo no destruidor de File. Contanto que o object File seja destruído no momento certo (que deveria ser assim mesmo), fechar o arquivo é feito para nós. Então, nosso código agora se parece com algo como:

 File file("/path/to/file"); // Do stuff with file // No need to close it - destructor will do that for us 

A razão pela qual isso não pode ser feito em Java é que não temos garantia sobre quando o object será destruído, portanto, não podemos garantir quando um recurso como um arquivo será liberado.

Em pointers inteligentes – na maioria das vezes, criamos objects na pilha. Por exemplo (e roubando um exemplo de outra resposta):

 void foo() { std::string str; // Do cool things to or using str } 

Isso funciona bem – mas e se quisermos retornar str? Nós poderíamos escrever isso:

 std::string foo() { std::string str; // Do cool things to or using str return str; } 

Então, o que há de errado com isso? Bem, o tipo de retorno é std :: string – então isso significa que estamos retornando por valor. Isso significa que copiamos str e realmente retornamos a cópia. Isso pode ser caro e podemos querer evitar o custo de copiá-lo. Portanto, podemos pensar em retornar por referência ou por ponteiro.

 std::string* foo() { std::string str; // Do cool things to or using str return &str; } 

Infelizmente, esse código não funciona. Estamos retornando um ponteiro para str – mas str foi criado na pilha, então seremos deletados assim que sairmos de foo (). Em outras palavras, quando o chamador pega o ponteiro, é inútil (e provavelmente pior do que inútil, já que usá-lo pode causar todos os tipos de erros)

Então, qual é a solução? Poderíamos criar str no heap usando new – assim, quando foo () for concluído, str não será destruído.

 std::string* foo() { std::string* str = new std::string(); // Do cool things to or using str return str; } 

Claro, esta solução também não é perfeita. O motivo é que criamos str, mas nunca o excluímos. Isso pode não ser um problema em um programa muito pequeno, mas, em geral, queremos ter certeza de que o apagamos. Poderíamos apenas dizer que o chamador deve excluir o object assim que ele terminar. A desvantagem é que o chamador tem que gerenciar a memory, o que adiciona complexidade extra e pode estar errado, levando a um memory leaks, ou seja, não excluindo o object, mesmo que ele não seja mais necessário.

É aí que entram os pointers inteligentes. O exemplo a seguir usa shared_ptr – sugiro que você observe os diferentes tipos de pointers inteligentes para aprender o que realmente deseja usar.

 shared_ptr foo() { shared_ptr str = new std::string(); // Do cool things to or using str return str; } 

Agora, shared_ptr contará o número de referências a str. Por exemplo

 shared_ptr str = foo(); shared_ptr str2 = str; 

Agora existem duas referências à mesma string. Uma vez que não há referências remanescentes para str, ele será excluído. Assim, você não precisa mais se preocupar em excluí-lo sozinho.

Edição rápida: como alguns dos comentários apontaram, este exemplo não é perfeito para (pelo menos!) Duas razões. Em primeiro lugar, devido à implementação de strings, copiar uma string tende a ser barato. Em segundo lugar, devido ao que é conhecido como otimização de valor de retorno nomeado, retornar por valor pode não ser caro, pois o compilador pode fazer alguma habilidade para acelerar as coisas.

Então, vamos tentar um exemplo diferente usando nossa class File.

Digamos que queremos usar um arquivo como um log. Isso significa que queremos abrir nosso arquivo apenas no modo de acréscimo:

 File file("/path/to/file", File::append); // The exact semantics of this aren't really important, // just that we've got a file to be used as a log 

Agora, vamos definir nosso arquivo como o log de alguns outros objects:

 void setLog(const Foo & foo, const Bar & bar) { File file("/path/to/file", File::append); foo.setLogFile(file); bar.setLogFile(file); } 

Infelizmente, este exemplo termina terrivelmente – o arquivo será fechado assim que este método terminar, significando que foo e bar agora possuem um arquivo de log inválido. Poderíamos construir um arquivo no heap e passar um ponteiro para o arquivo foo e bar:

 void setLog(const Foo & foo, const Bar & bar) { File* file = new File("/path/to/file", File::append); foo.setLogFile(file); bar.setLogFile(file); } 

Mas então quem é responsável por apagar o arquivo? Se não excluir o arquivo, teremos um memory leaks e de resources. Nós não sabemos se foo ou bar terminará com o arquivo primeiro, então não podemos esperar excluir o arquivo. Por exemplo, se foo excluir o arquivo antes de a barra terminar, a barra agora terá um ponteiro inválido.

Então, como você deve ter adivinhado, poderíamos usar pointers inteligentes para nos ajudar.

 void setLog(const Foo & foo, const Bar & bar) { shared_ptr file = new File("/path/to/file", File::append); foo.setLogFile(file); bar.setLogFile(file); } 

Agora, ninguém precisa se preocupar com a exclusão de arquivos – uma vez que foo e bar tenham terminado e não tenham mais nenhuma referência ao arquivo (provavelmente devido a foo e bar serem destruídos), o arquivo será automaticamente excluído.

RAII Este é um nome estranho para um conceito simples, mas impressionante. Melhor é o nome Scope Bound Resource Management (SBRM). A idéia é que muitas vezes você aloca resources no início de um bloco e precisa liberá-lo na saída de um bloco. Sair do bloco pode acontecer pelo controle de stream normal, pular fora dele e até mesmo por uma exceção. Para cobrir todos esses casos, o código se torna mais complicado e redundante.

Apenas um exemplo fazendo isso sem o SBRM:

 void o_really() { resource * r = allocate_resource(); try { // something, which could throw. ... } catch(...) { deallocate_resource(r); throw; } if(...) { return; } // oops, forgot to deallocate deallocate_resource(r); } 

Como você pode ver, existem muitas maneiras de nos ajudar. A ideia é que encapsulemos o gerenciamento de resources em uma class. Inicialização do seu object adquire o recurso (“Aquisição de resources é boot”). No momento em que saímos do bloco (escopo de bloco), o recurso é liberado novamente.

 struct resource_holder { resource_holder() { r = allocate_resource(); } ~resource_holder() { deallocate_resource(r); } resource * r; }; void o_really() { resource_holder r; // something, which could throw. ... if(...) { return; } } 

Isso é bom se você tem classs próprias que não são apenas para fins de alocação / desalocação de resources. A alocação seria apenas uma preocupação adicional para realizar seu trabalho. Mas, assim que você quiser apenas alocar / desalocar resources, o que está acima se torna imprevisível. Você tem que escrever uma class de quebra para cada tipo de recurso que você adquire. Para facilitar isso, os pointers inteligentes permitem que você automatize esse processo:

 shared_ptr create_entry(Parameters p) { shared_ptr e(Entry::createEntry(p), &Entry::freeEntry); return e; } 

Normalmente, os pointers inteligentes são wrappers finos em volta de new / delete que simplesmente chamam de delete quando o recurso que possuem sai de escopo. Alguns pointers inteligentes, como shared_ptr, permitem que você diga a eles um chamado deleter, que é usado em vez de delete . Isso permite, por exemplo, gerenciar identificadores de janelas, resources de expressões regulares e outras coisas arbitrárias, contanto que você diga shared_ptr sobre o deletério certo.

Existem diferentes pointers inteligentes para diferentes fins:

unique_ptr

é um ponteiro inteligente que possui um object exclusivamente. Não está no impulso, mas provavelmente aparecerá no próximo C ++ Standard. Não é copiável, mas suporta a transferência de propriedade . Algum código de exemplo (próximo C ++):

Código:

 unique_ptr p(new plot_src); // now, p owns unique_ptr u(move(p)); // now, u owns, p owns nothing. unique_ptr v(u); // error, trying to copy u vector> pv; pv.emplace_back(new plot_src); pv.emplace_back(new plot_src); 

Ao contrário de auto_ptr, unique_ptr pode ser colocado em um contêiner, porque os contêineres poderão conter tipos não-copiáveis ​​(mas móveis), como streams e unique_ptr também.

scoped_ptr

é um ponteiro inteligente de impulso que não é nem copiável nem móvel. É a coisa perfeita para ser usada quando você quer ter certeza de que os pointers são apagados quando sair do escopo.

Código:

 void do_something() { scoped_ptr sp(new pipe); // do something here... } // when going out of scope, sp will delete the pointer automatically. 

shared_ptr

é para propriedade compartilhada. Por isso, é copiável e móvel. Várias instâncias de ponteiro inteligente podem ter o mesmo recurso. Assim que o último ponteiro inteligente que possui o recurso ficar fora do escopo, o recurso será liberado. Algum exemplo do mundo real de um dos meus projetos:

Código:

 shared_ptr p(new plot_src(&fx)); plot1->add(p)->setColor("#00FF00"); plot2->add(p)->setColor("#FF0000"); // if p now goes out of scope, the src won't be freed, as both plot1 and // plot2 both still have references. 

Como você pode ver, a fonte da plotagem (function fx) é compartilhada, mas cada uma tem uma input separada, na qual definimos a cor. Há uma class weak_ptr que é usada quando o código precisa se referir ao recurso pertencente a um ponteiro inteligente, mas não precisa possuir o recurso. Em vez de passar um ponteiro bruto, você deve criar um weak_ptr. Ele lançará uma exceção quando perceber que você tentou acessar o recurso por um caminho de access weak_ptr, mesmo que não haja mais shared_ptr possuindo o recurso.

A premissa e as razões são simples, em conceito.

O RAII é o paradigma de design para garantir que as variables ​​lidem com todas as inicializações necessárias em seus construtores e todas as limpezas necessárias em seus destruidores. Isso reduz toda a boot e limpeza em uma única etapa.

O C ++ não requer RAII, mas é cada vez mais aceito que o uso de methods RAII produzirá um código mais robusto.

A razão pela qual o RAII é útil em C ++ é que o C ++ gerencia intrinsecamente a criação e a destruição de variables ​​conforme elas entram e saem do escopo, seja através do stream de código normal ou através do desenrolamento da pilha acionado por uma exceção. Isso é um brinde em C ++.

Ao vincular toda a boot e limpeza a esses mecanismos, você garante que o C ++ cuide desse trabalho também.

Falar sobre RAII em C ++ geralmente leva à discussão de pointers inteligentes, porque os pointers são particularmente frágeis quando se trata de limpeza. Ao gerenciar a memory alocada por heap adquirida do malloc ou new, geralmente é responsabilidade do programador liberar ou excluir essa memory antes que o ponteiro seja destruído. Os pointers inteligentes usarão a filosofia RAII para garantir que os objects alocados por heap sejam destruídos sempre que a variável ponteiro for destruída.

Ponteiro inteligente é uma variação do RAII. RAII significa aquisição de resources é boot. Ponteiro inteligente adquire um recurso (memory) antes do uso e, em seguida, joga fora automaticamente em um destruidor. Duas coisas acontecem:

  1. Alocamos a memory antes de usá-la, sempre, mesmo quando não nos sentimos assim – é difícil fazer outra maneira com um ponteiro inteligente. Se isso não estava acontecendo, você tentará acessar a memory NULL, resultando em uma falha (muito dolorosa).
  2. Nós liberamos memory mesmo quando há um erro. Não há memory pendurada.

Por exemplo, outro exemplo é o soquete de rede RAII. Nesse caso:

  1. Nós abrimos o socket de rede antes de usá-lo, sempre, mesmo quando não nos sentimos assim – é difícil fazer isso de outra forma com o RAII. Se você tentar fazer isso sem o RAII, pode abrir um soquete vazio para, digamos, a conexão do MSN. Então, mensagens como “vamos fazer hoje à noite” podem não ser transferidas, os usuários não vão transar e você corre o risco de ser demitido.
  2. Fechamos o soquete de rede mesmo quando há um erro. Nenhum soquete é deixado pendurado, já que isso pode impedir que a mensagem de resposta “sure ill be be bottom” (“na parte inferior”) atinja o remetente.

Agora, como você pode ver, RAII é uma ferramenta muito útil na maioria dos casos, pois ajuda as pessoas a transar.

Fontes C ++ de pointers inteligentes estão em milhões ao redor da rede, incluindo respostas acima de mim.

O Boost tem vários deles, incluindo os do Boost . Interprocessar para a memory compartilhada. Isso simplifica muito o gerenciamento de memory, especialmente em situações indutoras de dor de cabeça, como quando você tem 5 processos compartilhando a mesma estrutura de dados: quando todos terminam com um pedaço de memory, você quer que ele seja liberado automaticamente e não precise ficar sentado tentando descobrir quem deve ser responsável por chamar delete em um pedaço de memory, para que você não acabe com um memory leaks, ou um ponteiro que é erroneamente liberado duas vezes e pode corromper todo o heap.

 void foo ()
 {
    barra std :: string;
    //
    // mais código aqui
    //
 }

Não importa o que aconteça, a barra será apagada corretamente quando o escopo da function foo () for deixado para trás.

Internamente, as implementações std :: string geralmente usam pointers de referência contados. Portanto, a string interna só precisa ser copiada quando uma das cópias das strings é alterada. Portanto, um ponteiro inteligente contado de referência torna possível copiar apenas algo quando necessário.

Além disso, a contagem de referência interna possibilita que a memory seja adequadamente excluída quando a cópia da sequência interna não for mais necessária.