Roubar recursos das chaves do std :: map é permitido?


15

Em C ++, não há problema em roubar recursos de um mapa que eu não preciso mais mais? Mais precisamente, suponha que eu possua um std::mapcom std::stringchaves e que eu queira construir um vetor roubando os recursos das mapchaves s usando std::move. Observe que esse acesso de gravação às chaves corrompe a estrutura de dados interna (ordenação de chaves) da mapmas não a usarei posteriormente.

Pergunta : Posso fazer isso sem problemas ou isso levará a erros inesperados, por exemplo, no destruidor do, mapporque eu o acessei de uma maneira que std::mapnão foi planejada?

Aqui está um programa de exemplo:

#include<map>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
int main(int argc, char *argv[])
{
    std::vector<std::pair<std::string,double>> v;
    { // new scope to make clear that m is not needed 
      // after the resources were stolen
        std::map<std::string,double> m;
        m["aLongString"]=1.0;
        m["anotherLongString"]=2.0;
        //
        // now steal resources
        for (auto &p : m) {
            // according to my IDE, p has type 
            // std::pair<const class std::__cxx11::basic_string<char>, double>&
            cout<<"key before stealing: "<<p.first<<endl;
            v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));
            cout<<"key after stealing: "<<p.first<<endl;
        }
    }
    // now use v
    return 0;
}

Produz a saída:

key before stealing: aLongString
key after stealing: 
key before stealing: anotherLongString
key after stealing: 

EDIT: Eu gostaria de fazer isso para todo o conteúdo de um mapa grande e salvar alocações dinâmicas por esse roubo de recurso.


3
Qual é o propósito desse "roubo"? Para remover o elemento do mapa? Então, por que não fazer isso simplesmente (apagar o elemento do mapa)? Além disso, modificar um constvalor é sempre UB.
— Algum programador

aparentemente, isso levará a erros graves!
— rezaebrh 22/02

11
Não é uma resposta direta à sua pergunta, mas: e se você não retornar um vetor, mas um intervalo ou um par de iteradores? Isso evitaria copiar completamente. Em qualquer caso, você precisa de benchmarks para acompanhar o progresso da otimização e um criador de perfil para encontrar pontos de acesso.
— Ulrich Eckhardt

11
@ ALX23z Você tem alguma fonte para esta declaração. Não consigo imaginar como copiar um ponteiro é mais caro do que copiar uma região inteira da memória.
— Sebastian Hoffmann

11
@SebastianHoffmann, foi mencionado no CppCon recente, não tenho certeza sobre qual conversa, tho. A coisa é que std::stringtem otimização de cadeia curta. Isso significa que existe uma lógica não trivial em copiar e mover, e não apenas troca de ponteiros e, além disso, na maioria das vezes a mudança implica em copiar - para que você não lide com seqüências longas. De qualquer forma, a diferença estatística era pequena e, em geral, varia de acordo com o tipo de processamento de string.
— ALX23z 22/02

Respostas:


18

Você está fazendo um comportamento indefinido, usando const_castpara modificar uma constvariável. Não faça isso. A razão consté que os mapas são classificados por suas chaves. Portanto, modificar uma chave no local está quebrando a suposição subjacente na qual o mapa é construído.

Você nunca deve usar const_castpara remover constde uma variável e modificar essa variável.

Dito isto, o C ++ 17 tem a solução para o seu problema: std::mapa extractfunção:

#include <map>
#include <string>
#include <vector>
#include <utility>

int main() {
  std::vector<std::pair<std::string, double>> v;
  std::map<std::string, double> m{{"aLongString", 1.0},
                                  {"anotherLongString", 2.0}};

  auto extracted_value = m.extract("aLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));

  extracted_value = m.extract("anotherLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));
}

E não using namespace std;. :)


Obrigado, vou tentar isso! Mas você tem certeza de que não posso fazer o que fiz? Quero dizer map, não vou reclamar se eu não chamar seus métodos (o que não faço) e talvez a ordem interna não seja importante em seu destruidor?
— phinz 22/02

2
As chaves do mapa são criadas const. A mutação de um constobjeto é um UB instantâneo, independentemente de alguma coisa realmente ser acessada posteriormente.
— HTNW

Este método tem dois problemas: (1) Como desejo extrair todos os elementos, não quero extrair por chave (pesquisa ineficiente), mas por iterador. Eu vi que isso também é possível, então está tudo bem. (2) Corrija-me se estiver errado, mas para extrair todos os elementos haverá uma sobrecarga enorme (para reequilibrar a estrutura interna da árvore a cada extração)?
— phinz 22/02

2
@phinz Como você pode ver na cppreference extract ao usar iteradores, o argumento amortiza a complexidade constante. Algumas despesas gerais são inevitáveis, mas provavelmente não serão significativas o suficiente para importar. Se você tiver requisitos especiais não cobertos por isso, precisará implementar seu próprio mapatendimento a esses requisitos. Os stdcontêineres destinam-se a aplicações de uso geral comum e não são otimizados para casos de uso específicos.
— noz

@HTNW Tem certeza de que as chaves foram criadas const? Nesse caso, você pode indicar onde minha argumentação está errada.
— phinz 23/02

4

Seu código tenta modificar constobjetos, por isso tem um comportamento indefinido, como a resposta de druckermanly aponta corretamente.

Algumas outras respostas ( phinz e Deuchie's ) argumentam que a chave não deve ser armazenada como um constobjeto, porque o identificador do nó resultante da extração de nós fora do mapa permite o não constacesso à chave. Essa inferência pode parecer plausível a princípio, mas P0083R3 , o artigo que introduziu as extractfuncionalidades), possui uma seção dedicada sobre esse tópico que invalida esse argumento:

Preocupações

Várias preocupações foram levantadas sobre esse design. Vamos abordá-los aqui.

Comportamento indefinido

A parte mais difícil desta proposta, de uma perspectiva teórica, é o fato de o elemento extraído reter seu tipo de chave const. Isso evita sair dela ou alterá-la. Para resolver isso, fornecemos a função acessador de chave , que fornece acesso não constante à chave no elemento mantido pelo identificador do nó.Esta função requer implementação "mágica" para garantir que funcione corretamente na presença de otimizações do compilador. Uma maneira de fazer isso é com a união de pair<const key_type, mapped_type> e pair<key_type, mapped_type>. A conversão entre eles pode ser realizada com segurança usando uma técnica semelhante à usada std::launderna extração e reinserção.

Não sentimos que isso represente algum problema técnico ou filosófico. Uma das razões pela qual a Biblioteca Padrão existe é escrever código mágico e não portátil que o cliente não pode escrever em C ++ portátil (por exemplo,<atomic> , <typeinfo>, <type_traits>, etc.). Este é apenas outro exemplo. Tudo o que é exigido dos fornecedores de compiladores para implementar essa mágica é que eles não exploram comportamentos indefinidos em uniões para fins de otimização - e atualmente os compiladores já prometem isso (na medida em que isso está sendo aproveitado aqui).

Isso impõe uma restrição ao cliente que, se essas funções forem usadas, std::pairnão pode ser especializada de modo quepair<const key_type, mapped_type> ter um layout diferente pair<key_type, mapped_type>. Sentimos que a probabilidade de alguém realmente querer fazer isso é efetivamente zero e, na redação formal, restringimos qualquer especialização desses pares.

Observe que a chave função de membro é o único local em que esses truques são necessários e que nenhuma alteração nos contêineres ou pares é necessária.

(ênfase minha)


Na verdade, isso faz parte da resposta à pergunta original, mas só posso aceitar uma.
— phinz 23/02

0

Eu não acho que isso const_caste modificação levem a um comportamento indefinido neste caso, mas por favor, comente se essa argumentação está correta.

Esta resposta afirma que

Em outras palavras, você obtém UB se modificar um objeto const originalmente, e caso contrário não.

Portanto, a linha v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));na pergunta não leva ao UB se e somente se o stringobjeto p.firstnão foi criado como um objeto const. Agora observe que a referência sobreextract estados

A extração de um nó invalida os iteradores para o elemento extraído. Ponteiros e referências ao elemento extraído permanecem válidos, mas não podem ser usados ​​enquanto o elemento pertence a um identificador de nó: eles se tornam utilizáveis ​​se o elemento for inserido em um contêiner.

Portanto, se eu extracto node_handlecorrespondente a p, pcontinuar vivendo no seu local de armazenamento. Mas, após a extração, tenho permissão para moveafastar os recursos do pcódigo da resposta de druckermanly . Isso significa que pe, portanto, também o stringobjeto nãop.first foi criado como objeto const originalmente.

Portanto, acho que a modificação das mapchaves do não leva ao UB e, a partir da resposta de Deuchie , parece que também a estrutura de árvore agora corrompida (agora várias mesmas chaves de cadeia vazias) do mapnão introduz problemas no destruidor. Portanto, o código na pergunta deve funcionar bem, pelo menos no C ++ 17, onde o extractmétodo existe (e a declaração sobre ponteiros permanecendo válidos).

Atualizar

Agora sou da opinião de que esta resposta está errada. Não estou excluindo porque é referenciado por outras respostas.


11
Desculpe, phinz, sua resposta está errada. Vou escrever uma resposta para explicar isso - tem algo a ver com sindicatos e std::launder.
— LF

-1

EDIT: Esta resposta está errada. Comentários gentis apontaram os erros, mas não o excluo porque foi mencionado em outras respostas.

A @druckermanly respondeu à sua primeira pergunta, que dizia que alterar as chaves mappela força quebra a ordem em que mapa estrutura de dados interna é construída (árvore Vermelho-Preto). Mas é seguro usar o extractmétodo porque ele faz duas coisas: mova a chave para fora do mapa e exclua-a, para que não afete a ordem do mapa.

A outra pergunta que você fez, sobre se causaria problemas ao desconstruir, não é um problema. Quando um mapa desconstrói, ele chama o desconstrutor de cada um de seus elementos (mapped_types etc.), e o movemétodo garante que seja seguro desconstruir uma classe depois que ela for movida. Então não se preocupe. Em poucas palavras, é a operação moveque garante que é seguro excluir ou reatribuir algum novo valor à classe "movida". Especificamente para string, o movemétodo pode definir seu ponteiro de char para nullptr, para não excluir os dados reais que foram movidos quando o desconstrutor da classe original foi chamado.


Um comentário me lembrou o ponto que eu estava ignorando, basicamente ele estava certo, mas há uma coisa que eu não concordo totalmente: const_castprovavelmente não é um UB. consté apenas uma promessa entre o compilador e nós. objetos anotados como constainda são um objeto, igual aos que não o são const, em termos de seus tipos e representações na forma binária. Quando o consté descartado, ele deve se comportar como se fosse uma classe mutável normal. No que diz respeito a move, se você quiser usá-lo, precisará passar um em &vez de um const &, então, como vejo que não é um UB, ele simplesmente quebra a promessa deconst e afasta os dados.

Também fiz dois experimentos, usando o MSVC 14.24.28314 e o Clang 9.0.0, respectivamente, e eles produziram o mesmo resultado.

map<string, int> m;
m.insert({ "test", 2 });
m.insert({ "this should be behind the 'test' string.", 3 });
m.insert({ "and this should be in front of the 'test' string.", 1 });
string let_me_USE_IT = std::move(const_cast<string&>(m.find("test")->first));
cout << let_me_USE_IT << '\n';
for (auto const& i : m) {
    cout << i.first << ' ' << i.second << '\n';
}

resultado:

test
and this should be in front of the 'test' string. 1
 2
this should be behind the 'test' string. 3

Podemos ver que a string '2' está vazia agora, mas obviamente quebramos a ordem do mapa porque a string vazia deve ser recolocada na frente. Se tentarmos inserir, localizar ou excluir alguns nós específicos do mapa, isso pode causar uma catástrofe.

De qualquer forma, podemos concordar que nunca é uma boa idéia manipular os dados internos de qualquer classe ignorando suas interfaces públicas. A find, insert, removefunções e assim por diante confiar sua correção na ordem da estrutura de dados interna, e essa é a razão por que devemos ficar longe do pensamento de espreitar para dentro.


2
"saber se causaria problemas ao desconstruir não é um problema." Tecnicamente correto, pois o comportamento indefinido (alteração de um constvalor) ocorreu anteriormente. No entanto, o argumento "a move[função] garante que seja seguro desconstruir [um objeto de] uma classe depois que ela foi movida" não é válido: você não pode mover-se com segurança de um constobjeto / referência, pois isso requer modificação, o que requer modificações. constimpede. Você pode tentar contornar essa limitação usando const_cast, mas nesse momento você está, na melhor das hipóteses, entrando em um comportamento específico de implementação profunda, se não UB.
— hoffmale 23/02

@hoffmale Obrigado, esqueci e cometi um grande erro. Se não foi você quem apontou, minha resposta aqui pode enganar outra pessoa. Na verdade, devo dizer que a movefunção aceita um em &vez de um const&, portanto, se alguém insistir em que ele move uma chave de um mapa, ele deve usar const_cast.
— Deuchie 23/02

11
"objetos anotados como const ainda são um objeto, igual aos que não estão com const, em termos de seus tipos e representações na forma binária" Não. Objetos const podem ser colocados na memória somente leitura. Além disso, const permite que o compilador raciocine sobre o código e armazene em cache o valor em vez de gerar código para várias leituras (o que pode fazer uma grande diferença em termos de desempenho). Portanto, o UB causado por const_castserá desagradável. Pode funcionar na maioria das vezes, mas quebre seu código de maneiras sutis.
— LF

Mas aqui eu acho que podemos ter certeza de que os objetos não são colocados na memória somente leitura porque extract, depois , podemos nos mover do mesmo objeto, certo? (veja minha resposta)
— phinz 23/02

11
Desculpe, a análise na sua resposta está errada. Vou escrever uma resposta para explicar isso - tem algo a ver com sindicatos estd::launder
— LF
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.