A duplicação de código é um mal necessário em C?

16

Eu sou bastante novo em C, e gostaria de saber se a duplicação de código é um mal necessário quando se trata de escrever estruturas de dados comuns e C em geral?

Eu poderia tentar escrever uma implementação genérica para, hash mappor exemplo, mas estou sempre achando o resultado final confuso. Eu também poderia escrever uma implementação especializada apenas para este caso de uso específico, manter o código claro e fácil de ler e depurar. Obviamente, o último levaria a alguma duplicação de código.

As implementações genéricas são uma norma ou você escreve implementações diferentes para cada caso de uso?

Gerstmann
fonte
11
O dilema moral que você está descrevendo não é exclusivo de C. Anedoticamente, acho que a barra para criar uma implementação genérica é bastante alta, a menos que você tenha uma audiência para isso. A quantidade de esforço necessária para criar uma boa implementação genérica excede em muito a solução pontual, IME.
Robert Harvey
3
@RobertHarvey, na minha experiência, código reutilizável leva de 3 a 4 vezes mais esforços para escrever
gnat
1
@kevincline: Bem, o Java tem genéricos. É verdade que é o tipo de genérico "não preste atenção no homem por trás da cortina".
Robert Harvey
1
E antes dos genéricos, você poderia ter passado em Objeto e passado em Flutuante, Duplo, Inteiro e Longo, respectivamente.
Daniel Kaplan
3
O Deja vu é uma verificação de cheiro de código e a preferência pelo DRY está na raiz de todas as melhores práticas que merecem atenção na IMO. Mas eu me atirei nas linguagens orientadas para OOP tentando muito evitar a duplicação por si só. Se você não está vinculando preocupações não relacionadas entre si desnecessariamente ou destruindo a legibilidade, para que duas funções semelhantes se tornem uma, eu seguiria o instinto na maioria dos idiomas.
Erik Reppen

Respostas:

27

C torna difícil escrever código genérico. Ao contrário do C ++, que fornece modelos e funções virtuais, o C possui apenas três mecanismos para escrever código genérico:

  1. void* ponteiros
  2. Macros de pré-processador
  3. Ponteiros de função

void* os ponteiros estão longe do ideal, pois você perde toda a segurança de tipo fornecida pelo compilador, o que pode resultar em um comportamento indefinido difícil de depurar, resultante de transmissões de tipo inválidas.

As macros do pré-processador têm desvantagens bem conhecidas - a expansão do pré-processador é basicamente apenas um mecanismo de localização / substituição que ocorre antes da fase de compilação, o que novamente pode resultar em erros difíceis de depurar. O exemplo arquetípico é algo como:, #define add(x) (x+x)onde xpode ser incrementado duas vezes se você chamar add(i++). Você pode escrever um código genérico no estilo de modelo usando macros C, mas o resultado é realmente hediondo e difícil de manter .

Os ponteiros de função fornecem uma boa maneira de escrever código genérico, mas infelizmente não fornecem generalidade de tipo - apenas fornecem a possibilidade de polimorfismo em tempo de execução (e é por isso que, por exemplo, a biblioteca padrão qsortainda requer uma função que requer void*ponteiros.)

Você também pode implementar hierarquias de classes em C usando estruturas, como é feito na biblioteca GLib, que fornece uma GObjectclasse base genérica . Mas isso tem problemas semelhantes aos do uso de void*ponteiros, pois você ainda precisa confiar na conversão manual potencialmente insegura para aumentar e diminuir a conversão.

Então, sim, C torna difícil escrever código genérico e seguro / fácil de manter, o que infelizmente pode resultar em duplicação de código. Projetos C grandes geralmente usam linguagens de script para gerar código repetitivo durante o processo de compilação.

Charles Salvia
fonte
1
Pode-se também gerar código C repetitivo usando linguagens de modelagem externa / ferramentas ou um shell script ou usando formiga ou ...
Qual é o URL russo ofuscado pelo Google que você colocou na sua resposta? Clicar nele faz com que um prompt "aberto" apareça no meu navegador. Isso é seguro?
Robert Harvey
2
@RobertHarvey Minhas verificações de antivírus e segurança acham que está bem. É um arquivo de cabeçalho no idioma C, extensão .h. Parece legítimo.
maple_shaft
@ maple_shaft: OK. Eu removi a Googlization ao redor do link.
Robert Harvey
É apenas um arquivo .h (cabeçalho C) de texto simples
Charles Salvia
15

Não posso falar pelos outros, mas, na minha própria experiência pessoal com C, a duplicação de código não tem sido um problema. Se isso se deve ao tamanho do projeto ou a um conjunto de amostras encantado, não sei dizer. No entanto, existem três regras práticas que eu sigo que considero aplicáveis. Em nenhuma ordem específica, eles são ...

  1. Escreva para o que é necessário. A generalidade pode vir mais tarde, se for necessário.
  2. Se for necessária generalidade, ponteiros nulos, ponteiros de função e tamanho da estrutura podem ser inestimáveis. A rotina qsort (), por exemplo, utiliza todos os três.
  3. Dê um peso para deixar seu código claro.
Sparky
fonte
5

Vale a pena notar que você pode não precisar de uma solução totalmente genérica para reduzir a duplicação de código. Às vezes, a refatoração comum e um pouco de generalização de método são suficientes.

Ao escrever uma solução generalizada para um público amplo, você deve considerar:

  1. De que maneiras novas o consumidor pode usar seu código e como você o acomoda?
  2. Quais erros eu preciso pegar? Quais erros eu não devo pegar?
  3. Quão robusta a API precisa ser? Quantas sobrecargas eu preciso fornecer?
  4. Que medidas de segurança eu tenho que implementar, para que o código não possa ser usado para propósitos malignos?

Eric Lippert escreveu um artigo inteiro sobre uma única decisão de design no código do framework .NET. No geral, é mais simples refatorar.

Robert Harvey
fonte
3

Eu costumo usar uma implementação genérica, como a da glib , e se o elenco acabar sendo muito irritante, crie um pequeno invólucro específico para cada tipo. No entanto, espera-se muita conversão em C, assim como o uso de a void*como um tipo genérico; portanto, o que seria considerado "confuso" em outro idioma é apenas o típico C. C. Com bastante dificuldade, parecerá muito mais natural quando você obtenha mais experiência no idioma.

Karl Bielefeldt
fonte
2
"O que seria considerado 'bagunçado' em outro idioma é apenas o típico C. C: assustadoramente, parecerá muito mais natural quando você tiver mais experiência no idioma." Ha, ha, ha! C sempre foi um dos meus idiomas favoritos, mas isso ainda é bem engraçado!
precisa saber é o seguinte
0

Seu problema de reutilização não é exclusivo do C. No entanto, algumas implementações excelentes e muito reutilizáveis ​​do HashMap estão disponíveis, das quais você pode basear as suas. Um deles existe há uma ou duas décadas em Java: http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

Todas as coleções Java, incluindo o HashMap, estão sendo reescritas para o Java 8 para se livrar da idéia de um iterador e, em vez disso, exigem que você passe uma função para a coleção para que possa iterar internamente. Essa é uma grande vitória para escrever código simultâneo - a coleção pode gerenciar a concorrência internamente, para que o cliente não precise. Dito isto, a maioria dos programadores não está acostumada a passar indicadores de função. Isso faz um bom trabalho ao explicar para onde o HashMap está indo: http://cr.openjdk.java.net/~briangoetz/lambda/collections-overview.html

A sintaxe do Scala pode dificultar a conversão de algo assim em C, mas aqui está um exemplo de uma coleção que já funciona como as coleções do Java 8 - imutável por padrão, fornecendo vários métodos de transformação, usando ponteiros de função e outros coisas legais como aplicação parcial: http://www.scala-lang.org/api/current/index.html#scala.collection.Map

Se você estiver fazendo muitas coisas com o HashMaps e outras estruturas de dados mais avançadas, convém procurar uma linguagem que as suporte.

Voltando à idéia de duplicação de código em geral, você pode codificar muito rápido cortando e colando, mas produz código terrível. Eu diria que é aceitável em código descartável, como relatórios únicos. Às vezes, também não há problema quando não há código a jusante, como o código da interface do usuário que apenas cria uma tela da qual nada mais depende, mas na verdade essa é uma área muito cinzenta.

Eu não acho muito simplista dizer que sempre que você se encontrar duplicando o código, deve fazer uma função. Somente os erros que você corrige em uma cópia do código e não na outra são motivos suficientes para criar uma função.

GlenPeterson
fonte
1
- "Eu não acho que seja uma simplificação demais dizer que sempre que você se encontrar duplicando o código, você deve fazer uma função" - Em C, isso é uma simplificação excessiva. Tente criar uma pilha em C para números inteiros, duplos e seqüências de caracteres sem duplicar as funções push e pop de cada tipo de dados.
precisa saber é o seguinte
@ mike Isso não é possível usando ponteiros nulos?
Sakisk
@ Mike - bom ponto. Mas um contraponto seria que você poderia implementar o Stack para cada tipo de dados de forma que não precise implementar o Stack novamente. Portanto, o objetivo não é numberOfImplementations = 1, mas mais como 5. Você pode fazer a intimplementação funcionar para chars ... Faz tanto tempo desde que usei C que não consigo me lembrar.
precisa saber é o seguinte
1
@faif. Sim, mas a necessidade de usar ponteiros nulos faz o ponto para C. A transmissão de ponteiros é mais lenta e menos elegante do que as alternativas encontradas no C ++. Duplicando as fileiras de código um pouco mais alta na lista de possíveis opções quando codificação em C.
mike30
@ mike30 Existem provas / referências sobre: ​​"A transmissão de ponteiros em C é mais lenta que as alternativas encontradas em C ++?"
sakisk
0

Gostaria de saber se a duplicação de código é um mal necessário quando se trata de escrever estruturas de dados comuns e C em geral?

Em C, absolutamente para mim, como alguém que salta entre C e C ++. Definitivamente duplico coisas mais triviais diariamente em C do que em C ++, mas deliberadamente, e não as vejo necessariamente como "más" porque há pelo menos alguns benefícios práticos - acho um erro considerar todas as coisas estritamente "bom" ou "mau" - quase tudo é questão de troca. Entender claramente essas trocas é a chave para não evitar decisões lamentáveis ​​em retrospectiva, e simplesmente rotular as coisas como "boas" ou "más" geralmente ignora todas essas sutilezas.

Embora o problema não seja exclusivo de C, como outros apontaram, ele pode ser consideravelmente mais exacerbado em C devido à falta de algo mais elegante do que macros ou ponteiros nulos para genéricos, constrangimento de OOP não trivial e o fato de que o A biblioteca padrão C não vem com nenhum contêiner. No C ++, uma pessoa que implementa sua própria lista vinculada pode atrair uma multidão de pessoas que se perguntam por que não estão usando a biblioteca padrão, a menos que sejam estudantes. Em C, você convidaria uma multidão enfurecida se não puder implementar com confiança uma implementação elegante de lista vinculada enquanto dorme, pois é esperado que um programador em C possa pelo menos ser capaz de fazer esses tipos de coisas diariamente. Isto' Não é devido a uma obsessão estranha nas listas vinculadas que Linus Torvalds usou a implementação da pesquisa e remoção de SLL usando o duplo indireção como critério para avaliar um programador que entende a linguagem e tem "bom gosto". É porque os programadores de C podem ser obrigados a implementar essa lógica milhares de vezes em sua carreira. Nesse caso, para C, é como um chef avaliando as habilidades de um novo cozinheiro, fazendo-o apenas preparar alguns ovos para ver se eles pelo menos têm domínio das coisas básicas que precisam fazer o tempo todo.

Por exemplo, eu provavelmente implementei essa estrutura básica de dados de "lista livre indexada" uma dúzia de vezes em C localmente para cada site que usa essa estratégia de alocação (quase todas as minhas estruturas vinculadas para evitar a alocação de um nó por vez e reduzir pela metade a memória custos dos links em 64 bits):

insira a descrição da imagem aqui

Mas em C, apenas leva uma quantidade muito pequena de código para reallocuma matriz cultivável e agrupa alguma memória dela usando uma abordagem indexada para uma lista livre ao implementar uma nova estrutura de dados que usa essa.

Agora eu tenho a mesma coisa implementada em C ++ e só a tenho implementada uma vez como modelo de classe. Mas é uma implementação muito, muito mais complexa no lado do C ++, com centenas de linhas de código e algumas dependências externas que também abrangem centenas de linhas de código. E a principal razão pela qual é muito mais complicado é porque eu tenho que codificá-lo contra a ideia de que Tpoderia ser qualquer tipo de dados possível. Ele poderia ser lançado a qualquer momento (exceto ao destruí-lo, o que devo fazer explicitamente como nos contêineres da biblioteca padrão), tive que pensar no alinhamento adequado para alocar memória paraT (embora, felizmente, isso seja facilitado no C ++ 11), ele pode ser não-trivialmente construtível / destrutível (exigindo a colocação de invocações novas e manuais de dtor), eu tenho que adicionar métodos que nem tudo será necessário, mas algumas coisas serão necessárias, e eu tenho que adicionar iteradores, iteradores mutáveis ​​e somente leitura (const), e assim por diante.

Matrizes cultiváveis ​​não são ciência de foguetes

Em C ++, as pessoas fazem parecer que std::vectoré o trabalho de um cientista de foguetes, otimizado até a morte, mas não tem um desempenho melhor do que um array C dinâmico codificado em um tipo de dados específico que apenas usa reallocpara aumentar a capacidade do array em push backs com um dúzia de linhas de código. A diferença é que é necessária uma implementação muito complexa para tornar apenas uma sequência de acesso aleatório cultivável totalmente compatível com o padrão, evitar a chamada de ctors em elementos não inseridos, com exceção de exceção, fornecer iteradores de acesso aleatório const e não-const, use type características para desambiguar os preenchedores dos intervalos de seleção para certos tipos integrais deT, potencialmente trate os PODs de maneira diferente usando traços de tipo, etc. etc. etc. Nesse ponto, você realmente precisa de uma implementação muito complexa apenas para criar uma matriz dinâmica expansível, mas apenas porque está tentando lidar com todos os casos de uso possíveis que se possa imaginar. No lado positivo, você pode obter muita milhagem de todo esse esforço extra se realmente precisar armazenar PODs e UDTs não triviais, usar para algoritmos genéricos baseados em iteradores que funcionam em qualquer estrutura de dados compatível, se beneficiam do tratamento de exceções e do RAII, pelo menos às vezes substituem std::allocatorseu próprio alocador personalizado, etc. etc. Definitivamente vale a pena na biblioteca padrão quando você considera quanto de benefíciostd::vector teve no mundo inteiro as pessoas que o usaram, mas isso é algo implementado na biblioteca padrão projetada para atender às necessidades do mundo inteiro.

Implementações mais simples que lidam com casos de uso muito específicos

Como resultado de apenas lidar com casos de uso muito específicos com minha "lista livre indexada", apesar de implementar essa lista grátis uma dúzia de vezes no lado C e ter algum código trivial duplicado como resultado, provavelmente escrevi menos código total em C para implementar uma dúzia de vezes do que eu precisei implementá-lo apenas uma vez em C ++, e tive que gastar menos tempo mantendo essas dúzias de implementações em C do que para manter essa implementação em C ++. Uma das principais razões pelas quais o lado C é tão simples é que normalmente trabalho com PODs em C sempre que uso essa técnica e geralmente não preciso de mais funções do queinsert eerasenos sites específicos em que implemento isso localmente. Basicamente, posso apenas implementar o subconjunto mais jovem da funcionalidade que a versão C ++ fornece, pois sou livre para fazer muitas outras suposições sobre o que faço e não preciso do design ao implementá-lo para um uso muito específico. caso.

Agora, a versão C ++ é muito mais agradável e segura para o tipo de uso, mas ainda era uma PITA importante implementar e tornar compatível com iterador e salvo com exceção e bidirecional, por exemplo, de maneiras que apresentar uma implementação geral e reutilizável provavelmente custa mais tempo do que realmente economiza neste caso. E muito desse custo de implementá-lo de uma maneira generalizada é desperdiçado não apenas antecipadamente, mas repetidamente na forma de coisas como tempos de construção escalados pagos repetidamente a cada dia.

Não é um ataque ao C ++!

Mas isso não é um ataque ao C ++ porque eu amo o C ++, mas quando se trata de estruturas de dados, eu vim a favor do C ++ principalmente pelas estruturas de dados realmente não triviais que eu quero gastar muito tempo inicial para implementar em de uma maneira muito generalizada, torne a exceção segura contra todos os tipos possíveis de T, faça com que seja compatível com os padrões e iterável, etc., onde esse tipo de custo inicial realmente compensa na forma de uma tonelada de quilometragem.

No entanto, isso também promove uma mentalidade de design muito diferente. Em C ++, se eu quiser criar um Octree para detecção de colisão, tenho a tendência de generalizá-lo até o enésimo grau. Eu não quero apenas fazê-lo armazenar malhas triangulares indexadas. Por que devo limitá-lo a apenas um tipo de dados com o qual posso trabalhar quando tenho um mecanismo super poderoso de geração de código na ponta dos meus dedos que elimina todas as penalidades de abstração em tempo de execução? Quero que armazene esferas processuais, cubos, voxels, superfícies de NURBs, nuvens de pontos, etc etc etc e tente torná-lo bom para tudo, porque é tentador querer projetá-lo dessa maneira quando você tem modelos na ponta dos dedos. Talvez eu nem queira limitá-lo à detecção de colisões - que tal traçar raios, escolher, etc.? C ++ faz inicialmente parecer "meio fácil" generalizar uma estrutura de dados até o enésimo grau. E foi assim que eu costumava projetar esses índices espaciais em C ++. Tentei projetá-los para lidar com as necessidades de fome do mundo inteiro, e o que recebi em troca foi tipicamente um "pau para toda obra" com código extremamente complexo para equilibrá-lo com todos os possíveis casos de uso imagináveis.

Curiosamente, porém, obtive mais reutilização dos índices espaciais que implementei em C ao longo dos anos, e sem culpa do C ++, mas apenas o meu no que a linguagem me tenta a fazer. Quando codifico algo como uma octree em C, tenho a tendência de fazê-la funcionar com pontos e ser feliz com isso, porque a linguagem torna muito difícil até tentar generalizá-la até o enésimo grau. Mas devido a essas tendências, ao longo dos anos, tendi a projetar coisas que são realmente mais eficientes, confiáveis ​​e realmente adequadas para determinadas tarefas em mãos, uma vez que elas não se preocupam em ser gerais até o enésimo grau. Eles se tornam ases em uma categoria especializada, em vez de um valete de todos os negócios. Novamente, isso não é culpa do C ++, mas simplesmente das tendências humanas que tenho quando o uso, em vez de C.

De qualquer forma, eu amo os dois idiomas, mas existem tendências diferentes. No IC, a tendência é não generalizar o suficiente. Em C ++, tenho uma tendência a generalizar demais. Usar os dois me ajudou a me equilibrar.

As implementações genéricas são uma norma ou você escreve implementações diferentes para cada caso de uso?

Para coisas triviais, como listas indexadas de 32 bits vinculadas individualmente, usando nós de uma matriz ou de uma matriz que se realoca (equivalente analógico std::vectorem C ++) ou, digamos, uma octree que apenas armazena pontos e visa não fazer mais nada, eu não ' não se preocupe em generalizar a ponto de armazenar qualquer tipo de dados. Eu os implemento para armazenar um tipo de dados específico (embora possa ser abstrato e usar ponteiros de função em alguns casos, mas pelo menos mais específico do que digitar duck com polimorfismo estático).

E estou perfeitamente feliz com um pouco de redundância nesses casos, desde que eu faça um teste completo. Se eu não fizer o teste de unidade, a redundância começará a ser muito mais desconfortável, porque você pode ter um código redundante que pode estar duplicando erros, por exemplo, mesmo se o tipo de código que você está escrevendo provavelmente não precisar de alterações de design, ainda pode precisar de alterações porque está quebrado. Costumo escrever testes de unidade mais completos para o código C que escrevo como motivo.

Para coisas não triviais, geralmente é quando eu procuro C ++, mas se eu fosse implementá-lo em C, consideraria usar apenas void*ponteiros, talvez aceite um tamanho de tipo para saber quanta memória alocar para cada elemento e possivelmente copy/destroyponteiros de função copiar em profundidade e destruir os dados, se não forem trivialmente construtíveis / destrutíveis. Na maioria das vezes, eu não me incomodo e não uso muito C para criar as estruturas e algoritmos de dados mais complexos.

Se você usar uma estrutura de dados com frequência suficiente com um tipo de dados específico, também poderá agrupar uma versão com segurança de tipo sobre uma que funcione apenas com bits e bytes e ponteiros de função e void*, por exemplo, para reimpor a segurança de tipo através do wrapper C.

Eu poderia tentar escrever uma implementação genérica para um mapa de hash, por exemplo, mas estou sempre achando o resultado final confuso. Eu também poderia escrever uma implementação especializada apenas para este caso de uso específico, manter o código claro e fácil de ler e depurar. Obviamente, o último levaria a alguma duplicação de código.

As tabelas de hash são meio duvidosas, pois pode ser trivial de implementar ou realmente complexo, dependendo de quão complexas são as suas necessidades em relação a hashes, repetições, se você precisar fazer com que a tabela cresça automaticamente por conta própria implicitamente ou possa antecipar o tamanho da tabela. antecipadamente, se você usa endereçamento aberto ou encadeamento separado, etc. Mas uma coisa a ter em mente é que, se você adaptar uma tabela de hash perfeitamente às necessidades de um site específico, ela geralmente não será tão complexa na implementação e frequentemente ganha ser tão redundante quando adaptado precisamente para essas necessidades. Pelo menos essa é a desculpa que me dou se implementar algo localmente. Caso contrário, você pode apenas usar o método descrito acima com void*e ponteiros de função para copiar / destruir as coisas e generalizá-las.

Geralmente, não é preciso muito esforço ou muito código para superar uma estrutura de dados muito generalizada se a sua alternativa for extremamente restrita ao seu caso de uso exato. Como exemplo, é absolutamente trivial superar o desempenho do uso mallocde todos e cada nó (em vez de reunir um monte de memória para muitos nós) de uma vez por todas com o código que você nunca precisa revisitar para um caso de uso muito, muito exato mesmo quando novas implementações mallocsurgirem. Pode levar uma vida inteira para vencê-lo e codificar não menos complexo que você precisa dedicar uma grande parte de sua vida a mantê-lo atualizado, se quiser corresponder à sua generalidade.

Como outro exemplo, muitas vezes achei extremamente fácil implementar soluções 10 vezes mais rápidas ou mais do que as soluções VFX oferecidas pela Pixar ou Dreamworks. Eu posso fazer isso no meu sono. Mas isso não ocorre porque minhas implementações são superiores - longe, longe disso. Eles são absolutamente inferiores para a maioria das pessoas. Eles são apenas superiores para meus casos de uso muito, muito específicos. Minhas versões são muito, muito menos aplicáveis ​​do que as da Pixar ou Dreamwork. É uma comparação ridiculamente injusta, já que as soluções deles são absolutamente brilhantes em comparação com as minhas soluções simples, mas esse é o ponto. A comparação não precisa ser justa. Se tudo o que você precisa são algumas coisas muito específicas, não é necessário fazer uma estrutura de dados lidar com uma lista interminável de coisas que você não precisa.

Bits e bytes homogêneos

Uma coisa a explorar em C, uma vez que há uma falta inerente de segurança de tipo, é a idéia de armazenar as coisas de maneira homogênea, com base nas características de bits e bytes. Como resultado, existe mais desfoque entre o alocador de memória e a estrutura de dados.

Mas armazenar um monte de coisas de tamanho variável, ou mesmo coisas que apenas poderiam ser de tamanho variável, como um polimórfico Doge Cat, é difícil de ser feito com eficiência. Você não pode supor que eles possam ter tamanho variável e armazená-los contiguamente em um contêiner de acesso aleatório simples, porque o passo para passar de um elemento para o próximo pode ser diferente. Como resultado, para armazenar uma lista que contém cães e gatos, talvez você precise usar 3 instâncias separadas de estrutura / alocador de dados (uma para cães, uma para gatos e outra para uma lista polimórfica de ponteiros base ou ponteiros inteligentes, ou pior , aloque cada cão e gato contra um alocador de uso geral e os espalhe por toda a memória), o que fica caro e gera sua parcela de erros de cache multiplicados.

Portanto, uma estratégia a ser utilizada em C, apesar de possuir uma riqueza e segurança de tipo reduzida, é generalizar no nível de bits e bytes. Você pode supor que Dogse Catsexigir o mesmo número de bits e bytes, tenha os mesmos campos, o mesmo ponteiro para uma tabela de ponteiros de funções. Mas, em troca, você pode codificar menos estruturas de dados, mas igualmente importante, armazenar todas essas coisas de forma eficiente e contígua. Você está tratando cães e gatos como uniões analógicas nesse caso (ou você pode realmente usar uma união).

E isso tem um custo enorme para digitar segurança. Se há uma coisa que sinto falta mais do que qualquer outra coisa em C, é a segurança do tipo. Está se aproximando do nível da montagem, onde as estruturas estão apenas indicando a quantidade de memória alocada e como cada campo de dados está alinhado. Mas esse é realmente o meu principal motivo para usar C. Se você está realmente tentando controlar layouts de memória, onde tudo é alocado e onde tudo é armazenado um em relação ao outro, geralmente ajuda pensar apenas nas coisas no nível de bits e bytes e quantos bits e bytes você precisa para resolver um problema específico. Lá, a idiotice do sistema do tipo C pode realmente se tornar benéfica, e não uma desvantagem. Normalmente, isso acaba resultando em muito menos tipos de dados,

Duplicação Ilusória / Aparente

Agora, tenho usado a "duplicação" em um sentido genérico para coisas que podem até não ser redundantes. Vi pessoas distinguindo termos como duplicação "incidental / aparente" de "duplicação real". A meu ver, é que em muitos casos não há uma distinção tão clara. Acho a distinção mais parecida com "singularidade potencial" vs. "duplicação potencial" e pode ser de qualquer maneira. Geralmente depende de como você deseja que seus projetos e implementações evoluam e de como serão perfeitamente adaptados para um caso de uso específico. Mas eu sempre descobri que o que pode parecer duplicação de código posteriormente acaba não sendo redundante após várias iterações de melhorias.

Adote uma implementação simples de matriz expansível usando realloc, o equivalente analógico de std::vector<int>. Inicialmente, pode ser redundante com, digamos, o uso std::vector<int>em C ++. Mas você pode descobrir, por meio da medição, que pode ser benéfico pré-alocar 64 bytes antecipadamente para permitir que dezesseis inteiros de 32 bits sejam inseridos sem exigir uma alocação de heap. Agora não é mais redundante, pelo menos não com std::vector<int>. E então você pode dizer: "Mas eu poderia generalizar isso para um novo SmallVector<int, 16>, e você poderia. Mas, digamos que você ache útil, porque são para matrizes muito pequenas e de vida curta para quadruplicar a capacidade da matriz nas alocações de heap, em vez de aumentando em 1,5 (aproximadamente a quantidade que muitosvectorimplementações) enquanto trabalha com a suposição de que a capacidade do array é sempre uma potência de dois. Agora, seu contêiner é realmente diferente e provavelmente não existe um contêiner como esse. E talvez você possa tentar generalizar esses comportamentos, adicionando cada vez mais parâmetros de modelo para personalizar a pré-alocação mais pesada, personalizar o comportamento da realocação, etc. etc., mas nesse momento você pode encontrar algo realmente difícil de usar em comparação com uma dúzia de linhas simples de C código.

E você pode até chegar a um ponto em que precisa de uma estrutura de dados que aloque memória alinhada e preenchida de 256 bits, armazenando exclusivamente PODs para instruções do AVX 256, pré-aloca 128 bytes para evitar alocações de heap para tamanhos de entrada pequenos comuns, dobra em capacidade quando cheia e permite substituições seguras de elementos finais que excedem o tamanho da matriz, mas não excedem a capacidade da matriz. Nesse ponto, se você ainda está tentando generalizar uma solução para evitar duplicar uma pequena quantidade de código C, que os deuses da programação tenham piedade de sua alma.

Portanto, também existem momentos em que o que inicialmente começa a parecer redundante começa a crescer, à medida que você adapta uma solução para ajustar cada vez melhor um determinado caso de uso a algo totalmente único e não redundante. Mas isso é apenas para coisas em que você pode adaptá-las perfeitamente a um caso de uso específico. Às vezes, precisamos apenas de algo "decente" que seja generalizado para o nosso propósito, e aí eu me beneficio mais com estruturas de dados muito generalizadas. Mas, para coisas excepcionais perfeitamente criadas para um caso de uso específico, a idéia de "uso geral" e "feita perfeitamente para o meu propósito" começa a se tornar incompatível demais.

PODs e Primitivos

Agora, em C, frequentemente encontro desculpas para armazenar PODs e principalmente primitivas em estruturas de dados sempre que possível. Isso pode parecer um antipadrão, mas na verdade achei inadvertidamente útil melhorar a capacidade de manutenção do código sobre os tipos de coisas que costumava fazer com mais frequência em C ++.

Um exemplo simples é internar cadeias curtas (como é o caso das cadeias usadas para chaves de pesquisa - elas tendem a ser muito curtas). Por que se preocupar em lidar com todas essas seqüências de comprimento variável cujos tamanhos variam em tempo de execução, implicando construção e destruição não triviais (já que podemos precisar acumular alocação e liberar)? Que tal armazenar essas coisas em uma estrutura de dados central, como uma tabela trie ou hash segura para threads, projetada apenas para a internação de cadeias de caracteres e, em seguida, referir-se a essas cadeias com uma planilha antiga int32_tou:

struct IternedString 
{
    int32_t index;
};

... em nossas tabelas de hash, árvores preto-vermelho, listas de pulos etc., se não precisarmos de classificação lexicográfica? Agora, todas as nossas outras estruturas de dados que codificamos para trabalhar com números inteiros de 32 bits agora podem armazenar essas chaves de cadeia de caracteres internas que são efetivamente apenas 32 bits ints. E eu encontrei pelo menos em meus casos de uso (pode ser o meu domínio, já que trabalho em áreas como raytracing, processamento de malha, processamento de imagem, sistemas de partículas, ligação a linguagens de script, implementações de kit de GUI multithread de baixo nível, etc. - coisas de baixo nível, mas não tão baixas quanto um sistema operacional), que o código coincidentemente se torna mais eficiente e mais simples, apenas armazenando índices para coisas como essa. Isso faz com que eu esteja sempre trabalhando, digamos 75% das vezes, com apenasint32_t efloat32 nas minhas estruturas de dados não triviais, ou apenas armazenando coisas do mesmo tamanho (quase sempre de 32 bits).

E, naturalmente, se isso for aplicável ao seu caso, você pode evitar várias implementações de estrutura de dados para diferentes tipos de dados, pois você estará trabalhando com tão poucos em primeiro lugar.

Testes e Confiabilidade

Uma última coisa que eu ofereço, e pode não ser para todos, é favorecer a gravação de testes para essas estruturas de dados. Faça-os realmente bons em alguma coisa. Verifique se eles são ultra confiáveis.

Alguma duplicação de código menor se torna muito mais perdoável nesses casos, pois a duplicação de código é apenas uma carga de manutenção se você precisar fazer alterações em cascata no código duplicado. Você elimina um dos principais motivos para a alteração de código redundante, garantindo que ele seja extremamente confiável e muito adequado ao que está tentando fazer.

Meu senso de estética mudou ao longo dos anos. Eu não fico mais irritado porque estou vendo uma biblioteca implementar produto de ponto ou alguma lógica trivial da SLL que já está implementada em outra. Só fico irritado quando as coisas são pouco testadas e não são confiáveis, e descobri uma mentalidade muito mais produtiva. Eu realmente lidei com bases de código que duplicavam bugs através de códigos duplicados e vi os piores casos de codificação de copiar e colar, fazendo com que o que deveria ter sido uma mudança trivial em um local central se transformasse em uma mudança em cascata propensa a erros para muitos. No entanto, muitas vezes, isso foi resultado de testes ruins, do código falhando em se tornar confiável e bom no que estava fazendo em primeiro lugar. Antes, quando eu trabalhava em bases de código herdadas de buggy, minha mente associou todas as formas de duplicação de código como tendo uma probabilidade muito alta de duplicar bugs e exigindo alterações em cascata. No entanto, uma biblioteca em miniatura que faz uma coisa extremamente bem e com confiabilidade encontrará muito poucas razões para mudar no futuro, mesmo que tenha algum código de aparência redundante aqui e ali. Minhas prioridades estavam fora naquela época, quando a duplicação me irritava mais do que a baixa qualidade e a falta de testes. Essas últimas coisas devem ser a principal prioridade.

Duplicação de código para minimalismo?

Este é um pensamento engraçado que surgiu na minha cabeça, mas considere um caso em que possamos encontrar uma biblioteca C e C ++ que faça aproximadamente a mesma coisa: ambos têm aproximadamente a mesma funcionalidade, a mesma quantidade de manipulação de erros, um não é significativamente mais eficiente que o outro, etc. E o mais importante, ambos são implementados com competência, bem testados e confiáveis. Infelizmente, tenho que falar hipoteticamente aqui, pois nunca encontrei nada próximo de uma comparação lado a lado perfeita. Mas as coisas mais próximas que eu já encontrei dessa comparação lado a lado geralmente tinham a biblioteca C sendo muito, muito menor que o equivalente em C ++ (às vezes 1/10 do seu tamanho de código).

E acredito que o motivo disso é que, novamente, resolver um problema de maneira geral que lida com a maior variedade de casos de uso, em vez de um caso de uso exato, pode exigir centenas a milhares de linhas de código, enquanto o último pode exigir apenas uma dúzia. Apesar da redundância e apesar de a biblioteca padrão C ser péssima quando se trata de fornecer estruturas de dados padrão, muitas vezes acaba produzindo menos código em mãos humanas para resolver os mesmos problemas, e acho que isso se deve principalmente às diferenças nas tendências humanas entre essas duas línguas. Um promove a solução de um caso de uso muito específico, o outro tende a promover soluções mais abstratas e genéricas contra o maior número de casos de uso, mas o resultado final deles não

Eu estava olhando o raytracer de alguém no github outro dia e ele foi implementado em C ++ e exigia muito código para um raytracer de brinquedo. E eu não gastei muito tempo olhando o código, mas havia um monte de estruturas de uso geral ali que estavam lidando com muito mais do que o que um raytracer precisaria. E reconheço esse estilo de codificação porque costumava usar C ++ da mesma maneira de uma maneira super ascendente, concentrando-me em criar uma biblioteca completa de estruturas de dados de uso geral primeiro que vão muito além do imediato problema em questão e, em seguida, resolver o problema real em segundo lugar. Porém, embora essas estruturas gerais possam eliminar alguma redundância aqui e ali e desfrutar de muita reutilização em novos contextos, em troca, eles inflam enormemente o projeto, trocando um pouco de redundância com um monte de código / funcionalidade desnecessários, e o último não é necessariamente mais fácil de manter do que o primeiro. Pelo contrário, muitas vezes acho mais difícil de manter, uma vez que é difícil manter um design de algo geral que precisa equilibrar as decisões de design com a mais ampla gama de necessidades possível.


fonte