Maps Cache-Friendly em C++23 Localidade e Indexação Múltipla
O std::map tem sido por décadas a estrutura associativa fundamental para qualquer programador C++, e dominar seu uso e entender sua implementação baseada em árvore rubro-negra é essencial para compreender trade-offs entre complexidade algorítmica e performance real. Entretanto, com a chegada do std::flat_map ao C++23, pela primeira vez a biblioteca padrão oferece uma alternativa que prioriza localidade de cache sobre complexidade assintótica de inserção, refletindo uma mudança de paradigma no design de estruturas de dados que considera explicitamente a hierarquia de memória moderna. Esta é uma oportunidade pedagógica para estudar quando privilegiar a complexidade teórica versus performance prática, como a organização física dos dados afeta drasticamente o desempenho em sistemas reais, e por que uma estrutura com operações teoricamente mais lentas pode ser mais rápida na prática dependendo do padrão de acesso.
Containers associativos são estruturas de dados que armazenam elementos organizados por chaves em vez de posições numéricas sequenciais. Diferentemente de um vetor no qual você acessa o terceiro elemento através do índice
2, em um container associativo você acessa elementos através de chaves que podem ser strings, números, ou qualquer tipo comparável. Pense na diferença entre uma lista telefônica ordenada alfabeticamente, na qual você busca pessoas pelo nome, e uma agenda numerada sequencialmente em que você precisa saber que o contato está na posição15. A lista telefônica é associativa porque associa nomes a números de telefone, permitindo busca eficiente sem precisar percorrer todos os elementos. Em C++, os principais containers associativos sãostd::map,std::set, e suas variantes comostd::multimapestd::multiset, todos tradicionalmente implementados usando árvores balanceadas que garantem operações de busca, inserção e remoção em tempo logarítmico. O C++23 adiciona as variantesflatque mantêm as mesmas propriedades associativas mas com organização física diferente na memória, priorizando localidade de cache sobre complexidade de inserção.
A Chegada dos Flat Maps ao C++23
Depois de anos disponível apenas através da biblioteca Boost, o C++ finalmente incorporou à biblioteca padrão os containers ordenados com armazenamento contíguo os flat maps. Esta é uma evolução significativa que altera fundamentalmente a forma como os dados são organizados na memória.
A classe std::flat_map mantém seus elementos em vetores contíguos na memória, diferentemente do std::map tradicional que usa uma estrutura de árvore na qual cada elemento pode estar em uma região diferente da memória. Para visualizar essa diferença, a criativa leitora pode imaginar uma biblioteca: o std::map seria como ter os livros espalhados por diferentes salas, cada livro com um ponteiro indicando onde encontrar o próximo livro. Por outro lado, o std::flat_map seria como ter uma biblioteca em que todos os livros estão perfeitamente alinhados em uma única estante. Trazendo nossa metáfora para o ambiente computacional, quando seu algoritmo precisa percorrer a biblioteca, a segunda abordagem é muito mais eficiente porque o processador consegue carregar vários elementos de uma vez para seu cache.
Vejamos um exemplo básico de uso:
#include <flat_map>
std::flat_map<std::string, int> scores = {
{"alice", 100},
{"bob", 200},
{"carol", 150}
};
// Busca ainda é O(log n), mas com excelente localidade de cache
if (auto it = scores.find("bob"); it != scores.end()) {
std::cout << it->second << '\n';
}
No código acima, estamos criando um mapeamento de nomes para pontuações. A sintaxe é praticamente idêntica ao std::map tradicional, o que facilita a migração de código existente. A busca com find continua sendo logarítmica em termos de complexidade assintótica, mas na prática é mais rápida porque os dados estão organizados de forma contígua na memória.
As vantagens do std::flat_map em relação ao std::map clássico parecem mais significativas quando entendemos o comportamento da hierarquia de memória do computador. Neste cenário precisamos dar atenção a dois aspectos principais: localidade de referência e overhead de alocação.
A melhor localidade de referência ocorre quando seu algoritmo acessa um elemento e é muito provável que os elementos vizinhos já estejam no cache do processador, tornando travessias sequenciais significativamente mais rápidas.
O menor overhead de alocação ocorre porque não será necessário alocar memória separadamente para cada nó da árvore, apenas para os vetores subjacentes. Os dados armazenados em vetores contíguos também são ideais para otimizações Single Instruction, Multiple Data, SIMD, quando aplicável.
Como nada é perfeito, essa estrutura também tem suas desvantagens.
A inserção e remoção de elementos passam a ter complexidade $O(n)$ em vez de $O(\log n)$, porque pode ser necessário mover todos os elementos subsequentes para manter o vetor ordenado. Isso torna o std::flat_map ideal principalmente quando o conjunto de elementos é relativamente estático, ou seja, quando fazemos muitas consultas mas poucas modificações.
Um ponto de atenção na migração de código legado é a estabilidade dos iteradores. O std::map oferece uma garantia robusta: a inserção ou remoção de elementos não invalida ponteiros ou referências para outros elementos, pois os nós da árvore permanecem fixos em seus endereços de memória originais. Isso permite padrões de código nos quais mantemos um iterador apontando para um objeto específico enquanto modificamos outras partes do container sem risco.
Com o std::flat_map, essa premissa desaparece. Devido à contiguidade do armazenamento subjacente, qualquer inserção que provoque um redimensionamento do vetor, ou qualquer operação que desloque elementos para manter a ordenação, poderá invalidar todos os iteradores, ponteiros e referências existentes. Tratar um std::flat_map como um substituto direto (drop-in replacement) para o std::map em códigos que dependem dessa estabilidade de endereços resultará quase certamente em Undefined Behavior e falhas de segmentação difíceis de depurar.
Além dos mapas planos, o C++23 trouxe outros aprimoramentos interessantes para containers associativos. Os métodos std::map::merge e std::multimap::merge ganharam overloads mais flexíveis, permitindo combinações mais eficientes de containers. Algoritmos como std::shift_left e std::shift_right foram adicionados para ajudar a manter vetores ordenados, o que é particularmente útil quando trabalhamos com flat_map. A integração com ranges e views também foi aprimorada, tornando o código mais expressivo e componível.
Vale mencionar que o método contains(), introduzido no C++20, continua sendo uma das adições mais comuns no dia a dia. Este método simplifica a verificação de existência de chaves:
std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
// Antes do C++20, precisávamos fazer:
// if (m.find(2) != m.end()) { ... }
// Agora podemos simplesmente escrever:
if (m.contains(2)) {
std::cout << "encontrado!\n";
}
Algoritmos de Deslocamento e a Mecânica Interna do flat_map
Para entender verdadeiramente por que std::flat_map foi incluído no C++23 e não antes, precisamos examinar como os algoritmos que o suportam evoluíram ao longo das últimas versões do padrão. A chegada de std::shift_left e std::shift_right no C++20 representou uma peça fundamental deste quebra-cabeça, porque estes algoritmos implementam exatamente as operações que flat_map precisa executar constantemente: mover elementos em vetores ordenados de forma eficiente.
Quando removemos um elemento de um flat_map, a estrutura precisa fechar a lacuna deixada mantendo todos os elementos ordenados. Antes do C++20, você precisaria implementar esta operação manualmente usando combinações de std::copy ou std::move, frequentemente com lógica propensa a erros de índice. Vejamos como shift_left simplifica dramaticamente esta operação:
std::vector<int> sorted_data = {1, 3, 5, 7, 9, 11, 13};
// Queremos remover o elemento na posição 3 (valor 7)
auto pos = sorted_data.begin() + 3;
// shift_left move todos os elementos após pos uma posição para a esquerda
// sobrescrevendo o elemento que queremos remover
std::shift_left(pos, sorted_data.end(), 1);
// Agora podemos simplesmente remover o último elemento
sorted_data.pop_back();
// Resultado: {1, 3, 5, 9, 11, 13}
O que torna shift_left particularmente elegante é que ele foi projetado especificamente para operações de deslocamento, usando semântica de movimento quando possível. Compare isso com a abordagem anterior usando std::copy:
// Abordagem antiga, mais verbosa e menos clara na intenção
std::copy(pos + 1, sorted_data.end(), pos);
sorted_data.pop_back();
Ambos os códigos fazem a mesma coisa, mas shift_left expressa a intenção de forma muito mais clara. O nome do algoritmo comunica imediatamente o que está acontecendo: estamos deslocando elementos para a esquerda. Esta clareza não é apenas estética. Quando você está implementando ou mantendo um container complexo como flat_map, código que expressa intenção claramente reduz significativamente a probabilidade de bugs.
A operação inversa, inserção em posição ordenada, também se beneficia de shift_right. Quando adicionamos um novo elemento a um flat_map, precisamos encontrar a posição correta e abrir espaço para o novo elemento:
std::vector<int> sorted_data = {1, 3, 5, 9, 11, 13};
// Queremos inserir o valor 7 na posição correta
auto insert_pos = std::lower_bound(sorted_data.begin(),
sorted_data.end(), 7);
// Primeiro expandimos o vetor para criar espaço
sorted_data.push_back(0); // Valor temporário
// shift_right move todos os elementos de insert_pos até o fim
// uma posição para a direita, abrindo espaço
std::shift_right(insert_pos, sorted_data.end(), 1);
// Agora podemos inserir o novo valor na posição aberta
*insert_pos = 7;
// Resultado: {1, 3, 5, 7, 9, 11, 13}
Esta sequência de operações, busca binária seguida de deslocamento, é exatamente o que flat_map executa internamente em cada inserção. A complexidade permanece $O(n)$ porque estamos movendo potencialmente todos os elementos, mas a implementação usando shift_right é mais eficiente em nível de instruções porque o algoritmo foi otimizado pelos implementadores da biblioteca padrão.
Um aspecto fascinante destes algoritmos é que eles foram projetados pensando em otimizações futuras. A especificação permite que implementações usem instruções SIMD para mover múltiplos elementos simultaneamente quando o tipo permite. Para tipos trivialmente copiáveis, como inteiros ou ponteiros, compiladores modernos podem transformar um shift_left em uma única instrução memmove altamente otimizada, ou até usar instruções vetoriais que movem 16 ou 32 bytes por vez. Esta é uma das razões pelas quais flat_map pode ser surpreendentemente rápido mesmo com complexidade de inserção teoricamente pior.
Operações de merge e a Vantagem da Contiguidade
A operação de merge entre containers associativos ganhou overloads significativamente mais poderosas no C++23, e estas melhorias foram projetadas tendo em mente tanto std::map tradicional quanto os novos flat containers. Para entender a elegância do design, precisamos examinar como merge funciona em cada contexto e por que a contiguidade de memória do flat_map oferece vantagens específicas.
Quando fazemos merge de dois std::map, estamos essencialmente transferindo nós de uma árvore para outra. Cada nó precisa ser desvinculado da estrutura de origem, rebalanceado, e inserido na estrutura de destino, potencialmente causando rebalanceamentos adicionais. É uma operação complexa que toca muitas partes da memória de forma não-sequencial:
std::map<int, std::string> primary = {{1, "one"}, {3, "three"}};
std::map<int, std::string> secondary = {{2, "two"}, {4, "four"}};
// `merge` transfere todos os elementos únicos de secondary para primary
primary.merge(secondary);
// primary agora contém: {1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}
// secondary fica vazio (todos os elementos foram transferidos)
Esta operação tem complexidade $O(n \log(n+m))$ na qual $n$ é o tamanho do destino e $m$ é o tamanho da origem. Cada elemento de secondary precisa ser inserido em primary, e cada inserção é uma operação logarítmica na árvore.
Agora compare com flat_map. Quando fazemos merge de dois flat maps, ambos já estão ordenados internamente porque são mantidos em vetores ordenados. Isso significa que podemos usar um algoritmo muito mais eficiente: merge de sequências ordenadas, que é fundamentalmente $O(n+m)$ linear:
std::flat_map<int, std::string> primary = {{1, "one"}, {3, "three"}};
std::flat_map<int, std::string> secondary = {{2, "two"}, {4, "four"}};
// Internamente, isto se torna um `merge` de dois vetores ordenados
primary.merge(secondary);
// A implementação pode usar std::merge ou std::inplace_merge
// que são otimizados para dados contíguos
A implementação interna pode aproveitar que ambos os vetores já estão ordenados e realizar a operação em uma única passagem linear. Pense em mesclar duas pilhas ordenadas de cartas: você simplesmente compara os topos e move o menor para a pilha de resultado. Quando os dados estão em vetores contíguos, esta operação pode usar instruções de processamento em lote e tem excelente localidade de cache.
Mas a história fica ainda mais interessante quando consideramos merge de múltiplos containers. No C++23, podemos encadear operações de merge de forma muito mais fluida, e flat_map brilha neste cenário:
std::flat_map<int, std::string> consolidated;
std::vector<std::flat_map<int, std::string>> partitions = {
{{1, "one"}, {2, "two"}},
{{3, "three"}, {4, "four"}},
{{5, "five"}, {6, "six"}}
};
// Consolidamos todas as partições em um único flat_map
for (auto& partition : partitions) {
consolidated.merge(std::move(partition));
}
// consolidated agora contém todos os elementos de todas as partições
// e cada `merge` foi uma operação linear aproveitando ordenação
Este padrão aparece com frequência em processamento paralelo: você divide dados em partições, processa cada partição independentemente mantendo ordenação, e depois consolida os resultados. Com flat_map, cada consolidação é uma operação de merge linear sobre dados contíguos, e compiladores modernos podem vetorizar partes desta operação.
O C++23 também introduziu melhorias na forma como merge trata duplicatas. Quando uma chave existe em ambos os containers, apenas o valor do container de destino é mantido, e o elemento da origem permanece na origem. Isto permite estratégias sofisticadas de resolução de conflitos:
std::flat_map<std::string, int> current_scores = {
{"alice", 100}, {"bob", 200}
};
std::flat_map<std::string, int> new_scores = {
{"alice", 150}, // Tentativa de atualizar
{"carol", 300} // Novo jogador
};
current_scores.merge(new_scores);
// current_scores mantém alice:100 (valor original preservado)
// mas adiciona carol:300 (chave nova)
// new_scores ainda contém alice:150 (não foi movido porque alice já existia)
Esta semântica permite que você use merge como uma operação de “adicionar apenas elementos novos”, inspecionando depois o container de origem para ver quais elementos não foram transferidos. Para flat_map, verificar o que resta no container de origem após merge é particularmente eficiente porque você pode iterar linearmente sobre o vetor subjacente.
A coesão do design do C++23 fica evidente quando você percebe como estes componentes foram projetados para trabalhar juntos. Os algoritmos shift_left e shift_right fornecem os blocos de construção que flat_map precisa para manutenção de ordenação. As melhorias em merge tornam operações em lote sobre múltiplos containers muito mais eficientes. E toda esta infraestrutura aproveita a contiguidade de memória para permitir otimizações que seriam impossíveis com estruturas baseadas em nós. O resultado é um ecossistema de containers e algoritmos que não apenas funciona corretamente, mas que foi projetado desde o início para extrair máxima performance do hardware moderno com sua complexa hierarquia de memória.
Implementando um map com Dois Tipos de Chaves
Mesmo com todas as novidades do C++23, surge frequentemente uma necessidade que a biblioteca padrão não resolve diretamente: a capacidade de consultar o mesmo valor usando dois tipos diferentes de chaves. Imagine, por exemplo, um sistema em que cada usuário tem um ID numérico e também um UUID, e você precisa buscar o usuário tanto pelo ID quanto pelo UUID de forma eficiente. Ou um catálogo de produtos no qual cada item tem um código interno e também um nome, e ambos devem permitir acesso rápido.
Vamos primeiro entender o que queremos alcançar. Em um mundo ideal, céu azul, campos verdes e brisa suave, gostaríamos de usar nossa estrutura assim:
m.insert(0, '0', "zero");
m.insert(1, '1', "one");
// Ambas as formas devem funcionar e retornar o mesmo valor:
EXPECT_EQ(m[1], "one");
EXPECT_EQ(m['1'], "one");
Note que estamos inserindo um valor (“one”) e queremos recuperá-lo usando tanto a chave numérica, 1 quanto a chave caractere, '1'. A implementação que vamos desenvolver mantém dois std::map internos rigorosamente sincronizados. Esta abordagem pode parecer inicialmente redundante, mas na verdade é robusta e útil. Vejamos a estrutura completa:
template <typename Key1, typename Key2, typename Value>
class doublekey_map {
// Primeiro `map`: vai da primeira chave para a segunda chave
std::map<Key1, Key2> k1_to_k2_;
// Segundo `map`: vai da segunda chave para o valor
std::map<Key2, Value> k2_to_val_;
public:
void insert(Key1 k1, Key2 k2, Value v) {
// Estabelecemos a conexão entre as duas chaves
k1_to_k2_[k1] = k2;
// E conectamos a segunda chave ao valor
k2_to_val_[k2] = std::move(v);
}
std::optional<Value> get(Key1 k1) const {
// Primeiro, buscamos a segunda chave usando a primeira
auto it = k1_to_k2_.find(k1);
if (it != k1_to_k2_.end())
// Se encontramos, usamos a segunda chave para buscar o valor
if (auto jt = k2_to_val_.find(it->second); jt != k2_to_val_.end())
return jt->second;
return std::nullopt;
}
std::optional<Value> get(Key2 k2) const {
// Busca direta no segundo `map` quando usamos a segunda chave
if (auto it = k2_to_val_.find(k2); it != k2_to_val_.end())
return it->second;
return std::nullopt;
}
void erase(Key1 k1) {
// Encontramos a entrada pela primeira chave
if (auto it = k1_to_k2_.find(k1); it != k1_to_k2_.end()) {
// Removemos do segundo `map` usando a segunda chave
k2_to_val_.erase(it->second);
// E removemos do primeiro `map`
k1_to_k2_.erase(it);
}
}
void erase(Key2 k2) {
// Removemos do segundo `map`
if (auto it = k2_to_val_.find(k2); it != k2_to_val_.end()) {
// Precisamos encontrar e remover todas as entradas no primeiro `map`
// que apontam para esta segunda chave
for (auto i = k1_to_k2_.begin(); i != k1_to_k2_.end();) {
if (i->second == k2)
i = k1_to_k2_.erase(i);
else
++i;
}
k2_to_val_.erase(it);
}
}
};
Neste ponto, podemos investir algum tempo para entender em detalhes como esta estrutura funciona. O primeiro map, k1_to_k2_, funciona como um índice que conecta a primeira chave à segunda chave. O segundo map, k2_to_val_, armazena efetivamente os dados, conectando a segunda chave ao valor. Esta arquitetura de dois níveis é a chave para a robustez da solução.
Quando inserimos um elemento, primeiro estabelecemos a conexão entre as duas chaves no primeiro map, depois conectamos a segunda chave ao valor no segundo map. Note o uso de std::move(v) na inserção do valor, que permite transferir a propriedade de tipos movíveis sem cópias desnecessárias.
As funções de busca são sobrecarregadas para aceitar ambos os tipos de chaves. Quando buscamos pela primeira chave, fazemos uma busca em dois passos: primeiro encontramos a segunda chave correspondente, depois usamos essa segunda chave para encontrar o valor. Quando buscamos diretamente pela segunda chave, a operação é mais eficiente porque vai direto ao segundo map. Ambas as operações têm complexidade $O(\log n)$, que é exatamente o que queremos.
A remoção de elementos merece atenção especial. Quando removemos pela primeira chave, o processo é direto: encontramos a segunda chave correspondente, removemos o valor do segundo map, e então removemos a entrada do primeiro map. Quando removemos pela segunda chave, precisamos fazer uma varredura no primeiro map para remover todas as entradas que apontam para aquela segunda chave. Esta assimetria é necessária para manter a consistência.
Esta implementação revela-se robusta e eficiente em cenários reais. Com apenas dois std::map internos mantidos rigorosamente sincronizados, obtemos consulta em $O(\log n)$ por qualquer uma das duas chaves e consistência absoluta. A estrutura elimina por completo o risco de chaves órfãs e oferece segurança forte diante de exceções, porque se qualquer operação falhar, os maps permanecem em estados válidos. O uso sistemático de std::move garante suporte natural a tipos movíveis. É uma solução minimalista e confiável quando precisamos de acesso rápido por duas chaves distintas.
Uma dica interessante para código C++23: se sua aplicação faz muitas travessias e poucas modificações, você pode substituir os std::map internos por std::flat_map, obtendo ainda melhor performance em operações de leitura.
Modificando Chaves de Forma Segura com Node Extraction
Existe um problema interessante que surge ocasionalmente: e se precisarmos modificar a própria chave de um elemento já inserido em um map? Por exemplo, imagine que você digitou “two” errado e quer corrigir para “dois”, mas sem perder o valor associado nem realocar memória desnecessariamente.
O std::flat_map mantém suas chaves const por design, e isso é correto porque modificar uma chave em um container ordenado poderia quebrar o invariante de ordenação. Entretanto, nos containers baseados em nós como std::map e std::set, existe desde C++17 uma técnica idiomática que permite modificar chaves de forma segura: a extração de nós. Vejamos um exemplo básico:
std::map<std::string, int> m{{"one",1}, {"two",2}, {"three",3}};
// Extraímos o nó que contém a chave "two"
auto node = m.extract("two");
// Verificamos se a extração foi bem-sucedida
if (!node.empty()) {
// Modificamos a chave diretamente
node.key() = "dois";
// Reinserimos o nó com a nova chave
m.insert(std::move(node));
}
O que está acontecendo aqui é fascinante do ponto de vista de gerenciamento de memória. O método extract remove o nó da estrutura do map mas não o destroi, devolvendo um handle para o nó. Este handle permite modificar até mesmo membros que normalmente seriam const, como a chave. Depois de modificar, podemos reinserir o nó no container usando std::move, transferindo a propriedade de volta. O nó nunca é desalocado nem realocado, apenas reorganizado na estrutura do map.
Podemos generalizar esta técnica usando conceitos do C++20, refinados no C++23, para criar uma função que funciona com qualquer container associativo que suporte extração de nós:
template<class C>
void replace_key(C& container,
auto const& old_key,
auto const& new_key)
// Este conceito garante que o container suporta node handles
// e que podemos acessar key() e value()
requires requires(typename C::node_type n) {
n.key(); n.value();
}
{
// Extraímos o nó com a chave antiga
auto nh = container.extract(old_key);
// Verificamos se a extração foi bem-sucedida
if (!nh.empty()) {
// Para maps, modificamos a chave
// Para sets, modificamos o valor (que é a chave)
if constexpr (requires { nh.key(); })
nh.key() = new_key;
else
nh.value() = new_key;
// Reinserimos o nó modificado
container.insert(std::move(nh));
}
}
Esta função template é notavelmente flexível. O conceito na cláusula requires garante que só podemos instanciar esta função para tipos que realmente suportam node handles. O if constexpr permite que a mesma função funcione tanto com std::map, na qual acessamos key(), quanto com std::set, acessamos value(). A função funciona perfeitamente com std::map<K,V>, std::multimap, std::set e std::multiset.
Para entender melhor a vantagem desta abordagem, compare com o método tradicional que precisávamos usar antes do C++17:
// Abordagem antiga: requer cópia do valor e duas operações no `map`
auto it = m.find("two");
if (it != m.end()) {
auto value = std::move(it->second); // Move o valor para fora
m.erase(it); // Desaloca o nó
m["dos"] = std::move(value); // Aloca novo nó e move o valor de volta
}
// Com extract: zero cópias, apenas reorganização do nó
auto node = m.extract("two");
if (!node.empty()) {
node.key() = "dos";
m.insert(std::move(node));
}
Ambas as abordagens têm complexidade $O(\log n)$ em termos assintóticos, mas a segunda evita completamente a alocação e desalocação de memória. O nó simplesmente é removido da estrutura, modificado, e reinserido. Para valores grandes ou quando estamos em caminhos críticos de performance, esta diferença pode ser significativa.
Conclusão e Perspectivas
O C++23 finalmente entrega std::flat_map e std::flat_multimap, containers que a comunidade esperava há mais de uma década. Estes containers trazem as vantagens de localidade de cache que conhecíamos do Boost para a biblioteca padrão, tornando-os acessíveis sem dependências externas. As pequenas melhorias ergonômicas em algoritmos de merge e integração com ranges também facilitam o trabalho diário com estes containers.
Técnicas avançadas como maps de chave dupla e modificação segura de chaves via node handles continuam indispensáveis em cenários reais. A primeira resolve elegantemente o problema de indexação múltipla, comum em sistemas de informação. A segunda permite otimizações de memória que podem ser cruciais em sistemas de alta performance.
Com conceitos, if constexpr, dedução aprimorada de tipos e os novos containers flat, o C++23 permite escrever código ainda mais expressivo, seguro e performático. A linguagem continua evoluindo para tornar padrões complexos mais simples de expressar, sem sacrificar o controle fino sobre performance que sempre caracterizou C++.
Em novembro de 2025, std::flat_map já tem suporte completo no GCC 14+, Clang 18+ e MSVC 19.40+. Você pode usar estas funcionalidades com confiança em projetos modernos, sabendo que o suporte em compiladores está maduro e estável.