Na prática com C ++, o que é RAII , o que são indicadores inteligentes , como eles são implementados em um programa e quais são os benefícios de usar o RAII com indicadores inteligentes?
Na prática com C ++, o que é RAII , o que são indicadores inteligentes , como eles são implementados em um programa e quais são os benefícios de usar o RAII com indicadores inteligentes?
Respostas:
Um exemplo simples (e talvez em excesso) de RAII é uma classe 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 garantir que fechemos o arquivo assim que terminarmos. Isso tem duas desvantagens - primeiro, onde quer que usemos File, teremos que chamar File :: close () - se esquecermos de fazer isso, manteremos o arquivo por mais tempo do que precisamos. O segundo problema é o que acontece se uma exceção é lançada antes de fecharmos 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();
}
ou desde o Java 7, uma instrução try-with-resource:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ resolve os dois problemas usando RAII - ou seja, fechando o arquivo no destruidor de File. Desde que o objeto File seja destruído no momento certo (o que deveria ser de qualquer maneira), o fechamento do arquivo será resolvido por nós. Portanto, nosso código agora se parece com:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Isso não pode ser feito em Java, pois não há garantia de quando o objeto será destruído, portanto, não podemos garantir quando um recurso como arquivo será liberado.
Para ponteiros inteligentes - na maioria das vezes, apenas criamos objetos na pilha. Por exemplo (e roubar 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? 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 nisso? 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 evitar o custo de copiá-lo. Portanto, podemos ter a idéia de 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, portanto seremos excluídos quando sairmos de foo (). Em outras palavras, quando o chamador recebe o ponteiro, ele é inútil (e sem dúvida pior do que inútil, pois usá-lo pode causar todos os tipos de erros estranhos)
Então, qual é a solução? Poderíamos criar str no heap usando new - assim, quando foo () for concluído, o str não será destruído.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Obviamente, essa 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 excluiremos. Poderíamos dizer que o chamador deve excluir o objeto depois que ele terminar. A desvantagem é que o chamador precisa gerenciar a memória, o que aumenta a complexidade e pode causar erros, levando a um vazamento de memória, ou seja, não excluindo objetos, mesmo que não seja mais necessário.
É aqui que entram os ponteiros inteligentes. O exemplo a seguir usa shared_ptr - sugiro que você analise os diferentes tipos de ponteiros inteligentes para aprender o que realmente deseja usar.
shared_ptr<std::string> foo() {
shared_ptr<std::string> 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<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Agora, existem duas referências à mesma sequência. Quando não houver referências restantes para str, ele será excluído. Como tal, você não precisa mais se preocupar com a exclusão.
Edição rápida: como alguns dos comentários apontaram, este exemplo não é perfeito por (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 do valor de retorno nomeado, o retorno por valor pode não ser caro, uma vez que o compilador pode ser inteligente para acelerar as coisas.
Então, vamos tentar um exemplo diferente usando nossa classe File.
Digamos que queremos usar um arquivo como um log. Isso significa que queremos abrir nosso arquivo no modo somente 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 objetos:
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 horrivelmente - o arquivo será fechado assim que esse método terminar, o que significa que foo e bar agora têm um arquivo de log inválido. Nós poderíamos construir o arquivo na pilha 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 quem é responsável pela exclusão do arquivo? Se nenhum dos arquivos for excluído, haverá um vazamento de memória e recurso. Não sabemos se foo ou bar terminará primeiro com o arquivo; portanto, não podemos esperar que eles sejam excluídos. Por exemplo, se foo excluir o arquivo antes que a barra termine, agora a barra possui um ponteiro inválido.
Então, como você deve ter adivinhado, poderíamos usar indicadores inteligentes para nos ajudar.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Agora, ninguém precisa se preocupar em excluir um arquivo - depois que o foo e a barra terminarem e não tiver mais referências ao arquivo (provavelmente devido à destruição do foo e da barra), o arquivo será excluído automaticamente.
RAII Este é um nome estranho para um conceito simples, mas impressionante. Melhor é o nome Gerenciamento de recursos vinculados ao escopo (SBRM). A idéia é que, com frequência, você aloca recursos no início de um bloco e precisa liberá-lo na saída de um bloco. A saída do bloco pode ocorrer pelo controle de fluxo normal, saltando 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ê vê, existem muitas maneiras pelas quais podemos nos envolver. A idéia é que encapsulemos o gerenciamento de recursos em uma classe. A inicialização do seu objeto adquire o recurso ("Aquisição de recursos é inicialização"). No momento em que saímos do bloco (escopo do 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ê tiver classes próprias que não sejam apenas para alocar / desalocar recursos. A alocação seria apenas uma preocupação adicional para realizar seu trabalho. Porém, assim que você deseja alocar / desalocar recursos, o exposto acima se torna impraticável. Você precisa escrever uma classe de empacotamento para todo tipo de recurso que adquirir. Para facilitar, ponteiros inteligentes permitem automatizar esse processo:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
Normalmente, ponteiros inteligentes são invólucros finos em torno de novos / excluídos que são chamados delete
quando o recurso que eles possuem sai do escopo. Alguns indicadores inteligentes, como shared_ptr, permitem que você diga o chamado deleter, que é usado em vez de delete
. Isso permite, por exemplo, gerenciar identificadores de janela, recursos de expressão regular e outras coisas arbitrárias, desde que você informe o shared_ptr sobre o deleter correto.
Existem diferentes indicadores inteligentes para diferentes propósitos:
Código:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
Diferente do auto_ptr, o 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 fluxos e unique_ptr também.
Código:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
Código:
shared_ptr<plot_src> 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ê vê, a fonte de plotagem (função fx) é compartilhada, mas cada uma possui uma entrada separada, na qual definimos a cor. Há uma classe weak_ptr que é usada quando o código precisa se referir ao recurso pertencente a um ponteiro inteligente, mas não precisa ser o proprietário do recurso. Em vez de passar um ponteiro bruto, você deve criar um fraca_ptr. Ele emitirá uma exceção quando perceber que você tenta acessar o recurso por um caminho de acesso weak_ptr, mesmo que não haja mais shared_ptr que possua o recurso.
unique_ptr
, e sort
também serão alterados da mesma forma.
RAII é o paradigma de design para garantir que as variáveis manipulem toda a inicialização necessária em seus construtores e toda a limpeza necessária em seus destruidores. Isso reduz toda inicialização e limpeza em uma única etapa.
O C ++ não requer RAII, mas é cada vez mais aceito que o uso de métodos RAII produzirá código mais robusto.
A razão pela qual a RAII é útil no C ++ é que o C ++ gerencia intrinsecamente a criação e a destruição de variáveis à medida que elas entram e saem do escopo, seja por meio do fluxo normal de código ou pelo desenrolamento da pilha acionado por uma exceção. Isso é um brinde em C ++.
Ao vincular toda a inicialização e limpeza a esses mecanismos, você garante que o C ++ cuidará desse trabalho também.
Falar sobre RAII em C ++ geralmente leva à discussão de ponteiros inteligentes, porque os ponteiros são particularmente frágeis quando se trata de limpeza. Ao gerenciar a memória alocada a heap adquirida da malloc ou nova, geralmente é responsabilidade do programador liberar ou excluir essa memória antes que o ponteiro seja destruído. Ponteiros inteligentes usarão a filosofia RAII para garantir que os objetos alocados pelo heap sejam destruídos sempre que a variável do ponteiro for destruída.
Ponteiro inteligente é uma variação do RAII. RAII significa aquisição de recursos é inicialização. O ponteiro inteligente adquire um recurso (memória) antes do uso e o joga fora automaticamente em um destruidor. Duas coisas acontecem:
Por exemplo, outro exemplo é o soquete de rede RAII. Nesse caso:
Agora, como você pode ver, o RAII é uma ferramenta muito útil na maioria dos casos, pois ajuda as pessoas a transar.
As fontes C ++ de ponteiros inteligentes estão em milhões na rede, incluindo respostas acima de mim.
O Boost possui vários deles, incluindo os do Boost.Interprocess para memória compartilhada. Ele simplifica bastante o gerenciamento de memória, especialmente em situações que causam dor de cabeça, como quando você tem 5 processos compartilhando a mesma estrutura de dados: quando todo mundo termina com um pedaço de memória, você quer que ele seja liberado automaticamente e não precisa ficar parado tentando descobrir quem deve ser responsável por chamar delete
um pedaço de memória, para que não ocorra um vazamento de memória ou um ponteiro que seja liberado por engano duas vezes e possa corromper toda a pilha.
void foo () { barra std :: string; // // mais código aqui // }
Não importa o que aconteça, a barra será excluída corretamente assim que o escopo da função foo () for deixado para trás.
As implementações internamente std :: string geralmente usam ponteiros contados por referência. Portanto, a cadeia interna só precisa ser copiada quando uma das cópias das cadeias foi alterada. Portanto, um ponteiro inteligente contado como referência torna possível copiar apenas algo quando necessário.
Além disso, a contagem de referência interna torna possível que a memória seja excluída corretamente quando a cópia da sequência interna não for mais necessária.