1 pontos por GN⁺ 18 시간 전 | 2 comentários | Compartilhar no WhatsApp
  • A biblioteca padrão de C++ tem repetidamente depreciado formalmente designs ruins ou os deixado de lado ao lado de novos substitutos desde o C++11, criando uma estrutura em que o desenvolvedor precisa saber em que fase está cada “camada que não deve ser usada”
  • A camada de retirada oficial inclui itens com papers de depreciação e remoção, como std::auto_ptr, especificações dinâmicas de exceção, a interface de coleta de lixo do C++11 e std::aligned_storage; std::function também entra num fluxo de substituição de 15 anos cercado por std::move_only_function, std::copyable_function e std::function_ref
  • A camada de evasão informal inclui recursos que continuam no padrão, mas são evitados em código de produção, como o lento std::regex, std::async, cujo destrutor espera e cria armadilhas de deadlock, além de <iostream>, std::list, std::deque e std::vector<bool>
  • O problema dos contêineres padrão aparece com força em std::unordered_map, std::map e std::list; em um benchmark com a mesma carga de trabalho, o P99 da implementação ingênua em C++ foi de 302.653 cycles, contra 5.177 cycles da implementação ingênua em Rust, uma diferença de 58x
  • A escolha pela estabilidade de ABI é a diferença central: enquanto outras linguagens reduzem erros com remoções, edições ou transições de versão principal, o C++ preserva na prática defaults ruins dentro de std:: de forma quase permanente

Ponto de partida: a classificação de std::function como “legado”

  • A tabela de referência rápida de Sandor Dargo para std::copyable_function classifica std::function como “Legacy. Avoid in new code.”
  • std::function entrou no C++11, e o wrapper substituto mais recente, std::copyable_function, entra no C++26; o argumento de venda do novo recurso está mais próximo de “não use o original” do que de “use isto quando precisar de um callable copiável”
  • O const operator() de std::function tem um defeito de const-correctness que permite chamar callables non-const, algo que não pode ser corrigido sem quebrar a ABI
  • Em resposta a esse defeito, std::move_only_function entrou no fluxo do P0288R9 no C++23, std::copyable_function no P2548R6 no C++26, e std::function_ref no P0792R14 no C++26

Recursos do padrão oficialmente revertidos

  • std::auto_ptr teve sua semântica de cópia-movimento quebrando código genérico e contêineres padrão, foi marcado para depreciação no C++11 e removido no C++17 via N4190; o mesmo paper também removeu os adaptadores de <functional> do C++98 e std::random_shuffle
  • std::random_shuffle foi substituído por std::shuffle porque dependia de std::rand e de estado global
  • As especificações dinâmicas de exceção throw(X, Y) foram marcadas para depreciação no C++11 e removidas no C++17 por P0003R5; o alias throw() foi removido no C++20 por P1152
  • std::iterator foi marcado para depreciação no C++17 por P0174R2, e sua remoção no C++26 é promovida por P3365R1; a alternativa é definir diretamente os cinco typedefs
  • std::aligned_storage e std::aligned_union entraram no C++11 e foram marcados para depreciação no C++23 por P1413R3; os problemas citados incluem boilerplate com typename ::type, reinterpret_cast, comportamento indefinido com Len == 0 e ausência de constexpr
  • std::not1, std::not2, unary_negate e binary_negate foram marcados para depreciação no C++17, removidos no C++20 e substituídos por std::not_fn de P0005
  • A família std::declare_reachable, interface de coleta de lixo do C++11, foi removida no C++23 por P2186R2, já que implementações principais nunca ofereceram um coletor de lixo real
  • Concepts TS, Modules TS, Coroutines TS, Reflection TS, Executors TS e Networking TS também passaram por redesenho, substituição ou atraso antes da fusão; Reflection mudou para P2996, e Executors migrou para o fluxo sender/receiver de P2300

Recursos que continuam no padrão, mas são evitados na prática

  • std::regex entrou no C++11, mas o P1844R1 registrou em comitê que seu desempenho é “muito ruim em comparação com outras soluções disponíveis”; o fluxo substituto aponta para CTRE e P1433R0, e fora do padrão para Boost.Regex, RE2 e PCRE2
  • std::async bloqueia no destrutor do future retornado até a conclusão da tarefa assíncrona, e N3679 registra a armadilha de deadlock causada por isso
  • <iostream> é lento, preso a locale, não é thread-safe em formatação e tem mensagens de erro notoriamente ruins, mas ainda assim não foi marcado para depreciação mesmo após a chegada de std::format no C++20 via P0645 e de std::print/std::println no C++23 via P2093
  • std::list foi mostrado por Bjarne Stroustrup no keynote da GoingNative 2012 como perdendo até em cargas de trabalho de inserção no meio para std::vector, e o texto posterior Are lists evil? responde algo bem próximo de “sim”
  • std::deque aparece na issue pública microsoft/STL#147, onde se registra que o tamanho de bloco imposto pelo padrão é pequeno demais e que será necessária uma grande reforma de desempenho no próximo ABI break
  • std::valarray entrou em 1998 como contêiner numérico, mas a otimização por expression templates não se concretizou e, segundo o cppreference, as implementações não parecem ter código especial além do que existe para contêineres comuns
  • std::vector<bool> tem como análise de referência On vector<bool>, de Howard Hinnant; o armazenamento compactado em bits é útil, mas o fato de ser nomeado como uma especialização de std::vector o torna uma armadilha que causa comportamento incorreto em código genérico quando T = bool
  • volatile foi marcado para depreciação no C++20 por P1152R4 em operações compostas e em posições de parâmetro e retorno, depois parcialmente revertido no C++23 por P2327R1, com nova reversão adicional prevista no C++26 por P2866R0

Contêineres básicos que a ABI impede de consertar

  • std::unordered_map é praticamente impedido de usar open addressing pelas exigências de buckets e estabilidade de iteradores da especificação do C++11; a estrutura SwissTable do Google é apresentada como tendo cerca de 3x de vantagem de desempenho sobre std::unordered_map
  • Folly F14, Boost unordered_flat_map e ankerl::unordered_dense são substitutos na mesma linha; o HashMap de Rust usa como default da biblioteca padrão um port da SwissTable do hashbrown
  • std::map e std::set são red-black trees baseadas em nós, exigindo alocação no heap por nó e pointer chasing a cada iteração; Abseil btree_map e Rust BTreeMap evitam esse problema com estruturas baseadas em B-tree
  • O C++23 adicionou std::flat_map e std::flat_set com P0429R9, mas não consegue mudar o design básico de std::unordered_map, std::map e std::list
  • O benchmark de livro de ordens multi-símbolo compara, na mesma carga, com a mesma seed e no mesmo core isolado, std::unordered_map + std::map + std::list em C++ com HashMap + BTreeMap + VecDeque em Rust
Implementação P99 cycles
C++ naive (unordered_map + map + list) 302,653
C++ step 1 (flat_hash_map + map + deque) 9,951
C++ step 2 (flat_hash_map + btree_map + deque) 9,114
C++ step 3 (flat_hash_map + btree_map + vector) 4,268
Rust naive (HashMap + BTreeMap + VecDeque) 5,177
  • A troca de std::list por std::vector sozinha mostra cerca de 70x; a troca de std::unordered_map por flat_hash_map, entre 3x e 5x; e a troca de std::map por btree_map, 1,09x, um efeito dentro da faixa de ruído
  • O ponto da comparação não é que a linguagem Rust seja 58x mais rápida que C++, mas que a biblioteca padrão de Rust escolheu defaults corretos, enquanto a biblioteca padrão de C++ não consegue corrigir seus três defaults por causa da ABI

O problema do Vasa e o acúmulo de recursos

  • No documento WG21 P0977R0 de 2018, “Remember the Vasa!”, Bjarne Stroustrup usa o afundamento do navio de guerra sueco Vasa em 1628 como metáfora e diagnostica que o comitê tem “cerca de 150 cozinheiros” e não trata suficientemente o impacto de cada recurso no sistema como um todo
  • std::simd é tratado como exemplo representativo do mesmo padrão em std::simd Is a Solution to the Wrong Problem; o recurso começou na biblioteca Vc de Matthias Kretz, passou por P0214, Parallelism TS 2 e P1928, e entrou no C++26
  • Quando std::simd entrou no padrão, já existiam fora dele Google Highway, ISPC, EVE, xsimd e SIMDe, e argumenta-se que até o auto-vectorizer de GCC e Clang melhorou a ponto de loops escalares com -O3 -march=native superarem std::simd
  • std::simd compila 10x mais devagar que código escalar equivalente, fica atrás do auto-vectorizer que pretendia substituir e não consegue expressar vetores de largura escalável do ARM SVE nem runtime dispatch
  • As três implementações, libstdc++, libc++ e MSVC STL, são mantidas por equipes de engenharia de um dígito, e cada novo recurso padrão amplia a matriz de testes, os bugs de conformidade, as interações entre recursos e os itens do bug tracker herdados pelo próximo mantenedor
  • std::regex carrega problemas conhecidos há 15 anos, std::deque tem uma issue pedindo redesenho, e os modules do C++20 são descritos como ainda não funcionando de forma limpa nas três implementações mesmo seis anos após a padronização
  • O conhecimento prático para operar C++ moderno acaba concentrado em uma pequena minoria de especialistas dedicados, que aprenderam a cronologia das camadas erradas, os workarounds de terceiros, as diferenças entre as três implementações da biblioteca padrão e a distância entre teoria e prática

Diferença em relação a outras linguagens: não é errar, é a taxa de retenção

  • Python removeu mais de 20 módulos da biblioteca padrão com a PEP 594, removeu distutils no Python 3.12 com a PEP 632 e a PEP 387 explicita autoridade para encurtar o ciclo de depreciação de recursos perigosos ou quebrados
  • Java marcou a API de Applet para depreciação no Java 9, para remoção no Java 17, e completou o caminho de 8 anos até a remoção real no JEP 504; Nashorn foi removido no Java 15 pelo JEP 372
  • O Java SecurityManager foi colocado em estado de depreciação para remoção pelo JEP 411, foi permanentemente desativado pelo JEP 486, e o JEP 398 trata do caminho de remoção da API de Applet
  • Rust escolhe por crate as edições 2015, 2018, 2021 e 2024 em Cargo.toml; mem::uninitialized foi substituído por MaybeUninit, std::error::Error::description por source, e a macro try! pelo operador ?
  • C# aceitou transições de versão principal ao passar de .NET Framework para .NET Core, deixando para trás BinaryFormatter, AppDomains, Remoting, Code Access Security, WCF server e WebForms
  • JavaScript quase não remove recursos por restrições de compatibilidade com a web, mas cancelable promises foi retirado ainda no Stage 1, SIMD.js foi abandonado em favor de WebAssembly SIMD, e Go, por causa da promessa de compatibilidade do Go 1, optou por deixar io/ioutil apenas como deprecado
  • A diferença do C++ não está em ter cometido erros, mas na taxa de retenção com que quase nunca consegue remover itens como std::regex, std::unordered_map, std::vector<bool>, std::valarray e o defeito de const-correctness de std::function

A estabilidade de ABI cria preservação permanente

  • P1863R1 “ABI - Now or Never” foi o fluxo que perguntou se o C++23 deveria aceitar um ABI break ou escolher estabilidade de ABI permanente, e o comitê na prática escolheu estabilidade permanente
  • Essa escolha dificulta corrigir std::regex, migrar std::unordered_map para open addressing e mudar a estrutura de std::list, std::map e std::deque
  • A ABI da biblioteca padrão de C++ é imposta pelo linker dinâmico: objetos compilados com uma versão de libstdc++ precisam se ligar a objetos de outra versão, então detalhes como o layout de std::string e a composição de std::regex_traits ficam congelados nos binários distribuídos
  • Essa restrição é detalhada em documentos como a política de ABI do libstdc++ e o Itanium C++ ABI
  • Usuários de Python escolhem python==3.12, usuários de Rust escolhem a edição em Cargo.toml, usuários de Java escolhem a versão do JDK e usuários de C# escolhem um TFM como net6.0 ou net8.0, mas o C++ não tem um Cargo.toml para std::
  • -std=c++26 escolhe quais headers e regras de linguagem usar, mas não oferece outro std::string nem um std::unordered_map redesenhado
  • Por isso, em 2026, a biblioteca padrão de C++ usada em produção ainda carrega, por design e por imposição, defaults ruins que o comitê aceitou desde 1998
  • Codebases modernos de C++ em trading firms tier-one, mecanismos de busca e navegadores dependem fortemente de bibliotecas não padronizadas como Boost, Abseil, Folly, EASTL, //base do Chromium, contêineres próprios, allocators customizados, CTRE, Outcome e bibliotecas de corrotinas

2 comentários

 
dieafterwork 3 시간 전

O texto original é bem extenso, e lendo até o fim dá bastante a sensação de devoção ao Rust.
Ainda assim, acabei aprendendo muitas coisas que não sabia. Obrigado pelo bom texto.

 
Opiniões no Lobste.rs
  • Pensando se houve um churn parecido no ecossistema Rust, parecem ter sido só alguns casos grandes
    Na época da Leakpocalypse, chegou-se à conclusão de que não dava para depender que destrutores Drop sempre seriam executados para preservar invariantes de segurança, e quase não houve mudanças reais de API, exceto pela remoção de std::thread::scoped. Depois surgiu um substituto que faz a mesma coisa de forma sound
    std::mem::uninitialized foi descontinuado e hoje é considerado unsound. Os tipos Range existentes devem ser substituídos lentamente por tipos novos quase idênticos para corrigir problemas relativamente pequenos de API. std::error::Error::description foi descontinuado porque a maioria dos tipos de erro não quer armazenar strings, e existe um substituto direto na forma da implementação de Display
    Considerando que std ficou estável por 11 anos, isso é bem impressionante, e o restante de std ainda existe e funciona, e 98% ainda é considerado Rust idiomático. Já a biblioteca padrão de C++ parece estar numa posição perigosa, leve demais no gatilho para adicionar funcionalidades e surpreendentemente conservadora para descontinuar qualquer coisa

    • Eu de algum jeito não conhecia nada sobre a Leakpocalypse: faultlore (2015)
    • Também me vem à cabeça o problema de o trait Iterator pegar emprestado o próprio conteúdo. É um problema crônico que continua aparecendo nas conversas sobre Rust como “por que não posso usar isso e preciso de um workaround?”
      Do mesmo modo, o fato de f32 e f64 não implementarem Cmp e terem em vez disso o método f32::total_cmp também é um incômodo frequente para quem está começando, então a gente acaba suspirando e explicando o contexto
      O aparato de formatação de panic também não é muito bom, e há muitos posts de blog dizendo que o panic handler padrão usa formatação, é difícil de desativar e acaba consumindo bastante do tamanho do executável
  • Pessoalmente, acho que o design envelhecido da biblioteca padrão prejudica bastante a popularidade e a usabilidade de C++
    Muitos problemas atribuídos à linguagem em si na verdade deveriam ser direcionados à biblioteca padrão
    Por exemplo, dizer que “C++ compila devagar” não é exatamente correto. Não é inerentemente lento por usar recursos de C++; o que deixa lento é a enorme hipertrofia de headers e dependências, além de a biblioteca padrão usar templates em excesso até para abstrações simples
    “C++ não é seguro” também é parcialmente verdade, mas o design da biblioteca padrão piora isso. Não há motivo para não aplicar a uma nova biblioteca padrão padrões mais seguros de design de API usados em Rust. Claro, uma das grandes vantagens de C++ é a compatibilidade retroativa, então é um problema muito complexo

    • Em alguns casos, sim. Dá para fazer vec[idx] lançar exceção ou abortar em vez de fazer acesso fora dos limites/comportamento indefinido. Mas, em muitos casos, por causa das diferenças da linguagem, é muito mais difícil criar APIs seguras em C++
      Rust tem movimentação destrutiva por padrão; C++ não. Por isso, APIs de smart pointers inevitavelmente acabam sendo unsafe ou, no mínimo, surpreendentes e propensas a crash. Por exemplo, abortando o programa ao acessar um smart pointer após ele ter sido movido
      Rust tem anotações de tempo de vida; C++ não. Então Rust consegue evitar coisas como invalidação de iteradores no design da API de iteradores, enquanto em C++ isso é praticamente difícil de verdade. Rust também tem pattern matching, o que permite que APIs como Option ofereçam de forma ergonômica a abordagem de “verificar e usar logo em seguida”. C++ até poderia fornecer uma versão de std::option em que acessar um valor vazio não gerasse UB, mas ela seria muito mais incômoda de usar do que no C++ atual ou no Rust. O operador ? do Rust também ajuda muito aqui
      Sei que dá para acrescentar algo parecido com pattern matching em C++ com um conjunto de overloads, como em std::variant, mas me parece bem mais difícil de usar e mais fácil de errar
    • Acho que com C é a mesma coisa. Muitos problemas de C vêm do fato de a stdlib ser muito ruim
      Só uma biblioteca moderna com strings e arrays, alguns containers genéricos e suporte nativo a allocator já tornaria C muito mais ergonômico e fácil de usar. Claro, alguns defeitos da linguagem não desaparecem só trocando a biblioteca, mas ainda assim isso já levaria bem longe
      Se você olhar codebases modernas em C, elas usam extensivamente bibliotecas customizadas para allocator, strings, vector, hash table e operações de sistema de arquivos, e se você tiver experiência com C ou com gerenciamento manual de recursos, não é tão difícil seguir nessa linha
    • Na empresa, usamos uma implementação de slice<T, N> capaz de representar “um ponteiro para exatamente N bytes” ou “um ponteiro para uma quantidade arbitrária de bytes”
      Ela tem head(n), tail(n), slice(start, end) e operador de índice, e todos fazem verificação de limites
      É realmente um prazer trabalhar com abstrações assim, mas, para obter uma linguagem moderna e razoavelmente segura, na prática você acaba tendo que portar as bibliotecas padrão de Rust e Zig para C++. Ainda assim, no fim vale o esforço investido
    • Se decidirmos usar menos templates para abstrações simples, não vamos perder desempenho?
  • Se vai escrever um texto desses, por favor escreva você mesmo. Mesmo que a lista tenha sido feita manualmente, jogar isso num LLM e despejar o resultado numa página para pessoas lerem é rude demais. Se eu vir mais uma vez uma frase dizendo que um “engenheiro em atividade” aprende desde o “primeiro dia” a evitar a “funcionalidade X”, vou enlouquecer
    O vergonhoso é que aqui existe muito a ser dito, mas no fim não se diz nada. Deve ter havido um motivo para produzir esse texto, então eu queria que esse motivo fosse dito. Alguma parte de C++ deve ter te irritado, e alguma funcionalidade deve ter te confundido. O motivo de essas funcionalidades serem ruins não é só um fracasso objetivo de design, mas também o efeito que elas têm sobre nós
    Se você já usou std::iterator e levou bronca no Slack, ou deixou de usar reinterpret_cast porque ele tem 16 letras e ia estragar um pouco a formatação da linha, seria melhor ver esse tipo de história no Lobsters. Se essas histórias não existem, então não invente à força e não faça uma GPU gerar a mesma frase 10 vezes com multiplicação de matrizes. Comente só as partes que merecem comentário e escreva o resto em tabelas e bullet points

    • Esse texto não soa como se tivesse sido escrito por um LLM
  • Uso C++ há 20 anos e ainda uso, mas concordo bastante com este texto. O realmente bom em usar Rust hoje em dia, mais do que a segurança de memória, é a excelente biblioteca padrão e o ecossistema de pacotes
    Um exemplo representativo é a biblioteca de ranges. Já faz 6 anos que foi padronizada, mas as principais bibliotecas padrão ainda não conseguiram implementá-la completamente e, mesmo quando implementam, há só alguns poucos combinadores. O equivalente em Rust, os métodos de Iterator, são 76, e com um único cargo add surgem mais 130 via trait itertools
    Outra coisa de que sinto muita falta é pattern matching. Dá para tornar tipos union como std::variant ergonômicos. A proposta está em discussão, mas ainda não entrou nem no C++26, e isso é uma pena. Em compensação, contracts e executors estão entrando, mas sinceramente nunca vi ninguém ao meu redor pedindo isso

    • Um dos problemas do C++ é que não existe um critério oficial e documentado sobre o que deve ser funcionalidade da linguagem e o que deve ser funcionalidade da biblioteca padrão
      Em geral, o critério que eu vejo é o seguinte. Se uma funcionalidade dá suporte a casos de uso desejáveis e não pode ser expressa na biblioteca padrão, então ela deve entrar na linguagem. Sempre que possível, o ideal é decompor a funcionalidade desejada em elementos mínimos e independentes que também possam ser usados para outros fins
      Funcionalidades usadas em praticamente toda base de código devem entrar na biblioteca padrão. Se um tipo é comumente usado como interface entre bibliotecas, ele deve entrar na biblioteca padrão. Ninguém quer que toda biblioteca defina seu próprio tipo de tuple ou sua própria string. Em C++, com o primeiro caso isso de fato acontecia até o C++11, e com o segundo ainda acontece porque std::string é um desastre. Isso também se aplica a tipos de interface, e hoje em dia o C++ lida com isso principalmente com concepts
      O resto deve ir para bibliotecas reutilizáveis e modulares. Rust é bastante bom em ter um conjunto estável e blessed de bibliotecas externas, então a pressão do tipo “toda engine escrita em Rust precisa desta estrutura de dados, então vamos colocá-la na biblioteca padrão” é muito menor. Quem faz jogos pode simplesmente importar os crates de que precisa. C++ nunca assimilou de verdade a ideia de “um bom pacote para recomendar a um problema que muita gente tem, mas não a maioria”
  • O que me preocupa é quais das coisas que estão sendo adicionadas agora acabarão sendo revertidas no futuro. Contracts acabou de entrar no C++26, e já estão apontando falhas graves de design
    Não quero criticar genericamente o “design por comitê”. Acho que organizações assim cumprem um propósito importante e têm pontos fortes próprios. Só que essa força não está em projetar funcionalidades totalmente novas do zero, em campo aberto
    Onde WG21 e WG14 realmente brilham é em pegar funcionalidades cujo espaço de design já foi explorado até certo ponto e, se possível, já com várias implementações existentes, e transformá-las em recursos padronizados que a maioria dos usuários e implementadores consiga aceitar. std::embed é um exemplo disso
    Em contrapartida, quando se padroniza cedo demais — como com a extensão de GC mencionada no artigo, std::memory_order_consume ou os modules do C++20 — antes mesmo de alguém conseguir implementar direito, a coisa tende a dar muito errado

    • C++ e Haskell foram ambos projetados por comitê, mas as duas linguagens são quase opostas entre si. Sempre que der vontade de pensar que “$X foi projetado por comitê” implica alguma coisa sobre $X, vale lembrar disso
  • Fiquei bem chocado quando percebi, há um tempo, que o C++ não versiona a biblioteca padrão. Não esperava que este texto fosse apontar isso tão diretamente
    Também achei interessante a menção de que o Go é de forma parecida conservador em relação à compatibilidade futura. Mas o Go também é igualmente conservador até nas funcionalidades que adiciona, então parece ter evitado a maior parte dos problemas do C++. O fato de não ter uma ABI estável provavelmente também ajudou
    Entre as bibliotecas populares que conheço, a única que expõe explicitamente uma ABI de C++ é a libcamera, e isso é bem incômodo. Pela minha experiência, até bibliotecas C++ normalmente exportam símbolos com ABI C, e isso também facilita a interoperabilidade com outras linguagens. Posso estar deixando passar alguma coisa
    E entre Clang e MSVC não existem umas quirks de compatibilidade de ABI? Lembro do Conan desencorajar explicitamente ou até proibir misturar compiladores, então fico em dúvida sobre por que o comitê de C++ se esforça tanto para preservar estabilidade de ABI

    • Isso não está totalmente certo. O C++ só não versiona a biblioteca padrão independentemente da linguagem
      Há duas coisas intimamente relacionadas aqui: a especificação da biblioteca padrão e a implementação. A especificação é para a combinação completa linguagem+biblioteca, e a implementação em geral tenta suportar pelo menos uma ou mais versões da especificação
      Existem muitas bibliotecas que expõem interfaces em C++, inclusive algumas bem grandes, como o Qt
      O problema é que a máquina abstrata do C++ não define o processo de linkedição. Então ela não consegue definir como bibliotecas dinâmicas funcionam. O linking dinâmico de C++ em sistemas UNIX segue o modelo de C. Finge que é linking dinâmico e joga a responsabilidade para problemas do loader. Isso leva a coisas horríveis como copy relocation. O Windows tem uma noção muito mais fundamentada do que é uma biblioteca compartilhada, mas por causa disso alguns idioms de bibliotecas C++ de UNIX não funcionam no Windows
      Bibliotecas compartilhadas são um grande problema para recursos como templates em C++. Se você quer poder instanciar um template com tipos do usuário, a definição completa precisa estar no header, porque o compilador não consegue enxergar além dos limites da unidade de compilação. Em bibliotecas compartilhadas, o mesmo código é instanciado em vários lugares. Se o programa e a biblioteca instanciam o mesmo template com os mesmos parâmetros, ambos ficam com cópias, e o linker e o loader precisam fazer com que só uma seja usada no programa final carregado
      Comparando com Swift, o Swift declara explicitamente: “bibliotecas compartilhadas existem, e a linguagem expõe construções em nível de linguagem para representá-las”. Se você quiser expor genéricos além do limite de uma biblioteca compartilhada, isso é possível, mas para todos os chamadores externos eles são rebaixados para uma versão com despacho dinâmico. Em C++ também dá para implementar isso manualmente. Você faz um template de versão geral usando um wrapper com apagamento de tipo e escreve explicitamente outras instanciações concretas. Mas isso é difícil e manual. Em Swift, simplesmente “é assim que funciona no limite de uma biblioteca compartilhada”
      O mesmo vale para ocultação de tipos. O C++ usa o padrão pImpl para criar uma interface pública que expõe o comportamento além do limite da biblioteca, mas esconde a implementação. O Swift tem uma máquina abstrata que sabe onde estão os limites da biblioteca e diz: “o tamanho de um tipo que não foi explicitamente marcado como ABI-stable não é uma constante de tempo de compilação além do limite de uma biblioteca compartilhada”
      Isso também é outra forma de o padrão negar a realidade. Quase toda base de código C++ não trivial em que trabalhei foi compilada com -fno-rtti -fno-exceptions ou com as opções equivalentes do CL.EXE. O padrão não reconhece isso como possibilidade. A maioria das funções da biblioteca padrão ainda pressupõe exceções para relatar erros, então, se você compilar com -fno-exception, ela simplesmente chama abort. Isso faz com que elementos da biblioteca padrão que fazem alocação dinâmica de memória fiquem inutilizáveis em ambientes embarcados. std::vector<T>::push_back pode fazer o programa travar
      A parte do artigo que diz que “o comitê não só não consegue remover recursos ruins, como continua adicionando novos recursos que engenheiros de software da prática não pediram” é 100% igual ao modo como contracts surgiu. O Verus mostra o que um bom sistema de contracts pode possibilitar em uma linguagem voltada para ambientes como os do C++. O P2900 contracts é uma combinação de requisitos conflitantes, então piora qualquer problema para o qual contracts poderia servir
      Não acho verdadeira a conclusão de que um “engenheiro de C++” recebe um salário muito maior do que um “engenheiro que sabe programar”. Na prática, ninguém escreve código exatamente segundo o padrão C++; cada um escreve de acordo com o seu subconjunto-de-um-superconjunto interno favorito
    • O go vet também tem valor aqui. Porque fornece upgrade automático para melhoria de API
  • Desde o ano passado, abandonei quase totalmente o C++ e fui primeiro para Kotlin, depois para Swift. Na empresa ainda preciso manter C++, mas o código novo que escrevo é muito mais limpo, conciso e seguro. Estou aceitando tradeoffs em tamanho do código e talvez em desempenho, mas vale a pena

  • Lembrei que a semântica do for loop do Go mudou quebrando compatibilidade retroativa, então achei que esta frase estava errada: https://go.dev/blog/loopvar-preview
    Mas aí descobri que o Go também usa aqui uma abordagem parecida com as editions do Rust. Você precisa declarar a versão Go 1.22 ou superior para que a semântica mude. Talvez também desse para remover io/ioutil desse jeito, mas aparentemente não vale a pena quebrar código até atravessando o limite de uma edition

  • Se o C++ não tivesse realmente tentado essas más ideias e provado que eram más ideias, talvez o Rust não existisse na forma atual. Um grande obrigado!

  • Tenho interesse em um substituto da biblioteca padrão ao estilo Rust para C++. Conheço o rpp, que tem esse objetivo: https://github.com/TheNumbat/rpp
    Haveria outras opções? Não estou falando de outras implementações da stdlib de C++, como EASTL, mas de bibliotecas que sigam o Rust mais de perto. Sei que algumas partes, como std::initializer_list, estão embutidas na sintaxe, mas o resto todo pode ser trocado