Existem várias maneiras, mas primeiro você precisa entender por que a limpeza de objetos é importante e, portanto, o motivo std::exit
é marginalizado entre os programadores de C ++.
RAII e desbobinamento de pilha
O C ++ faz uso de um idioma chamado RAII , que em termos simples significa que os objetos devem executar a inicialização no construtor e a limpeza no destruidor. Por exemplo, a std::ofstream
classe [pode] abrir o arquivo durante o construtor, o usuário realiza operações de saída e, finalmente, no final de seu ciclo de vida, geralmente determinado por seu escopo, é chamado o destruidor que essencialmente fecha o arquivo e libera qualquer conteúdo escrito no disco.
O que acontece se você não chegar ao destruidor para liberar e fechar o arquivo? Quem sabe! Mas, possivelmente, ele não gravará todos os dados que deveria gravar no arquivo.
Por exemplo, considere este código
#include <fstream>
#include <exception>
#include <memory>
void inner_mad()
{
throw std::exception();
}
void mad()
{
auto ptr = std::make_unique<int>();
inner_mad();
}
int main()
{
std::ofstream os("file.txt");
os << "Content!!!";
int possibility = /* either 1, 2, 3 or 4 */;
if(possibility == 1)
return 0;
else if(possibility == 2)
throw std::exception();
else if(possibility == 3)
mad();
else if(possibility == 4)
exit(0);
}
O que acontece em cada possibilidade é:
- Possibilidade 1: Return essencialmente deixa o escopo da função atual, para que ele saiba sobre o final do ciclo de vida
os
, chamando seu destruidor e fazendo a limpeza adequada, fechando e liberando o arquivo para o disco.
- Possibilidade 2: lançar uma exceção também cuida do ciclo de vida dos objetos no escopo atual, fazendo assim uma limpeza adequada ...
- Possibilidade 3: Aqui o desenrolar da pilha entra em ação! Mesmo que a exceção seja lançada
inner_mad
, o desbobinador passará pela pilha mad
e main
realizará a limpeza adequada, todos os objetos serão destruídos corretamente, incluindo ptr
e os
.
- Possibilidade 4: Bem, aqui?
exit
é uma função C e não está ciente nem é compatível com os idiomas C ++. Ele não executa a limpeza em seus objetos, inclusive os
no mesmo escopo. Portanto, seu arquivo não será fechado corretamente e, por esse motivo, o conteúdo poderá nunca ser gravado nele!
- Outras possibilidades: deixará o escopo principal apenas, executando um implícito
return 0
e, portanto, tendo o mesmo efeito que a possibilidade 1, ou seja, limpeza adequada.
Mas não tenha tanta certeza do que acabei de lhe contar (principalmente as possibilidades 2 e 3); continue lendo e descobriremos como executar uma limpeza adequada baseada em exceções.
Possíveis maneiras de terminar
Retorno do main!
Você deve fazer isso sempre que possível; sempre prefere retornar do seu programa retornando um status de saída adequado da main.
O chamador do seu programa e, possivelmente, o sistema operacional, pode querer saber se o que seu programa deveria fazer foi feito com êxito ou não. Por esse mesmo motivo, você deve retornar zero ou EXIT_SUCCESS
sinalizar que o programa foi finalizado com êxito e EXIT_FAILURE
sinalizar que o programa foi finalizado sem êxito, qualquer outra forma de valor de retorno é definida pela implementação ( §18.5 / 8 ).
No entanto, você pode estar muito envolvido na pilha de chamadas e retornar tudo isso pode ser doloroso ...
[Não] lance uma exceção
Lançar uma exceção executará a limpeza adequada do objeto usando o desenrolamento da pilha, chamando o destruidor de cada objeto em qualquer escopo anterior.
Mas aqui está o problema ! É definido pela implementação se o desenrolamento da pilha é executado quando uma exceção lançada não é tratada (pela cláusula catch (...)) ou mesmo se você tem uma noexcept
função no meio da pilha de chamadas. Isto é afirmado em §15.5.1 [exceto.terminar] :
Em algumas situações, o tratamento de exceções deve ser abandonado para técnicas de tratamento de erros menos sutis. [Nota: Essas situações são:
[...]
- quando o mecanismo de tratamento de exceções não puder encontrar um manipulador para uma exceção lançada (15.3), ou quando a procura de um manipulador (15.3) encontrar o bloco mais externo de uma função com uma noexcept
especificação que não permita a exceção (15.4), ou [...]
[...]
Nesses casos, std :: terminate () é chamado (18.8.3). Na situação em que nenhum manipulador correspondente é encontrado, é definido como implementação se a pilha é ou não desenrolada antes que std :: terminate () seja chamado [...]
Então nós temos que pegá-lo!
Lance uma exceção e pegue-a na principal!
Como as exceções não capturadas podem não executar o desenrolamento da pilha (e consequentemente não executam a limpeza adequada) , devemos capturar a exceção no main e retornar um status de saída ( EXIT_SUCCESS
ou EXIT_FAILURE
).
Portanto, uma configuração possivelmente boa seria:
int main()
{
/* ... */
try
{
// Insert code that will return by throwing a exception.
}
catch(const std::exception&) // Consider using a custom exception type for intentional
{ // throws. A good idea might be a `return_exception`.
return EXIT_FAILURE;
}
/* ... */
}
[Não] std :: exit
Isso não realiza nenhum tipo de desenrolamento da pilha, e nenhum objeto ativo na pilha chama seu respectivo destruidor para executar a limpeza.
Isso é imposto em §3.6.1 / 4 [basic.start.init] :
Encerrar o programa sem sair do bloco atual (por exemplo, chamando a função std :: exit (int) (18.5)) não destrói nenhum objeto com duração automática de armazenamento (12.4) . Se std :: exit for chamado para encerrar um programa durante a destruição de um objeto com duração de armazenamento estático ou de encadeamento, o programa terá um comportamento indefinido.
Pense nisso agora, por que você faria uma coisa dessas? Quantos objetos você danificou dolorosamente?
Outras alternativas [tão ruins]
Existem outras maneiras de encerrar um programa (além de travar) , mas elas não são recomendadas. Apenas para fins de esclarecimento, eles serão apresentados aqui. Observe como o término normal do programa não significa o desenrolamento da pilha, mas um estado aceitável para o sistema operacional.
std::_Exit
causa um encerramento normal do programa, e é isso.
std::quick_exit
causa uma finalização normal do programa e chama os std::at_quick_exit
manipuladores, nenhuma outra limpeza é executada.
std::exit
causa um encerramento normal do programa e chama os std::atexit
manipuladores. Outros tipos de limpeza são executados, como chamar destruidores de objetos estáticos.
std::abort
causar um encerramento anormal do programa, nenhuma limpeza será realizada. Isso deve ser chamado se o programa for finalizado de uma maneira realmente inesperada. Ele não fará nada além de sinalizar o SO sobre o término anormal. Alguns sistemas executam um dump principal nesse caso.
std::terminate
chama o std::terminate_handler
que chama std::abort
por padrão.
main()
retorno de uso, em funções, use um valor de retorno adequado ou lance uma exceção adequada. Não useexit()
!