Por que não existe uma construção 'finalmente' em C ++?

57

O tratamento de exceções em C ++ é limitado a tentativa / lançamento / captura. Diferentemente do Object Pascal, Java, C # e Python, mesmo no C ++ 11, a finallyconstrução não foi implementada.

Tenho visto muita literatura C ++ discutindo "código de exceção seguro". Lippman escreve que o código seguro de exceção é um tópico importante, mas avançado e difícil, além do escopo de seu Primer - o que parece implicar que o código seguro não é fundamental para o C ++. Herb Sutter dedica 10 capítulos ao tópico em seu Excepcional C ++!

No entanto, parece-me que muitos dos problemas encontrados ao tentar escrever "código de exceção seguro" poderiam ser bem resolvidos se a finallyconstrução fosse implementada, permitindo ao programador garantir que, mesmo no caso de uma exceção, o programa possa ser restaurado para um estado seguro, estável e sem vazamentos, próximo ao ponto de alocação de recursos e código potencialmente problemático. Como um programador experiente em Delphi e C #, uso try .. finalmente bloqueia bastante o código, assim como a maioria dos programadores nessas linguagens.

Considerando todos os 'sinos e assobios' implementados no C ++ 11, fiquei surpreso ao descobrir que 'finalmente' ainda não estava lá.

Então, por que a finallyconstrução nunca foi implementada em C ++? Não é realmente um conceito muito difícil ou avançado de entender e ajuda bastante o programador a escrever 'código seguro de exceção'.

Vetor
fonte
25
Por que não finalmente? Porque você libera coisas no destruidor que são acionadas automaticamente quando o objeto (ou ponteiro inteligente) sai do escopo. Os destruidores são superiores ao finalmente {}, pois separam o fluxo de trabalho da lógica de limpeza. Assim como você não gostaria que as chamadas liberassem (), sobrecarregando seu fluxo de trabalho em um idioma de coleta de lixo.
Mike30
2
Veja também Os desenvolvedores de Java abandonaram o RAII de forma consciente?
BlueRaja - Danny Pflughoeft
8
Fazendo a pergunta: "Por que não existe finallyno C ++ e quais técnicas para tratamento de exceções são usadas em seu lugar?" é válido e específico para este site. As respostas existentes cobrem isso muito bem, eu acho. Transformando-o em uma discussão sobre "Os motivos dos designers de C ++ para não incluirem finallyvale a pena?" e "Deve finallyser adicionado ao C ++?" e continuando a discussão nos comentários sobre a pergunta e todas as respostas não se encaixam no modelo deste site de perguntas e respostas.
Josh Kelley
2
Se você finalmente tiver, já possui uma separação de preocupações: o principal bloco de código está aqui e a preocupação com a limpeza é resolvida aqui.
Kaz
2
@Kaz. A diferença é implícita versus limpeza explícita. Um destruidor fornece uma limpeza automática semelhante a como um primitivo antigo simples é limpo à medida que sai da pilha. Você não precisa fazer chamadas de limpeza explícitas e pode se concentrar em sua lógica principal. Imagine como seria complicado se você tivesse que limpar as primitivas alocadas da pilha em uma tentativa / finalmente. A limpeza implícita é superior. A comparação da sintaxe da classe com funções anônimas não é relevante. Embora a passagem de funções de primeira classe para uma função que libere um identificador possa centralizar a limpeza manual.
mike30

Respostas:

57

Como alguns comentários adicionais sobre a resposta de @ Nemanja (que, uma vez que cita Stroustrup, é realmente a melhor resposta possível):

É realmente apenas uma questão de entender a filosofia e os idiomas do C ++. Veja o exemplo de uma operação que abre uma conexão com o banco de dados em uma classe persistente e precisa garantir que ela seja fechada se uma exceção for lançada. Isso é uma questão de segurança de exceção e se aplica a qualquer idioma com exceções (C ++, C #, Delphi ...).

Em uma linguagem que usa try/ finally, o código pode se parecer com isso:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Simples e direto. Existem, no entanto, algumas desvantagens:

  • Se a linguagem não possui destruidores determinísticos, eu sempre tenho que escrever o finallybloco, caso contrário, eu vazo recursos.
  • Se DoRiskyOperationfor mais do que uma chamada de método única - se eu tiver algum processamento para fazer no trybloco -, a Closeoperação poderá acabar sendo um pouco decente da Openoperação. Não consigo escrever minha limpeza ao lado da minha aquisição.
  • Se eu tiver vários recursos que precisam ser adquiridos e liberados de uma maneira segura para exceções, posso terminar com várias camadas profundas de try/ finallyblocks.

A abordagem C ++ seria assim:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

Isso resolve completamente todas as desvantagens da finallyabordagem. Ele tem algumas desvantagens, mas são relativamente pequenas:

  • Há uma boa chance de você precisar escrever a ScopedDatabaseConnectionclasse. No entanto, é uma implementação muito simples - apenas 4 ou 5 linhas de código.
  • Isso envolve a criação de uma variável local extra - da qual você aparentemente não é fã, com base no seu comentário sobre "constantemente criando e destruindo classes para confiar em seus destruidores para limpar sua bagunça é muito ruim" - mas um bom compilador otimizará de qualquer trabalho extra que uma variável local extra envolva. Um bom design de C ++ depende muito desses tipos de otimizações.

Pessoalmente, considerando essas vantagens e desvantagens, considero o RAII uma técnica muito preferível finally. Sua milhagem pode variar.

Por fim, como o RAII é um idioma tão bem estabelecido no C ++ e para aliviar os desenvolvedores de alguns dos encargos de escrever várias Scoped...classes, existem bibliotecas como ScopeGuard e Boost.ScopeExit que facilitam esse tipo de limpeza determinística.

Josh Kelley
fonte
8
C # tem a usinginstrução, que limpa automaticamente qualquer objeto que implementa a IDisposableinterface. Portanto, embora seja possível errar, é muito fácil acertar.
Robert Harvey
18
Ter que escrever uma classe inteiramente nova para cuidar da reversão temporária de alteração de estado, usando um idioma de design implementado pelo compilador com uma try/finallyconstrução, porque o compilador não expõe uma try/finallyconstrução e a única maneira de acessá-la é através da classe baseada em classe. idioma de design, não é uma "vantagem"; é a própria definição de uma inversão de abstração.
Mason Wheeler
15
@MasonWheeler - Umm, eu não disse que ter que escrever uma nova classe é uma vantagem. Eu disse que é uma desvantagem. No geral, porém, prefiro o RAII a ter que usar finally. Como eu disse, sua milhagem pode variar.
Josh Kelley
7
@ JoshKelley: 'Um bom design em C ++ depende muito desse tipo de otimização.' Escrever um monte de códigos estranhos e depois confiar na otimização do compilador é um bom design ?! IMO é a antítese do bom design. Entre os fundamentos do bom design está o código conciso e de fácil leitura. Menos para depurar, menos para manter, etc etc etc. Você NÃO deve escrever montes de código e depois confiar no compilador para fazer tudo desaparecer - IMO que não faz sentido!
Vector
14
@ Mikey: Então duplicar o código de limpeza (ou o fato de que a limpeza deve ocorrer) em todo o código-base é "conciso" e "facilmente legível"? Com o RAII, você escreve esse código uma vez e é automaticamente aplicado em qualquer lugar.
Mankarse
55

De Por que o C ++ não fornece uma construção "finalmente"? nas perguntas frequentes sobre estilo e técnica C ++ de Bjarne Stroustrup :

Como o C ++ suporta uma alternativa quase sempre melhor: A técnica "aquisição de recursos é inicialização" (TC ++ PL3, seção 14.4). A idéia básica é representar um recurso por um objeto local, para que o destruidor do objeto local libere o recurso. Dessa forma, o programador não pode esquecer de liberar o recurso.

Nemanja Trifunovic
fonte
5
Mas não há nada nessa técnica específica para C ++, existe? Você pode executar o RAII em qualquer idioma com objetos, construtores e destruidores. É uma ótima técnica, mas a RAII meramente existente não implica que uma finallyconstrução seja sempre inútil para todo o sempre, apesar do que Strousup está dizendo. O mero fato de que escrever "código de exceção seguro" é muito importante em C ++ é a prova disso. Caramba, o C # tem dois destruidores e finally, e ambos se acostumam.
Tacroy
28
@Tacroy: C ++ é uma das poucas linguagens mainstream que possui destruidores determinísticos . Os "destruidores" do C # são inúteis para esse fim, e você precisa escrever manualmente "usando" os blocos para ter o RAII.
Nemanja Trifunovic
15
@ Mikey, você tem a resposta de "Por que o C ++ não fornece uma construção" finalmente "?" diretamente do próprio Stroustrup. O que mais você poderia pedir? Isso é o porquê.
5
@ Mikey Se você se preocupa com o bom funcionamento do seu código, em particular com a falta de recursos, quando são lançadas exceções, você está preocupado com a segurança de exceções / tentando escrever um código seguro de exceção. Você não está chamando assim e, devido à disponibilidade de diferentes ferramentas, você a implementa de maneira diferente. Mas é exatamente disso que as pessoas C ++ falam quando discutem a segurança de exceções.
19
@ Kaz: Só preciso me lembrar de fazer a limpeza no destruidor uma vez e, a partir de então, apenas uso o objeto. Eu preciso me lembrar de fazer a limpeza no bloco final sempre que usar a operação que aloca.
deworde
19

O motivo que o C ++ não possui finallyé porque não é necessário no C ++. finallyé usado para executar algum código, independentemente de uma exceção ocorreu ou não, o que quase sempre é algum tipo de código de limpeza. No C ++, esse código de limpeza deve estar no destruidor da classe relevante e o destruidor sempre será chamado, como um finallybloco. O idioma de usar o destruidor para sua limpeza é chamado RAII .

Dentro da comunidade C ++, pode haver mais discussões sobre o código 'exceção seguro', mas é quase igualmente importante em outros idiomas que possuem exceções. O ponto principal do código 'exceção segura' é que você pensa em que estado seu código é deixado se ocorrer uma exceção em qualquer uma das funções / métodos que você chama.
No C ++, o código 'exceção seguro' é um pouco mais importante, porque o C ++ não possui uma coleta de lixo automática que cuida de objetos que ficam órfãos devido a uma exceção.

A razão pela qual a segurança de exceção é discutida mais na comunidade C ++ provavelmente também decorre do fato de que em C ++ você precisa estar mais ciente do que pode dar errado, porque há menos redes de segurança padrão no idioma.

Bart van Ingen Schenau
fonte
2
Nota: Por favor, não afirme que o C ++ possui destruidores determinísticos. O objeto Pascal / Delphi também possui destruidores determinísticos, mas também suporta 'finalmente', pelas boas razões que expliquei nos meus primeiros comentários abaixo.
Vector
13
@ Mikey: Dado que nunca houve uma proposta para adicionar finallyao padrão C ++, acho que é seguro concluir que a comunidade C ++ não considera the absence of finallyum problema. A maioria das linguagens não possui finallya destruição determinística consistente que o C ++ possui. Vejo que Delphi tem os dois, mas não conheço sua história o suficiente para saber qual foi a primeira.
Bart van Ingen Schenau
3
O Dephi não suporta objetos baseados em pilha - apenas referências baseadas em heap e objeto na pilha. Portanto, 'finalmente' é necessário para chamar explicitamente destruidores, etc., quando apropriado.
Vector
2
Existe uma grande quantidade de sujeira no C ++ que, sem dúvida, não é necessária; portanto, essa não pode ser a resposta certa.
Kaz
15
Nas mais de duas décadas em que usei a linguagem e trabalhei com outras pessoas que a usaram, nunca encontrei um programador C ++ que dissesse "Eu realmente gostaria que a linguagem tivesse um finally". Não me lembro de nenhuma tarefa que teria facilitado, se eu tivesse acesso a ela.
Gort the Robot
12

Outros discutiram o RAII como a solução. É uma solução perfeitamente boa. Mas isso realmente não explica por que eles não adicionaram finallytão bem, já que é uma coisa amplamente desejada. A resposta para isso é mais fundamental para o design e o desenvolvimento de C ++: durante todo o desenvolvimento de C ++, os envolvidos resistiram fortemente à introdução de recursos de design que podem ser alcançados usando outros recursos sem muita confusão e, especialmente, onde isso exige a introdução de novas palavras-chave que poderiam tornar o código antigo incompatível. Como o RAII fornece uma alternativa altamente funcional finallye você pode realmente criar o seu próprio finallyno C ++ 11 de qualquer maneira, houve poucas exigências.

Tudo que você precisa fazer é criar uma classe Finallyque chame a função passada para seu construtor em seu destruidor. Então você pode fazer isso:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

A maioria dos programadores nativos de C ++ prefere, em geral, objetos RAII projetados de maneira limpa.

Jack Aidley
fonte
3
Está faltando a captura de referência no seu lambda. Deve ser Finally atEnd([&] () { database.close(); });também, eu imagino o seguinte é melhor: { Finally atEnd(...); try {...} catch(e) {...} }(I levantou o finalizador para fora do try-bloco para que ele executa após os blocos catch.)
Thomas Eding
2

Você pode usar um padrão de "interceptação" - mesmo que não queira usar o bloco try / catch.

Coloque um objeto simples no escopo necessário. No destruidor desse objeto, coloque sua lógica "final". Não importa o que, quando a pilha for desenrolada, o destruidor do objeto será chamado e você receberá seu doce.

Arie R
fonte
11
Isso não responde à pergunta e simplesmente prova que finalmente não é uma má idéia, afinal ...
Vector
2

Bem, você pode finallycriar o seu próprio método, usando o Lambdas, que compilaria o seguinte (usando um exemplo sem RAII, é claro, não o melhor código):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Veja este artigo .

einpoklum - restabelece Monica
fonte
-2

Não tenho certeza se concordo com as afirmações aqui de que RAII é um superconjunto finally. O calcanhar de Aquiles da RAII é simples: exceções. O RAII é implementado com destruidores e sempre é errado no C ++ jogar fora de um destruidor. Isso significa que você não pode usar RAII quando precisar do seu código de limpeza. Se finallyimplementado, por outro lado, não há razão para acreditar que não seria legal jogar de um finallybloco.

Considere um caminho de código como este:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Se tivéssemos finally, poderíamos escrever:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Mas não há como obter o comportamento equivalente usando RAII.

Se alguém sabe como fazer isso em C ++, estou muito interessado na resposta. Eu ficaria feliz com algo que dependesse, por exemplo, de impor todas as exceções herdadas de uma única classe com alguns recursos especiais ou algo assim.

Cientista maluco
fonte
11
No seu segundo exemplo, se for complex_cleanuppossível lançar, você pode ter um caso em que duas exceções não capturadas estão em andamento ao mesmo tempo, como faria com RAII / destruidores e o C ++ se recusa a permitir isso. Se você deseja que a exceção original seja vista, complex_cleanupevite quaisquer exceções, exatamente como faria com os RAII / destruidores. Se você deseja que complex_cleanupa exceção seja vista, acho que você pode usar blocos try / catch aninhados - embora seja uma tangente e difícil de ajustar em um comentário, vale a pena fazer uma pergunta separada.
Josh Kelley
Eu quero usar RAII para obter um comportamento idêntico ao primeiro exemplo, com mais segurança. Um arremesso em um finallybloco putativo claramente funcionaria da mesma forma que um arremesso em um catchbloco WRT em exceções em voo - não em chamada std::terminate. A questão é "por que não finallyem C ++?" e todas as respostas dizem "você não precisa disso ... RAII FTW!" O que quero dizer é que sim, o RAII é bom para casos simples como gerenciamento de memória, mas até que o problema das exceções seja resolvido, é necessário muito pensamento / sobrecarga / preocupação / reprojeto para ser uma solução de uso geral.
Cientista louco
3
Entendo o seu ponto - existem alguns problemas legítimos com destruidores que podem dar errado - mas esses são raros. Dizer que as exceções do RAII + tem problemas não resolvidos ou que o RAII não é uma solução de uso geral simplesmente não corresponde à experiência da maioria dos desenvolvedores de C ++.
Josh Kelley
11
Se você se deparar com a necessidade de criar exceções em destruidores, estará fazendo algo errado - provavelmente usando ponteiros em outros lugares quando não forem necessários.
Vector
11
Isso está muito envolvido para comentários. Poste uma pergunta sobre isso: Como você lidaria com esse cenário em C ++ usando o modelo RAII ... ele não parece funcionar ... Novamente, você deve direcionar seus comentários : digite @ e nome do membro que você está falando no início do seu comentário. Quando os comentários estão em sua própria postagem, você é notificado de tudo, mas outros não, a menos que você os encaminhe.
Vector