Voler des ressources des clés de std :: map autorisé?

15

En C ++, est-ce OK de voler des ressources d'une carte dont je n'ai plus besoin par la suite? Plus précisément, supposons que j'ai un std::mapavec des std::stringclés et que je veux en construire un vecteur en volant les ressources des mapclés s en utilisant std::move. Notez qu'un tel accès en écriture aux clés corrompt la structure de données interne (ordre des clés) du mapmais je ne l'utiliserai pas par la suite.

Question : Puis-je le faire sans aucun problème ou cela entraînera-t-il des bogues inattendus, par exemple dans le destructeur du, mapcar j'y ai accédé d'une manière qui std::mapn'était pas destinée?

Voici un exemple de programme:

#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;
}

Il produit la sortie:

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

EDIT: Je voudrais le faire pour tout le contenu d'une grande carte et enregistrer les allocations dynamiques par ce vol de ressources.

Phinz
la source
3
Quel est le but de ce "vol"? Pour supprimer l'élément de la carte? Alors pourquoi ne pas simplement faire cela (effacer l'élément de la carte)? De plus, la modification d'une constvaleur est toujours UB.
Un programmeur mec le
apparemment, cela entraînera de graves bugs!
rezaebrh
1
Pas une réponse directe à votre question, mais: que se passe-t-il si vous ne renvoyez pas un vecteur mais une plage ou une paire d'itérateurs? Cela éviterait de copier complètement. Dans tous les cas, vous avez besoin de repères pour suivre la progression de l'optimisation et d'un profileur pour trouver des points d'accès.
Ulrich Eckhardt
1
@ ALX23z Avez-vous une source pour cette déclaration. Je ne peux pas imaginer à quel point la copie d'un pointeur coûte plus cher que la copie d'une région entière de la mémoire.
Sebastian Hoffmann
1
@SebastianHoffmann, il a été mentionné sur la récente CppCon, ne savez pas sur quel discours, tho. Le truc, c'est l' std::stringoptimisation des chaînes courtes. Cela signifie qu'il existe une logique non triviale sur la copie et le déplacement et pas seulement l'échange de pointeurs et, en outre, la plupart du temps, le déplacement implique la copie - de peur de traiter avec des chaînes assez longues. La différence statistique était de toute façon faible et en général, elle varie sûrement en fonction du type de traitement de chaîne effectué.
ALX23z

Réponses:

18

Vous faites un comportement indéfini, en utilisant const_castpour modifier une constvariable. Ne fais pas ça. La raison en constest que les cartes sont triées par leurs clés. Ainsi, la modification d'une clé en place rompt l'hypothèse sous-jacente sur laquelle la carte est construite.

Vous ne devez jamais utiliser const_castpour supprimer constd'une variable et modifier cette variable.

Cela étant dit, C ++ 17 a la solution à votre problème: std::mapla extractfonction de:

#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())));
}

Et non using namespace std;. :)

druckermanly
la source
Merci, je vais essayer ça! Mais êtes-vous sûr que je ne peux pas faire comme moi? Je veux dire que je mapne me plaindrai pas si je n'appelle pas ses méthodes (ce que je ne fais pas) et peut-être que l'ordre interne n'est pas important dans son destructeur?
phinz
2
Les clés de la carte sont créées const. La mutation d'un constobjet est une UB instantanée, que quelque chose y accède ou non par la suite.
HTNW
Cette méthode a deux problèmes: (1) Comme je veux extraire tous les éléments, je ne veux pas extraire par clé (recherche inefficace) mais par itérateur. J'ai vu que c'est aussi possible, donc ça va. (2) Corrigez-moi si je me trompe, mais pour l'extraction de tous les éléments, il y aura un énorme surcoût (pour rééquilibrer la structure arborescente interne à chaque extraction)?
phinz
2
@phinz Comme vous pouvez le voir sur cppreference extract lorsque vous utilisez des itérateurs comme argument a amorti la complexité constante. Certains frais généraux sont inévitables, mais ils ne seront probablement pas suffisamment importants pour être importants. Si vous avez des exigences spéciales qui ne sont pas couvertes par cela, alors vous devrez implémenter la vôtre en mapsatisfaisant à ces exigences. Les stdconteneurs sont destinés à une utilisation générale commune et ne sont pas optimisés pour des cas d'utilisation spécifiques.
noyer
@HTNW Êtes-vous sûr que les clés sont créées const? Dans ce cas, pouvez-vous s'il vous plaît indiquer où mon argumentation est fausse.
phinz
4

Votre code tente de modifier des constobjets, il a donc un comportement indéfini, comme le souligne correctement la réponse de druckermanly .

Certaines autres réponses ( celles de Phinz et Deuchie ) soutiennent que la clé ne doit pas être stockée en tant constqu'objet car la poignée de nœud résultant de l'extraction de nœuds hors de la carte permet le non- constaccès à la clé. Cette inférence peut sembler plausible au premier abord, mais P0083R3 , l'article qui a introduit les extractfonctionnalités), a une section dédiée sur ce sujet qui invalide cet argument:

Préoccupations

Plusieurs préoccupations ont été exprimées à propos de cette conception. Nous les aborderons ici.

Comportement indéfini

La partie la plus difficile de cette proposition d'un point de vue théorique est le fait que l'élément extrait conserve son type de clé const. Cela évite d'en sortir ou de le changer. Pour résoudre ce problème, nous avons fourni la fonction d'accesseur de clé , qui fournit un accès non constant à la clé dans l'élément détenu par le handle de nœud. Cette fonction nécessite une implémentation "magique" pour s'assurer qu'elle fonctionne correctement en présence des optimisations du compilateur. Une façon de procéder consiste à réunir l'union de pair<const key_type, mapped_type> et pair<key_type, mapped_type>. La conversion entre ceux-ci peut être effectuée en toute sécurité en utilisant une technique similaire à celle utilisée par l' std::launderextraction et la réinsertion.

Nous pensons que cela ne pose aucun problème technique ou philosophique. L' une des raisons pour lesquelles la bibliothèque standard existe est d'écrire du code non portable et magique que le client ne peut pas écrire en C ++ portable (par exemple <atomic>, <typeinfo>, <type_traits>, etc.). Ceci est juste un autre exemple de ce genre. Tout ce qui est exigé des éditeurs de compilateurs pour implémenter cette magie est qu'ils n'exploitent pas un comportement indéfini dans les unions à des fins d'optimisation - et actuellement les compilateurs le promettent déjà (dans la mesure où il est utilisé ici).

Cela impose une restriction au client qui, si ces fonctions sont utilisées, std::pairne peut pas être spécialisé tel qu'il pair<const key_type, mapped_type>a une disposition différente de celle pair<key_type, mapped_type>. Nous pensons que la probabilité que quiconque veuille réellement le faire est effectivement nulle, et dans le libellé formel, nous restreignons toute spécialisation de ces paires.

Notez que la fonction de membre clé est le seul endroit où de telles astuces sont nécessaires et qu'aucune modification des conteneurs ou de la paire n'est requise.

(c'est moi qui souligne)

LF
la source
C'est en fait une partie de la réponse à la question d'origine mais je ne peux en accepter qu'une.
phinz
0

Je ne pense pas que const_castet la modification conduisent à un comportement indéfini dans ce cas, mais veuillez commenter si cette argumentation est correcte.

Cette réponse affirme que

En d'autres termes, vous obtenez UB si vous modifiez un objet const à l'origine, et sinon non.

Ainsi, la ligne v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));de la question ne mène pas à UB si et seulement si l' stringobjet p.firstn'a pas été créé en tant qu'objet const. Notez maintenant que la référence sur lesextract états

L'extraction d'un nœud invalide les itérateurs de l'élément extrait. Les pointeurs et les références à l'élément extrait restent valides, mais ne peuvent pas être utilisés tant que l'élément appartient à un handle de nœud: ils deviennent utilisables si l'élément est inséré dans un conteneur.

Donc, si je extractle node_handlecorrespondant à p, pcontinue de vivre à son emplacement de stockage. Mais après extraction, je suis autorisé à moveéloigner les ressources de pcomme dans le code de la réponse de druckermanly . Cela signifie que pet donc aussi l' stringobjet p.firstn'a pas été créé comme objet const à l'origine.

Par conséquent, je pense que la modification des mapclés de ne conduit pas à UB et de la réponse de Deuchie , il semble que la structure arborescente maintenant corrompue (maintenant plusieurs mêmes clés de chaîne vides) du mapn'introduise pas de problèmes dans le destructeur. Ainsi, le code de la question devrait fonctionner correctement au moins en C ++ 17 où la extractméthode existe (et l'instruction sur les pointeurs restant valides).

Mise à jour

Je suis maintenant d'avis que cette réponse est erronée. Je ne le supprime pas car il est référencé par d'autres réponses.

Phinz
la source
1
Désolé, Phinz, votre réponse est fausse. Je vais écrire une réponse pour expliquer cela - cela a quelque chose à voir avec les syndicats et std::launder.
LF
-1

EDIT: Cette réponse est fausse. Des commentaires aimables ont souligné les erreurs, mais je ne les supprime pas car elles ont été référencées dans d'autres réponses.

@druckermanly a répondu à votre première question, qui disait que la modification mapforcée des clés rompt l'ordre sur lequel mapla structure de données interne est construite (arbre rouge-noir). Mais il est sûr d'utiliser la extractméthode car elle fait deux choses: déplacer la clé hors de la carte, puis la supprimer, afin qu'elle n'affecte pas du tout l'ordre de la carte.

L'autre question que vous avez posée, à savoir si cela causerait des problèmes lors de la déconstruction, n'est pas un problème. Lorsqu'une carte déconstruit, elle appelle le déconstructeur de chacun de ses éléments (mapped_types, etc.), et la moveméthode garantit qu'il est sûr de déconstruire une classe après qu'elle a été déplacée. Alors ne t'inquiète pas. En bref, c'est l'opération movequi garantit qu'il est sûr de supprimer ou de réaffecter une nouvelle valeur à la classe "déplacée". Plus précisément pour string, la moveméthode peut définir son pointeur char sur nullptr, afin de ne pas supprimer les données réelles qui ont été déplacées lors de l'appel du déconstructeur de la classe d'origine.


Un commentaire m'a rappelé le point que je négligeais, en gros il avait raison mais il y a une chose que je ne suis pas totalement d'accord: ce const_castn'est probablement pas un UB. constest juste une promesse entre le compilateur et nous. les objets notés comme constétant toujours un objet, identiques à ceux qui n'en ont pas const, en termes de types et de représentations sous forme binaire. Lorsque le constest rejeté, il doit se comporter comme s'il s'agissait d'une classe mutable normale. En ce qui concerne move, si vous voulez l'utiliser, vous devez passer un &au lieu d'un const &, donc comme je vois que ce n'est pas un UB, cela rompt simplement la promesse de constet éloigne les données.

J'ai également fait deux expériences, en utilisant MSVC 14.24.28314 et Clang 9.0.0 respectivement, et elles ont donné le même résultat.

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';
}

production:

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

Nous pouvons voir que la chaîne '2' est vide maintenant, mais nous avons évidemment rompu l'ordre de la carte car la chaîne vide devrait être repositionnée à l'avant. Si nous essayons d'insérer, de trouver ou de supprimer certains nœuds spécifiques de la carte, cela peut provoquer une catastrophe.

Quoi qu'il en soit, nous pouvons convenir que ce n'est jamais une bonne idée de manipuler les données internes des classes en contournant leurs interfaces publiques. Le find, insert, les removefonctions et ainsi de suite dépendent de leur exactitude sur la recevabilité de la structure de données interne, et qui est la raison pour laquelle nous devons rester loin de la pensée de jeter un oeil à l' intérieur.

Deuchie
la source
2
"quant à savoir si cela causerait des problèmes lors de la déconstruction, n'est pas un problème." Techniquement correct, car le comportement indéfini (modification d'une constvaleur) s'est produit plus tôt. Cependant, l'argumentmove [fonction] garantit qu'il est sûr de déconstruire [un objet] d'une classe après qu'elle a été déplacée" ne tient pas: vous ne pouvez pas vous déplacer en toute sécurité à partir d'un constobjet / référence, car cela nécessite une modification, ce qui constempêche. Vous pouvez essayer de contourner cette limitation en utilisant const_cast, mais à ce stade, vous allez au mieux entrer dans un comportement spécifique à l'implémentation en profondeur, sinon UB.
hoffmale
@hoffmale Merci, j'ai oublié et j'ai fait une grosse erreur. Si ce n'est pas vous qui l'avez signalé, ma réponse ici pourrait induire quelqu'un en erreur. En fait, je devrais dire que la movefonction prend un &au lieu d'un const&, donc si l'on insiste pour qu'il déplace une clé d'une carte, il doit l'utiliser const_cast.
Deuchie
1
"Les objets notés const sont toujours un objet, comme ceux qui ne le sont pas avec const, en termes de types et de représentations sous forme binaire." Les objets const ne peuvent pas être mis en mémoire morte. En outre, const permet au compilateur de raisonner sur le code et de mettre en cache la valeur au lieu de générer du code pour plusieurs lectures (ce qui peut faire une grande différence en termes de performances). L'UB causée par const_castva donc être désagréable. Cela peut fonctionner la plupart du temps, mais cassez votre code de manière subtile.
LF
Mais ici, je pense que nous pouvons être sûrs que les objets ne sont pas mis en mémoire morte car après extract, nous sommes autorisés à nous déplacer du même objet, non? (voir ma réponse)
phinz
1
Désolé, l'analyse dans votre réponse est fausse. Je vais écrire une réponse pour expliquer cela - cela a quelque chose à voir avec les syndicats etstd::launder
LF