Qual é a diferença conceitual entre finalmente e um destruidor?

12

Primeiro, estou ciente de Por que não existe uma construção 'finalmente' em C ++? mas uma longa discussão de comentários sobre outra questão parece justificar uma pergunta separada.

Além do problema que finallyem C # e Java pode existir basicamente apenas uma vez (== 1) por escopo e um único escopo pode ter vários (== n) destruidores de C ++, acho que eles são essencialmente a mesma coisa. (Com algumas diferenças técnicas.)

No entanto, outro usuário argumentou :

... Eu estava tentando dizer que um dtor é inerentemente uma ferramenta para (Release semântica) e, finalmente, é inerentemente uma ferramenta para (Commit semântica). Se você não vê o porquê: considere por que é legítimo lançar exceções umas sobre as outras em blocos finalmente e por que o mesmo não é para destruidores. (Em certo sentido, trata-se de dados versus controle. Destrutores são para liberar dados, finalmente é para liberar controle. Eles são diferentes; é lamentável que o C ++ os una.)

Alguém pode esclarecer isso?

Martin Ba
fonte

Respostas:

6
  • Transação ( try)
  • Saída / resposta de erro ( catch)
  • Erro externo ( throw)
  • Erro do programador ( assert)
  • Reversão (o mais próximo pode ser proteção de escopo em idiomas que os suportam nativamente)
  • Liberando recursos (destruidores)
  • Fluxo de controle independente de transação diversa ( finally)

Não é possível apresentar uma descrição melhor do finallyque o fluxo de controle independente de transações diversas. Ele não necessariamente mapeia tão diretamente para qualquer conceito de alto nível no contexto de uma mentalidade de transação e recuperação de erros, especialmente em uma linguagem teórica que possui destruidores e finally.

O que me falta mais inerentemente é um recurso de linguagem que representa diretamente o conceito de reverter efeitos colaterais externos. Guardas de escopo em linguagens como D são a coisa mais próxima que consigo pensar que chega perto de representar esse conceito. Do ponto de vista do fluxo de controle, uma reversão no escopo de uma função específica precisaria distinguir um caminho excepcional de um regular, enquanto simultaneamente automatiza a reversão implicitamente de quaisquer efeitos colaterais causados ​​pela função, caso a transação falhe, mas não quando a transação for bem-sucedida. . Isso é fácil o suficiente com destruidores se, digamos, definirmos um valor booleano como succeededtrue no final de nosso bloco try, para impedir a lógica de reversão em um destruidor. Mas é uma maneira bastante indireta de fazer isso.

Embora possa parecer que isso não pouparia muito, a reversão de efeitos colaterais é uma das coisas mais difíceis de corrigir (ex: o que torna tão difícil escrever um contêiner genérico seguro para exceções).


fonte
4

De certa forma, eles são - da mesma maneira que uma Ferrari e um trânsito podem ser usados ​​para beliscar as lojas por um litro de leite, mesmo que sejam projetados para diferentes usos.

Você pode colocar uma construção try / finalmente em todos os escopos e limpar todas as variáveis ​​definidas no escopo finalmente para emular um destruidor de C ++. Conceitualmente, é isso que o C ++ faz - o compilador chama automaticamente o destruidor quando uma variável sai do escopo (ou seja, no final do bloco do escopo). Você teria que organizar sua tentativa / finalmente, para que a tentativa seja a primeira coisa e, finalmente, a última em cada escopo. Você também teria que definir um padrão para cada objeto para ter um método de nome específico que ele usa para limpar seu estado que você chamaria no bloco final, embora eu ache que você possa deixar o gerenciamento de memória normal fornecido pelo seu idioma. limpe o objeto agora esvaziado quando quiser.

No entanto, não seria fácil fazer isso, e mesmo que o .NET tenha introduzido o IDispose como um destruidor gerenciado manualmente e usando blocos como uma tentativa de tornar esse gerenciamento manual um pouco mais fácil, ainda não é algo que você gostaria de fazer na prática. .

gbjbaanb
fonte
4

Do meu ponto de vista, a principal diferença é que um destruidor em c ++ é um mecanismo implícito (invocado automaticamente) para liberar recursos alocados enquanto a tentativa ... finalmente pode ser usada como um mecanismo explícito para fazer isso.

Nos programas c ++, o programador é responsável por liberar recursos alocados. Isso geralmente é implementado no destruidor de uma classe e é feito imediatamente quando uma variável sai do escopo ou quando a exclusão é chamada.

Quando em c ++, uma variável local de uma classe é criada sem o uso newdos recursos dessas instâncias são liberadas implícitas pelo destruidor quando há uma exceção.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

Em java, c # e outros sistemas com gerenciamento automático de memória, o coletor de lixo dos sistemas decide quando uma instância de classe é destruída.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Não há mecanismo implícito para isso, então você deve programar isso explicitamente usando try

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}
k3b
fonte
No C ++, por que o destruidor é chamado automaticamente apenas com um objeto de pilha e não com um objeto de heap no caso de uma exceção?
Giorgio
@ Giorgio Porque os recursos de heap ficam em um espaço de memória que não está diretamente vinculado à pilha de chamadas. Por exemplo, imagine um aplicativo multithread com 2 threads Ae B. Se um encadeamento for lançado, a reversão da A'stransação não deve destruir os recursos alocados B, por exemplo - os estados do encadeamento são independentes um do outro e a memória persistente que permanece no heap é independente de ambos. No entanto, normalmente em C ++, a memória heap ainda está vinculada a objetos na pilha.
@Giorgio Por exemplo, um std::vectorobjeto pode viver na pilha, mas apontar para a memória na pilha - tanto o objeto de vetor (na pilha) quanto seu conteúdo (na pilha) seriam desalocados durante o desenrolar da pilha nesse caso, pois destruir o vetor na pilha invocaria um destruidor que libera a memória associada no heap (e também destrói esses elementos de heap). Normalmente, para segurança de exceção, a maioria dos objetos C ++ vive na pilha, mesmo que sejam apenas identificadores que apontam para a memória na pilha, automatizando o processo de liberação da memória da pilha e da pilha na pilha.
4

Que bom que você postou isso como uma pergunta. :)

Eu estava tentando dizer que destruidores e finallysão conceitualmente diferentes:

  • Destrutores são para liberar recursos ( dados )
  • finallyé para retornar ao chamador ( controle )

Considere, digamos, este pseudocódigo hipotético:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyaqui está resolvendo inteiramente um problema de controle e não um problema de gerenciamento de recursos.
Não faria sentido fazer isso em um destruidor por vários motivos:

  • Não coisa está sendo "adquirido" ou "criado"
  • A falha na impressão no arquivo de log não resultará em vazamentos de recursos, corrupção de dados etc. (supondo que o arquivo de log aqui não seja retornado ao programa em outro local)
  • É legítimo logfile.printfracassar, enquanto a destruição (conceitualmente) não pode fracassar

Aqui está outro exemplo, desta vez como em Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

No exemplo acima, novamente, não há recursos a serem liberados.
De fato, o finallybloco está adquirindo recursos internamente para atingir seu objetivo, o que poderia falhar potencialmente. Portanto, não faz sentido usar um destruidor (se o Javascript tiver um).

Por outro lado, neste exemplo:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyestá destruindo um recurso b,. É um problema de dados. O problema não é devolver o controle corretamente ao chamador, mas evitar vazamentos de recursos.
Falha não é uma opção e nunca deve (conceitualmente) ocorrer.
Cada lançamento de bé necessariamente associado a uma aquisição e faz sentido usar o RAII.

Em outras palavras, apenas porque você pode usar para simular ou isso não significa que ambos são um e o mesmo problema ou que ambos são soluções apropriadas para os dois problemas.

user541686
fonte
Obrigado. Eu discordo, mas ei :-) Eu acho que poderei adicionar uma resposta minuciosa da visão oposta nos próximos dias ...
Martin Ba
2
Como o fato de que finallyé usado principalmente para liberar recursos (que não são de memória) afeta isso?
Bart van Ingen Schenau
1
@BartvanIngenSchenau: Eu nunca argumentei que qualquer idioma atualmente existente tenha uma filosofia ou implementação que corresponda ao que eu descrevi. As pessoas não terminaram de inventar tudo o que poderia existir ainda. Argumentei apenas que haveria valor em separar as duas noções, pois são idéias diferentes e têm casos de uso diferentes. Para satisfazer sua curiosidade, acredito que D tem os dois. Provavelmente também existem outros idiomas. Porém, não considero relevante e não me importo com o porquê, por exemplo, do Java ser a favor finally.
user541686
1
Um exemplo prático que encontrei no JavaScript são funções que alteram temporariamente o ponteiro do mouse para uma ampulheta durante alguma operação demorada (que pode gerar uma exceção) e, em seguida, voltam ao normal na finallycláusula. A visão de mundo do C ++ apresentaria uma classe que gerencia esse "recurso" de uma atribuição a uma variável pseudo-global. Que sentido conceitual isso faz? Mas os destruidores são o martelo do C ++ para a execução necessária do código de fim de bloco.
dan04
1
@ dan04: Muito obrigado, esse é o exemplo perfeito para isso. Eu poderia jurar que me deparei com tantas situações em que a RAII não fazia sentido, mas tive muita dificuldade em pensar nelas.
user541686
1

A resposta do k3b realmente expressa bem:

um destruidor em c ++ é um mecanismo implícito (invocado automaticamente) para liberar recursos alocados enquanto a tentativa ... finalmente pode ser usada como um mecanismo explícito para fazer isso.

Quanto a "recursos", gosto de me referir a Jon Kalb: RAII deve significar aquisição de responsabilidade é inicialização .

De qualquer forma, quanto ao implícito versus explícito, isso parece realmente ser:

  • Um d'tor é uma ferramenta para definir quais operações devem acontecer - implicitamente - quando a vida útil de um objeto termina (o que geralmente coincide com o final do escopo)
  • Um bloco de finalmente é uma ferramenta para definir - explicitamente - quais operações devem acontecer no final do escopo.
  • Além disso, tecnicamente, você sempre pode jogar de finalmente, mas veja abaixo.

Eu acho que é isso para a parte conceitual, ...


... agora há IMHO alguns detalhes interessantes:

Também não acho que o c'tor / d'tor precise conceitualmente "adquirir" ou "criar" qualquer coisa, além da responsabilidade de executar algum código no destruidor. Qual é o que finalmente também faz: execute algum código.

E, embora o código em um bloco finalmente possa certamente gerar uma exceção, isso não é suficiente para dizer que eles são conceitualmente diferentes acima de explícito versus implícito.

(Além disso, não estou convencido de que o código "bom" deva ser lançado finalmente - talvez essa seja outra questão.)

Martin Ba
fonte
O que você achou do meu exemplo de Javascript?
user541686
Em relação aos seus outros argumentos: "Nós realmente queremos registrar a mesma coisa independentemente?" Sim, é apenas um exemplo e você está meio que perdendo o objetivo, e sim, ninguém nunca é proibido de registrar detalhes mais específicos para cada caso. O ponto aqui é que você certamente não pode alegar que nunca há uma situação na qual você gostaria de registrar algo comum a ambos. Algumas entradas de log são genéricas, outras são específicas; você quer os dois. E, novamente, você está meio que perdendo completamente o objetivo, concentrando-se no registro. Motivar exemplos de 10 linhas é difícil; por favor, tente não perder o ponto.
user541686
Você nunca endereçou estes ...
user541686
@ Mehrdad - Não mencionei o seu exemplo de javascript, porque levaria outra página para discutir o que penso dele. (Eu tentei, mas ele me levou tanto tempo, a frase algo coerente que eu saltei-lo :-)
Martin Ba
@Mehrdad - quanto aos seus outros pontos - parece que temos que concordar em discordar. Vejo para onde você está buscando a diferença, mas não estou convencido de que elas sejam algo conceitualmente diferente: principalmente porque estou principalmente no campo que acha que jogar de finalmente é uma péssima ideia ( nota : eu também pense no seu observerexemplo que jogar seria uma péssima ideia.) Sinta-se à vontade para abrir um bate-papo, se quiser discutir mais sobre isso. Certamente foi divertido pensar nos seus argumentos. Felicidades.
Martin Ba