Omitir “destruidores” em C está levando YAGNI longe demais?

9

Estou trabalhando em um aplicativo incorporado médio em C usando técnicas semelhantes a OO. Minhas "classes" são módulos .h / .c usando estruturas de dados e estruturas de ponteiros de função para emular encapsulamento, polimorfismo e injeção de dependência.

Agora, seria de esperar que uma myModule_create(void)função viesse com uma myModule_destroy(pointer)contraparte. Mas, quando o projeto está sendo incorporado, os recursos que são instanciados realisticamente nunca devem ser liberados.

Quero dizer, se eu tiver 4 portas seriais UART e criar 4 instâncias UART com os pinos e configurações necessários, não há absolutamente nenhuma razão para querer destruir o UART # 2 em algum momento durante o tempo de execução.

Então, seguindo o princípio YAGNI (você não vai precisar disso), devo omitir os destruidores? Isso me parece extremamente estranho, mas não consigo pensar em uma utilidade para eles; os recursos são liberados quando o dispositivo é desligado.

Asics
fonte
4
Se você não fornecer uma maneira de descartar o objeto, estará transmitindo uma mensagem clara de que eles terão vida útil "infinita" depois de criados. Se isso faz sentido para o seu aplicativo, eu digo: faça.
glampert
4
Se você está indo tão longe no acoplamento do tipo ao seu caso de uso específico, por que tem uma myModule_create(void)função? Você pode apenas codificar as instâncias específicas que você espera usar na interface que você expõe.
Doval
@Doval eu pensei sobre isso. Sou estagiário, usando partes e partes de código do meu supervisor, por isso estou tentando lidar com "fazer o certo", experimentando o estilo OO em C para obter experiência e consistência com os padrões da empresa.
Asics
2
@glampert nails; Eu acrescentaria que você deve limpar a vida útil esperada infinita na documentação da função create.
Blrfl

Respostas:

11

Se você não fornecer uma maneira de descartar o objeto, estará transmitindo uma mensagem clara de que eles terão vida útil "infinita" depois de criados. Se isso faz sentido para o seu aplicativo, eu digo: faça.

Glampert está certo; não há necessidade de destruidores aqui. Eles apenas criariam inchaço no código e uma armadilha para os usuários (o uso de um objeto depois que seu destruidor é chamado é um comportamento indefinido).

No entanto, você deve ter certeza de que realmente não há necessidade de descartar os objetos. Por exemplo, você precisa ter um objeto para um UART que não esteja em uso no momento?

Demi
fonte
3

A maneira mais fácil que encontrei para detectar vazamentos de memória é poder sair corretamente do seu aplicativo. Muitos compiladores / ambientes fornecem uma maneira de verificar a memória que ainda está alocada quando o aplicativo sai. Se não for fornecido, geralmente há uma maneira de adicionar algum código antes de sair, o que pode ser compreendido.

Portanto, eu certamente forneceria construtores, destruidores e lógica de desligamento mesmo em um sistema incorporado que "teoricamente" nunca deveria sair para facilitar a detecção de vazamento de memória sozinho. Atualmente, a detecção de vazamento de memória é ainda mais importante se o código do aplicativo nunca deve sair.

Dunk
fonte
Observe que isso não se aplica se a única vez que você fizer alocações for durante a inicialização, que é um padrão que eu consideraria seriamente nos dispositivos de restrição de memória.
CodesInChaos
@ Códigos: Não há razão para se limitar. Embora você possa criar o excelente design que pré-aloca memória na inicialização, quando as pessoas depois de você aparecerem que não estão familiarizadas com esse grande esquema ou não percebem a importância dele, elas estarão alocando memória no voar e lá vai o seu design. Apenas faça isso corretamente e aloque / desaloque e verifique se o que você implementou realmente funciona. Se você realmente possui um dispositivo com restrição de memória, o que normalmente é feito é substituir os novos blocos de alocação de operador / malloc e pré-reserva.
Dunk
3

No meu desenvolvimento, que faz uso extensivo de tipos de dados opacos para promover uma abordagem semelhante ao OO, também lutei com essa questão. No começo, decidi eliminar o destruidor da perspectiva do YAGNI, bem como da perspectiva do "código morto" do MISRA. (Eu tinha bastante espaço de recursos, isso não era uma consideração.)

No entanto ... a falta de um destruidor pode dificultar os testes, como nos testes automatizados de unidade / integração. Convencionalmente, cada teste deve suportar uma configuração / desmontagem para que os objetos possam ser criados, manipulados e destruídos. Eles são destruídos para garantir um ponto de partida limpo e sem manchas para o próximo teste. Para fazer isso, a classe precisa de um destruidor.

Portanto, na minha experiência, o "não é" no YAGNI acaba sendo um "são" e acabei criando destruidores para todas as classes, achando que precisaria ou não. Mesmo que eu tenha pulado os testes, pelo menos um destruidor projetado corretamente existe para o pobre slob que me segue terá um. (E, com um valor muito menor, torna o código mais reutilizável, pois pode ser usado em um ambiente onde seria destruído.)

Enquanto isso endereça YAGNI, ele não endereça código morto. Para isso, acho que uma macro de compilação condicional, como #define BUILD_FOR_TESTING, permite que o destruidor seja eliminado da compilação de produção final.

Fazendo desta maneira: você tem destruidor para teste / reutilização futura e satisfaz os objetivos de design do YAGNI e as regras "sem código morto".

Greg Willits
fonte
Tenha cuidado ao # definir o código de teste / prod. É razoavelmente seguro quando aplicado a uma função inteira, como você descreve, porque se a função é realmente necessária, a compilação falhará. No entanto, o uso de #ifdef inline dentro de uma função é muito mais arriscado, pois agora você está testando um caminho de código diferente do que está sendo executado no prod.
Kevin
0

Você poderia ter um destruidor não operacional, algo como

  void noop_destructor(void*) {};

depois defina o destruidor de Uarttalvez usar

  #define Uart_destructor noop_destructor

(adicione o elenco adequado, se necessário)

Não se esqueça de documentar. Talvez você queira mesmo

 #define Uart_destructor abort

Como alternativa, caso especial no código comum chamando destruidor o caso quando a função do ponteiro destruidor é NULLevitar chamá-lo.

Basile Starynkevitch
fonte