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.

phinz
fonte
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;. :)

druckermanly
fonte
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)

LF
fonte
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.

phinz
fonte
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.

Deuchie
fonte
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