4 pontos por GN⁺ 2023-10-10 | 1 comentários | Compartilhar no WhatsApp
  • Em 2023, a forma de escrever código C mudou bastante, e o estilo foi reorganizado em torno de nomes de tipos curtos, da exclusão de strings terminadas em nulo, do retorno de structs e da compilação em uma única unidade de tradução
  • Aliases curtos como u8, i32, size e s8, junto com a remoção de const e struct, são escolhas para reduzir o ruído visual e a carga cognitiva em declarações repetitivas
  • Strings são tratadas não como terminadas em nulo, mas como um fat pointer s8 com data e len; em ambientes Win32 e UTF-16, c16 e s16 são usados em conjunto
  • No desenho de funções, há preferência por retornar structs em vez de usar parâmetros de saída, com um padrão em que o valor de retorno inicializado com zero só recebe ok no ponto de sucesso
  • Valoriza-se regras locais legíveis, de macros, assert e declarações Win32 até assembly inline, mas ao contribuir para outros projetos, segue-se o estilo daquele projeto

Revelando a intenção com nomes de tipos curtos

  • São usados aliases curtos para tipos básicos de inteiros, caracteres e tamanhos de ponteiros
    • Exemplos: u8, c16, b32, i32, u32, u64, f32, f64, uptr, byte, size, usize
  • Como esses nomes aparecem com frequência em todo o programa, a concisão traz benefícios diretos para leitura e revisão
  • O sufixo _t não é mais usado; hoje ele parece um elemento visualmente dispersivo
  • Para o prefixo de tipos signed, prefere-se i em vez de s
    • s fica reservado para nomes de tipos de string
  • Para o tipo de tamanho, usa-se size em vez de isize
    • Porque o signed size é visto como o padrão mais importante
    • usize tem um uso mais restrito, principalmente ao interagir com interfaces externas
  • b32 expressa a intenção de “booleano de 32 bits”
    • Usa um tamanho de palavra natural em vez de _Bool
    • Na prática, considera-se que ele muitas vezes estará em registradores ou no padding de structs
    • Quando memória realmente importa, booleanos são compactados em uma variável flags
  • c16 é o tipo para caracteres UTF-16 necessário no Win32
    • Se for baseado em char16_t, isso ajuda debuggers como o GDB a exibir os dados como caracteres
    • O nome de tipo oficial do Win32 é wchar_t, mas prefere-se explicitar UTF-16
  • u8 é usado para octetos e principalmente dados UTF-8, enquanto byte é separado para memória bruta e como tipo especial de aliasing
  • Preocupar-se com sistemas que não dão suporte a tipos de largura fixa é visto como pouco prático
    • Nomes longos de tipos como int_fast32_t também são vistos como desperdício desnecessário
  • Ao mostrar apenas trechos de código isolados, esses aliases não são usados sozinhos
    • Porque, para o leitor entender o contexto, o typedef também seria necessário

Regras para macros e assert

  • Macros com aparência de função usam letras minúsculas
    • Exemplos: countof(a), lengthof(s), new(a, t, n)
  • Para constantes, ainda se prefere ALL_CAPS, mas para macros funcionais, minúsculas são consideradas mais legíveis
  • Macros funcionais têm menos problemas de namespace do que macros comuns
    • É possível ter ao mesmo tempo uma macro new() e variáveis ou campos new
    • Porque ela não é expandida como macro se não estiver na forma de chamada de função
  • Para GCC e Clang, a macro assert usa a forma while (!(c)) __builtin_unreachable()
  • Esse estilo de assert não exige separar configurações de build
    • Não é preciso ter definições separadas para builds de debug e release
    • A ativação é controlada pela presença do Undefined Behavior Sanitizer, ou UBSan
    • A libubsan fornece diagnósticos com nome de arquivo e número de linha
    • Em builds de release, isso se torna uma dica prática de otimização
  • Para ativar assertions em builds de release, coloca-se o UBSan em modo trap com -fsanitize-trap e habilita-se pelo menos -fsanitize=unreachable
  • Em teoria, -funreachable-traps também permitiria isso, mas, no momento da escrita, está quebrado em algumas versões recentes do GCC

O que é reduzido nas declarações

  • Não se usa const em parâmetros
    • Considera-se que ele não tem papel prático na otimização
    • O autor diz não lembrar de casos em que isso tenha pego, ou teria pegado, um erro
    • Um bom nome de parâmetro é considerado suficiente para cumprir o papel de documentação no protótipo
  • A remoção de const foi uma mudança que aumentou a produtividade ao reduzir a carga cognitiva e o ruído visual
  • Como pequena exceção, const ainda é preferido como dica para colocar tabelas estáticas em memória somente leitura próxima ao código
    • Se necessário, remove-se o const com cast
  • Para ponteiros nulos, usa-se o literal 0
    • É um estilo usado há cerca de 7 anos
    • Embora exista a possibilidade teórica de defeitos, nenhum caso real foi visto em centenas de milhares de linhas de código
  • restrict é usado apenas quando necessário
    • O código é estruturado de modo a não usar parâmetros de saída em loops, ou a evitar os próprios parâmetros de saída
  • inline não é usado
    • Porque tudo é compilado em uma única unidade de tradução
  • Todas as structs recebem typedef
    • Remover a palavra-chave struct torna o código mais fácil de ler
    • Structs recursivas recebem uma forward declaration logo acima, e os campos usam nomes curtos
  • Todas as funções, exceto o entry point, são declaradas como static
    • Porque se assume a compilação em uma única unidade de tradução
  • Graças aos nomes de tipos curtos e à remoção de const e struct, é confortável colocar o tipo de retorno da função e o nome da função na mesma linha
  • Houve uma época em que nomes de tipos eram escritos com inicial maiúscula, mas isso acabou sendo abandonado

Strings com s8, em vez de terminação nula

  • Uma das mudanças mais produtivas foi rejeitar completamente strings terminadas em nulo e passar a usar o tipo de string s8, com data e len
  • Estrutura de s8:
    • u8 *data
    • size len
  • A macro s8(s) envolve um literal de string C como uma string s8
  • s8 é passado e retornado por valor, como um fat pointer
  • s8 também funciona bem como prefixo de funções
    • Porque nomes da família str são reservados
    • Exemplos: s8span, s8equals, s8compare, s8hash, s8trim, s8clone
  • Para comparar literais, usa-se uma forma como s8equals(tagname, s8("body"))
  • Também foi tentado o uso de flexible array member para agrupar tamanho e array em uma única allocation, mas a falta de flexibilidade pareceu pesar mais do que os benefícios
  • Houve casos em que se pensou que um programa simples não precisava de um tipo de string, mas, em geral, essa avaliação se mostrou errada
  • s16 também é usado como tipo compatível com UTF-16
    • Ele possui c16 *data e size len
    • Ainda não há total convicção sobre a abordagem de prefixar literais com u na macro

Retorno de structs e forma de inicialização

  • Prefere-se retornar structs em vez de usar parâmetros de saída
    • Na prática, é uma forma de retornar vários valores, embora não haja destructuring
  • O exemplo i32parse(s8) retorna tanto o value, o resultado do parsing, quanto ok, o estado
  • O custo adicional de cópia não é visto como um grande problema na prática
    • Porque a convenção de chamada pode transformar isso em um parâmetro de saída restrict oculto
    • Ou, quando há inline, a sobrecarga do valor de retorno deixa de ser significativa
  • Essa abordagem reduz a tentação de indicar erros por meio de sinais in-band, como um retorno null especial
  • Prefere-se o padrão de criar, no topo da função, um valor de retorno inicializado com zero e usá-lo em todos os return
    • Em caso de erro, retorna-se imediatamente o estado inicializado com zero
    • No caminho de sucesso, define-se ok como true imediatamente antes do retorno
  • Exceto por dados estáticos e pelas macros s8 e s16, o uso de initializers também é reduzido
    • Evita-se designated initializer e inicializa-se por atribuições
  • Inicialização por atribuição é legível e, como há um sequence point entre cada atribuição, fornece uma ordem explícita
  • Em inicializações nas quais a ordem de chamada pode afetar o resultado, como em funções de geração de números aleatórios, não é preciso pensar nos casos possíveis de valores

Declarações Win32 e assembly inline

  • Prefere-se __attribute em vez de __attribute__
    • O sufixo __ final é considerado excessivo e desnecessário
  • Em programação de sistema Win32, não se inclui windows.h; os protótipos necessários são escritos manualmente
    • Em geral, porque não há muitas declarações e definições necessárias
    • Isso reduz o tempo de build e polui menos o namespace
    • Também se encaixa de forma mais limpa com tipos customizados como u32, b32 e uptr, em vez de DWORD, BOOL e ULONG_PTR
  • O exemplo de declarações Win32 usa a macro W32(r) __declspec(dllimport) r __stdcall
    • Funções como ExitProcess, GetStdHandle, VirtualAlloc, WriteConsoleA e WriteConsoleW são declaradas diretamente
  • Em assembly inline, os parênteses externos são tratados como se fossem chaves
    • Assim como em if, há um espaço antes do parêntese de abertura
    • Cada linha de constraint começa com dois-pontos
  • Como exemplo em que é possível conferir esse estilo em um programa pequeno, há wordhist.c
  • Como exemplo um pouco maior, há asmint.c, uma implementação de uma minilinguagem de programação

1 comentários

 
GN⁺ 2023-10-10
Opiniões do Hacker News
  • Parece que ele considerou que #define sizeof(x) (size)sizeof(x) não precisava de parênteses externos, mas há uma exceção bem pequena
    Como conversões têm precedência maior que multiplicação, sizeof(x) * 3 funciona com segurança como (size)sizeof(x) * 3
    Porém, em (size)sizeof(x)[y], a indexação de array é aplicada antes da conversão, então vira (size)(sizeof(x)[y]), não ((size)sizeof(x))[y]
    Em código real não há motivo para indexar sizeof(x), mas C permite integer[pointer] com o mesmo significado de pointer[integer], então esse macro pode compilar e se comportar incorretamente por falta de parênteses
    Mais fundamentalmente, também é difícil concordar com a afirmação de que signed size é melhor. O autor diz que tamanhos sem sinal são uma fonte de defeitos, mas o código apresentado também tem um bug que corrompe a memória se count for negativo
    Com inteiros sem sinal, contagens negativas não são representáveis e, se houver overflow, elas se tornam um valor positivo muito grande, sendo pegas pela verificação existente. Pessoalmente, prefiro usar inteiros sem sinal, mas sempre que possível com um wrapper de verificação de intervalo que interrompa a execução em caso de overflow

    • A semântica de _Bool, na verdade, me agrada
      Porque uma expressão que funciona bem em if (flags & FLAG_ALLOCATED) pode ser extraída para uma variável booleana, como em _Bool need_free = flags & FLAG_ALLOCATED;
      flags & FLAG_ALLOCATED pode ser um valor arbitrário diferente de zero quando está definido, não necessariamente 1, e _Bool o normaliza para 1. Se você armazenar em int, if (need_free) passa, mas if (need_free == true) pode falhar
      Também há desvantagens. Durante uma refatoração, se você deixar passar que a conversão implícita para _Bool estava fazendo algo útil, pode acabar com código incorreto como if ((flags & FLAG_ALLOCATED) == true)
      Além disso, ao ler uma struct do disco ou preenchê-la com bytes arbitrários, se um campo _Bool não for 0 nem 1, há risco de comportamento indefinido
    • Na verdade, (size)(sizeof(x)[y]) também deve surpreender muita gente, mas é o mesmo que (size)(sizeof ((x)[y]))
      sizeof não é uma função, e sim um operador unário, enquanto indexação e chamada de função têm precedência maior que sizeof. Por isso, prefiro deixar um espaço depois de sizeof e usar parênteses no operando só quando necessário
      https://en.cppreference.com/w/c/language/operator_precedence
      Para escrever o macro corretamente, ficaria #define sizeof(x) ((size)(sizeof (x)))
    • Boa observação. A lição é que, se a definição de um macro não se expande para um único token, ela deve sempre ser envolvida por parênteses. As regras de precedência de C são realmente complicadas
  • Definir os próprios tipos parece um passo além do necessário
    Mesmo alguém já familiarizado com os tipos de C precisa aprender um sistema peculiar separado para entender um programa. Especificar o tamanho faz sentido, então dá para entender usar uint32_t em vez de uint
    Esses tipos deveriam estar definidos em um header apropriado, e talvez eu esteja errado por não usar C há muito tempo

    • Na prática, o int de C é 32 bits
      Não em targets de 16 bits, mas você realmente vai portar um programa de 5 MB para 16 bits? Esse tipo de preocupação geralmente não vale a pena
      O problema é long. Em algumas máquinas ele tem 32 bits; em outras, 64 bits, o que é confuso. Felizmente, long long é sempre 64 bits, então basta abandonar long
      char de 8 bits, short de 16 bits, int de 32 bits, long long de 64 bits, e pronto. Em C, já se perdeu tempo demais infinitamente com o tamanho de int
    • O autor de fato delimitou isso como um estilo pessoal de programação. Sinceramente, os tipos padrão são verbosos demais, e acho que teria sido bom se a lista concisa que essa pessoa apresentou tivesse sido adotada no passado
    • Falando meio em tom de piada, uma parte considerável da programação é lidar com o sistema de tipos dos outros
      Para quem usa C com frequência, as abreviações mostradas aqui são familiares e, para um esquema de tipos customizado, é bastante elegante. Lembra Rust
    • Esses tipos estão em stdint.h
      Sempre me surpreende ver vários projetos recriarem esse arquivo com tanto esforço
      Traduzir tipos padrão para nomes próprios é irritante para quem lê. Uma vez, em um projeto C++, perguntei por que usavam tantos typedef para coleções, referências e objetos compostos, e ouvi que isso tornava tudo mais fácil de entender
      Mais tarde vi que a pessoa tinha uma cola de typedefs colada ao lado do monitor
    • Definir tipos inteiros próprios faz sentido em plataformas com restrição de recursos
      É comum ver tipos como dim_t, que podem ser de 32 ou 64 bits dependendo do uso. Mesmo em plataformas de 64 bits, estruturas com compressão de ponteiros costumam usar inteiros de 32 bits
      Por exemplo, se você alocar seu próprio heap e armazenar apenas offsets de 32 bits, em cargas de trabalho abaixo de 4 GB o uso de memória cai pela metade, e a localidade de cache também melhora, aumentando o desempenho
  • Abandonar convenções consolidadas de C por preferência pessoal parece um pouco exagerado
    Usar u8 ou i32 em vez de uint8_t ou int32_t reduz algumas letras, mas pode confundir outras pessoas ao lerem o código
    Usar um tipo de string customizado em vez de strings terminadas em nulo também parece aumentar a dificuldade de colaboração, considerando que C foi criado em torno desse tipo de string
    Escrever diretamente os protótipos da API Win32 sem incluir windows.h pode reduzir o tempo de compilação, mas é como trocar uma rodovia bem conservada por uma trilha na mata. Boa parte disso parece mais preferência pessoal do que código C fácil de todos lidarem

    • u8 ou i32 não servem para economizar digitação, mas para reduzir a carga sensorial na leitura
      O argumento da “quantidade de teclas pressionadas”, que sempre aparece no debate verbosidade versus concisão, tem um grande defeito. É falsa a crença de que a concisão só ajuda a digitar mais rápido, e que a verbosidade é sempre melhor para ler
      A verbosidade também tem vantagens na leitura, mas a concisão também tem as suas, e nenhum dos lados é claramente vencedor. São apenas compromissos diferentes
    • Nomes como u16 são bastante usados, e é pouco provável que confundam programadores
      O ponto em que a coisa realmente desmorona é quando dois programas diferentes definem cada um o seu u16 e o expõem em arquivos de cabeçalho, e então um terceiro programa inclui os dois cabeçalhos ao mesmo tempo
      Tipos de biblioteca com namespace acabam ficando na forma libname_u32, e, a essa altura, dá vontade de simplesmente usar uint32_t em vez do prefixo libname_
    • A possibilidade de um programador C competente ficar confuso ao ver u8 ou i32 é, no máximo, teórica, e soa um tanto como um espantalho argumentativo
      Ele pode até se irritar, mas não ficará confuso. Como disse Rich Hickey, tudo é difícil de ler antes de você aprender a ler
  • Dizem que usar booleanos de 32 bits pode parecer desperdício de memória para iniciantes; se for assim, acho que eu também sou iniciante
    Já ouvi alguns casos em que não é pior do que um bool de 8 bits, mas não vejo casos em que seja de fato melhor. Se houver booleanos adjacentes em uma struct, ou se uma variável booleana de uma função for expulsa dos registradores e parar na pilha, ainda assim há desperdício de memória
    Mesmo que sejam só alguns bytes, não entendo por que pessimizá-lo de propósito. O que se ganha usando um tamanho maior?

    • Depende inteiramente da arquitetura e da CPU, mas, pela experiência passada, um caso claro foi em trabalhos de processamento numérico
      Havia uma estrutura com um valor condicional no início de cada estrutura por ciclo, seguido por 512, 1024 ou 2048 valores de amostra; um júnior, tentando economizar espaço, empacotou a estrutura e transformou o valor condicional em 1 byte de 8 bits
      Esse código “melhorado” reduziu a vazão em cerca de 10 vezes em chips Intel e gerou BUS ERROR na arquitetura RISC SPARC
      Ao empacotar o cabeçalho da estrutura, o array de dados ficou desalinhado; a Intel teve de buscar silenciosamente duas palavras de 32 bits e combiná-las, enquanto a SPARC, corretamente, reclamou de dados desalinhados
      Se for um cálculo em pipeline em que a vazão importa, e não armazenamento de arquivo de longo prazo, às vezes é melhor alinhar os dados ao alinhamento da arquitetura do que empacotá-los para “economizar espaço”
    • Em geral, uma otimização fácil é preencher os campos da struct para alinhá-los a limites de 32 bits
      Quase todos os compiladores fazem isso, então basta procurar por “alinhamento/padding de structs”. Se o compilador vai deixar espaço vazio de qualquer forma, é melhor usar essa memória explicitamente; caso contrário, você pode perder desempenho
      Mais precisamente, cada campo deve ficar em um endereço divisível pelo seu próprio tamanho ou pelo tamanho da linha de palavra, e a estrutura inteira também deve receber padding até um múltiplo do tamanho do maior campo. Na prática, isso normalmente significa alinhamento de 32 bits
      Referência: http://www.catb.org/esr/structure-packing/
    • Se você usa o tipo bool real, o sanitizer avisa quando o valor não é 0, ou seja, false, nem 1, ou seja, true
    • A maioria das arquiteturas de computador é otimizada para acessos alinhados de 32 bits ou mais. O que se ganha normalmente, embora nem sempre, é desempenho
    • Fico curioso para ver um exemplo de função em que uma variável booleana é empurrada para a pilha e esses 3 bytes passam a importar
  • Não concordo com o argumento sobre retorno de structs e parâmetros de saída
    Isso torna muito mais difícil compor funções que podem retornar erros e faz os tipos se multiplicarem por toda parte. Na prática, quase qualquer função pode falhar, então, especialmente se você também trata falta de memória, um estilo previsível de retorno de erros é mais importante

    • Quase ninguém trata falta de memória
      É muito difícil e traz pouquíssimo benefício. Nesse ponto, surgem problemas muito diferentes e muito maiores do que escolhas de estilo de programação
    • Se fosse possível fazer um desempacotamento significativo de structs, daria para obter a semântica usual de errno e parâmetros de saída, mas C não tem isso
      Ainda assim, compor valores opcionais em C sempre foi um pouco doloroso. Se você não usa exceções, exceções e mônadas parecem ser as duas grandes opções em várias linguagens, mas nenhuma das duas se encaixa em C nem na filosofia da maioria dos programadores C
      Para chamadas simples um-para-um, dá para tentar macros, mas há limites. Por mais terrível que C++ seja, C++ optional é mais agradável de usar do que if(foo(x,y, out1, out2) != WHATEVER_LIBRARY_OK) { ... }
    • Retornar option ou tipos soma é o certo, mas em C isso é realmente trabalhoso de escrever
      O padrão de colocar if (thing(...)) goto fail em toda chamada de função também não parece lá muito excelente, embora o pessoal de Go pareça gostar
      Ou então há thread_local mylibrary_errno, que internamente na biblioteca pode ser de fato a abordagem correta, e nas fronteiras pode ser convertido para um valor de retorno enum
  • Em “signed sizes are the way”, dá vontade de dizer que bastava ler até aí
    signed size é um vazamento de abstração muito surpreendente e um jeito de chamar desastre
    Também é difícil aceitar a afirmação de que const não tem papel prático e nunca pegou erros. As pessoas confundem buffers de entrada e de saída com frequência, e const expõe isso imediatamente
    A orientação de deixar todas as funções, exceto os pontos de entrada, como static também pode fazer você amaldiçoar o autor quando, ao depurar, não consegue encontrar uma variável ou função
    Preferir retorno de structs facilita devolver por engano um ponteiro para a stack e abrir uma grande brecha de segurança. Ao passar um buffer de saída, a semântica de propriedade fica clara
    Esse conselho pode ser mais ou menos adequado para quem escreve principalmente código de sistema em 64 bits, mas no mundo embarcado de 32 bits pode rapidamente virar um problema

    • Bjarne Stroustrup escreveu uma nota detalhada defendendo signed size
      https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
    • Quem não gosta de const nunca vai ficar satisfeito; então use const onde for necessário, propague tanto quanto precisar e ignore as reclamações
      Se eles removerem, coloque de volta. Eles sempre se cansam primeiro. Faço isso há 25 anos e ainda estou aqui
      static pode depender das ferramentas. Uns 15 anos atrás passei a usar static por padrão e size_t em tudo, e ainda não tive problemas
  • É interessante que minha experiência tenha me levado em outra direção
    https://dlang.org/blog/2023/10/02/crafting-self-evident-code...
    O texto é centrado em D, mas os princípios também se aplicam a C

    • Foi uma leitura divertida
      Achei interessante a parte de mover a expressão condicional para dentro de doX() e doZ(). Não sei se isso é sempre correto; depende de onde a abstração fica e do modelo mental que se tem do código
      Por exemplo, deleteRecords(); não é melhor que if let x = deadRecords() deleteRecords(x);. O segundo parece mais bagunçado, mas tem o valor de mostrar logo de cara que se trata de poda, não exclusão
      Se a função for renomeada de forma inteligente para algo como pruneDeadProjects(), tudo bem, mas simplesmente mover a condição para dentro da função pode tornar o contexto perigoso e virar uma abstração vazada
  • Sou a favor de usar typedef em todas as structs, pois ajuda na concisão
    Acho que dá para usar typedef generosamente. Mas é melhor dar typedef apenas no alvo em si, não em ponteiros. Se precisar de um ponteiro, basta usar (type *) quando quiser
    Em especial, no caso de ponteiros de função, o correto é dar typedef na própria função, não no ponteiro de função. Assim, também é possível usar esse typedef nas declarações de funções para ter checagem de tipo dos parâmetros, e não é preciso corrigir todas as declarações ao mudar a assinatura da função
    A maioria das bases de código C faz isso errado: dá typedef no ponteiro de função e ainda precisa escrever manualmente declarações de funções compatíveis com aquela definição de ponteiro
    Ainda não estou convencido sobre usar structs como tipo de retorno. Prefiro deixar um código numérico de erro como valor de retorno, e receber os demais valores de retorno por parâmetros de saída

    • Para structs opacas, prefiro usar typedef para imitar classes em que todos os campos são privados, e usar struct para estruturas de dados simples
      Classes devem ser acessadas apenas por funções, enquanto structs devem poder ser acessadas diretamente
      Isso, em geral, fica mais próximo das convenções dos padrões C/POSIX. É a diferença, por exemplo, entre pthread_t e struct stat
    • Concordo em não dar typedef no ponteiro em si
      Coisas como SDL_net fazem exatamente isso, e eu não gosto. Na prática é um ponteiro, mas foi tipado com typedef como se fosse um tipo por valor
      Entendo a intenção, mas é um jeito bem incômodo
  • Muita coisa neste texto faz sentido
    Comecei a escrever um SO bare-metal para Arm64; ainda está no início, mas estou fazendo coisas parecidas. Uso strings Pascal e também mudei os nomes dos tipos. Só que no estilo int8, não i8
    Decidi rapidamente que não pretendia portar software real, então não preciso seguir as funções nem as convenções da biblioteca C padrão. Isso me permite experimentar com mais liberdade
    C é uma linguagem antiga o bastante para que o peso da época em que cada byte era precioso ainda apareça até nos nomes das funções. É bom se livrar disso, e o conteúdo deste texto, junto com várias pequenas mudanças de nome, parece uma arrumação bem limpa

    • Dizer que você não precisa seguir as funções nem as convenções da biblioteca C padrão quer dizer que é só um projeto de hobby e que não vai virar algo grande e profissional como o GNU?
    • Nunca tinha pensado em economizar bytes até nos símbolos
  • typedef float f32; e typedef double f64; parecem uma base perigosa, pois assumem que float tem 32 bits e double tem 64 bits
    O OpenCV define float16_t, o CUDA implementa ponto flutuante de meia precisão, e microcontroladores podem ter implementações próprias
    O C++23 introduz tipos de ponto flutuante de largura fixa, mas não sei como impor isso em C. Parece melhor ter uma macro que verifique, em tempo de compilação, se não há perda de dados
    No geral, como outros disseram, talvez seja melhor deixar algumas coisas nos valores padrão em prol da legibilidade, mesmo que não fique tão conciso
    [0] https://docs.opencv.org/4.x/df/dc9/classcv_1_1float16__t.htm...
    [1] https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH...
    [2] https://en.cppreference.com/w/cpp/types/floating-point