Como lidar com casos de falha no construtor de classe C ++?

21

Eu tenho uma classe CPP cujo construtor faz algumas operações. Algumas dessas operações podem falhar. Eu sei que os construtores não retornam nada.

Minhas perguntas são,

  1. É permitido executar algumas operações além da inicialização de membros em um construtor?

  2. É possível dizer à função de chamada que algumas operações no construtor falharam?

  3. Posso new ClassName()retornar NULL se ocorrer algum erro no construtor?

MayurK
fonte
22
Você pode lançar uma exceção de dentro do construtor. É um padrão completamente válido.
Andy
1
Você provavelmente deve dar uma olhada em alguns dos padrões de criação do GoF . Eu recomendo o padrão de fábrica.
SpaceTrucker
2
Um exemplo comum de # 1 é a validação de dados. IE se você tiver uma classe Square, com um construtor que leva um parâmetro, o comprimento de um lado, você quer verificar se esse valor for maior que 0.
David diz Reintegrar Monica
1
Para a primeira pergunta, deixe-me avisá-lo que funções virtuais podem se comportar de maneira não intuitiva em construtores. Mesmo com desconstrutores. Cuidado chamando isso.
1
# 3 - Por que você gostaria de retornar um NULL? Um dos benefícios do OO NÃO é ter que verificar os valores de retorno. Basta pegar () as possíveis exceções apropriadas.
MrWonderful

Respostas:

42
  1. Sim, embora alguns padrões de codificação possam proibi-lo.

  2. Sim. A maneira recomendada é lançar uma exceção. Como alternativa, você pode armazenar as informações de erro dentro do objeto e fornecer métodos para acessar essas informações.

  3. Não.

Sebastian Redl
fonte
4
A menos que o objeto ainda esteja no estado válido, embora parte dos argumentos do construtor não atendam aos requisitos e, portanto, seja marcada como um erro, 2) não é realmente recomendado. É melhor quando um objeto existe em um estado válido ou não existe.
217 Andy
@DavidPacker concordou, veja aqui: stackoverflow.com/questions/77639/… Mas algumas diretrizes de codificação proíbem exceções, o que é problemático para os construtores.
Sebastian Redl
De alguma forma, eu já lhe dei um voto positivo por essa resposta, Sebastian. Interessante. : D
Andy
10
@oxi Não, não é. Seu novo substituído é chamado para alocar memória, mas a chamada para o construtor é feita pelo compilador após o retorno do operador, o que significa que você não consegue capturar o erro. Isso pressupõe que novo seja chamado; não é para objetos alocados à pilha, que deve ser a maioria deles.
Sebastian Redl
1
Para o número 1, RAII é um exemplo comum em que é necessário fazer mais no construtor.
Eric
20

Você pode criar um método estático que execute o cálculo e retorne um objeto em caso de sucesso ou não em caso de falha.

Dependendo de como essa construção do objeto é feita, talvez seja melhor criar outro objeto que permita a construção de objetos em um método não estático.

Chamar um construtor indiretamente é freqüentemente chamado de "fábrica".

Isso também permitiria retornar um objeto nulo, que pode ser uma solução melhor do que retornar nulo.

nulo
fonte
Obrigado @null! Infelizmente não pode aceitar duas respostas aqui :( Caso contrário, eu teria aceito esta resposta também !! Obrigado novamente!
MayurK
@ MayurK não se preocupe, a resposta aceita não é para marcar a resposta correta, mas a que funcionou para você.
null
3
@ nulo: Em C ++, você não pode simplesmente retornar NULL. Por exemplo, int foo() { return NULLvocê retornaria 0(zero) um objeto inteiro. Se std::string foo() { return NULL; }você chamar acidentalmente std::string::string((const char*)NULL)qual é o comportamento indefinido (NULL não aponta para uma sequência terminada em \ 0).
MSalters
3
std :: optional pode estar muito longe, mas você sempre pode usar o boost :: optional se quiser ir por esse caminho.
Sean Burton
1
@Ld: No C ++, os objetos não são restritos aos tipos de classe. E com a programação genérica, não é incomum terminar com fábricas int. Por exemplo, std::allocator<int>é uma fábrica perfeitamente sã.
MSalters
5

O @SebastianRedl já deu respostas simples e diretas, mas alguma explicação extra pode ser útil.

TL; DR = existe uma regra de estilo para manter os construtores simples, há motivos para isso, mas esses motivos se relacionam principalmente a um estilo histórico (ou simplesmente ruim) de codificação. O tratamento de exceções em construtores é bem definido, e os destruidores ainda serão chamados para variáveis ​​e membros locais totalmente construídos, o que significa que não deve haver nenhum problema no código C ++ idiomático. A regra de estilo persiste de qualquer maneira, mas normalmente isso não é um problema - nem toda inicialização precisa estar no construtor e, particularmente, não necessariamente nesse construtor.


É uma regra de estilo comum que os construtores devem fazer o mínimo absoluto possível para configurar um estado válido definido. Se sua inicialização é mais complexa, ela deve ser tratada fora do construtor. Se não houver um valor barato para inicializar que seu construtor possa configurar, você deve enfraquecer os invariantes impostos por sua classe para adicionar um. Por exemplo, se a alocação de armazenamento para o gerenciamento da sua classe for muito cara, adicione um estado nulo ainda não alocado, porque é claro que ter estados de casos especiais como nulo nunca causou problemas a ninguém. Ahem.

Embora comum, certamente nesta forma extrema está muito longe do absoluto. Em particular, como meu sarcasmo indica, estou no campo que diz que o enfraquecimento dos invariantes é quase sempre um preço muito alto. No entanto, existem razões por trás da regra de estilo e existem maneiras de ter construtores mínimos e invariantes fortes.

Os motivos estão relacionados à limpeza automática do destruidor, principalmente diante de exceções. Basicamente, deve haver um ponto bem definido quando o compilador se torna responsável por chamar destruidores. Enquanto você ainda está em uma chamada de construtor, o objeto não é necessariamente totalmente construído, portanto, não é válido chamar o destruidor para esse objeto. Portanto, a responsabilidade de destruir o objeto só é transferida para o compilador quando o construtor é concluído com êxito. Isso é conhecido como RAII (Alocação de Recursos É Inicialização), que não é realmente o melhor nome.

Se ocorrer um lançamento de exceção dentro do construtor, qualquer coisa parcialmente construída precisará ser explicitamente limpa, normalmente em a try .. catch.

No entanto, os componentes do objeto que já foram construídos com êxito já são de responsabilidade dos compiladores. Isso significa que, na prática, não é realmente um grande problema. por exemplo

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

O corpo deste construtor está vazio. Enquanto os construtores para base1, member2e member3são seguros exceção, não há nada para se preocupar. Por exemplo, se o construtor de member2arremessos, esse construtor é responsável por se limpar. A base base1já foi completamente construída, então seu destruidor será chamado automaticamente. member3nunca foi parcialmente construído, por isso não precisa de limpeza.

Mesmo quando há um corpo, as variáveis ​​locais que foram totalmente construídas antes da exceção ser lançada serão automaticamente destruídas, como qualquer outra função. Corpos de construtores que manipulam ponteiros brutos ou "possuem" algum tipo de estado implícito (armazenado em outro local) - normalmente significando que uma chamada de função de início / aquisição deve corresponder a uma chamada de final / liberação - podem causar problemas de segurança de exceção, mas o problema real lá está falhando ao gerenciar um recurso corretamente por meio de uma classe. Por exemplo, se você substituir ponteiros brutos por unique_ptrno construtor, o destruidor de unique_ptrserá chamado automaticamente, se necessário.

Ainda existem outras razões pelas quais as pessoas dão para optar por construtores que fazem o mínimo. Uma é simplesmente porque a regra de estilo existe, muitas pessoas assumem que as chamadas de construtores são baratas. Uma maneira de obter isso, mas ainda ter fortes invariantes, é ter uma classe de fábrica / construtor separada que, em vez disso, possui os invariantes enfraquecidos e que configura o valor inicial necessário usando (potencialmente muitas) chamadas normais de função de membro. Depois de ter o estado inicial necessário, passe esse objeto como argumento para o construtor da classe com os invariantes fortes. Isso pode "roubar as entranhas" do objeto de invariantes fracos - mover semântica - que é uma noexceptoperação barata (e geralmente ).

E é claro que você pode agrupar isso em uma make_whatever ()função, para que os chamadores dessa função nunca precisem ver a instância da classe enfraquecido-invariantes.

Steve314
fonte
O parágrafo em que você escreve "Enquanto você ainda está em uma chamada de construtor, o objeto não é necessariamente totalmente construído, portanto, não é válido chamar o destruidor para esse objeto. Portanto, a responsabilidade de destruir o objeto só é transferida para o compilador quando o construtor for concluído com êxito "poderia realmente usar uma atualização referente à delegação de construtores. O objeto é totalmente construído quando qualquer construtor mais derivado é concluído e o destruidor será chamado se ocorrer uma exceção dentro de um construtor delegador.
Ben Voigt
Assim, o construtor "faça o mínimo" pode ser privado, e a função "make_whatever ()" pode ser outro construtor que chama o privado.
Ben Voigt
Esta não é a definição de RAII que eu estou familiarizado. Meu entendimento do RAII é adquirir intencionalmente um recurso (e somente no) construtor de um objeto e liberá-lo em seu destruidor. Dessa maneira, o objeto pode ser usado na pilha para gerenciar automaticamente a aquisição e liberação dos recursos que ele encapsula. O exemplo clássico é um bloqueio que adquire um mutex quando construído e o libera na destruição.
Eric
1
@ Eric - Sim, é uma prática absolutamente padrão - uma prática padrão que é comumente chamada de RAII. Não sou apenas eu que estico a definição - é até Stroustrup, em algumas conversas. Sim, RAII é sobre como vincular ciclos de vida de recursos a ciclos de vida de objetos, sendo o modelo mental a propriedade.
Steve314
1
@ Eric - respostas anteriores excluídas porque foram mal explicadas. De qualquer forma, os próprios objetos são recursos que podem pertencer. Tudo deve ter um proprietário, em uma cadeia até a mainfunção ou variáveis ​​estáticas / globais. Um objeto alocado usando new, não é propriedade até que você atribui essa responsabilidade, mas ponteiros inteligentes possuir os objetos alocados-heap eles fazem referência, e os recipientes possuem suas estruturas de dados. Os proprietários podem optar por excluir mais cedo, o destruidor dos proprietários é o responsável final.
Steve314