Rubare risorse dalle chiavi di std :: map è permesso?


15

In C ++, va bene rubare risorse da una mappa che non mi serve più in seguito? Più precisamente, supponiamo che io abbia una std::mapcon le std::stringchiavi e che voglio costruirne un vettore rubando le risorse delle mapchiavi s usando std::move. Si noti che tale accesso in scrittura alle chiavi corrompe la struttura di dati interna (ordinamento delle chiavi) del mapma non lo userò in seguito.

Domanda : posso farlo senza problemi o questo porterà a bug imprevisti, ad esempio nel distruttore del mapperché ho std::mapavuto accesso in un modo che non era previsto?

Ecco un esempio di programma:

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

Produce l'output:

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

EDIT: vorrei fare questo per l'intero contenuto di una grande mappa e salvare allocazioni dinamiche da questo furto di risorse.


3
Qual è lo scopo di questo "furto"? Per rimuovere l'elemento dalla mappa? Allora perché non farlo semplicemente (cancella l'elemento dalla mappa)? Inoltre, la modifica di un constvalore è sempre UB.
Qualche programmatore, amico, il

apparentemente porterà a gravi bug!
Rezaebrh

1
Non una risposta diretta alla tua domanda, ma: cosa succede se non si restituisce un vettore ma un intervallo o una coppia di iteratori? Ciò eviterebbe di copiare completamente. In ogni caso, sono necessari parametri di riferimento per monitorare l'avanzamento dell'ottimizzazione e un profiler per trovare gli hotspot.
Ulrich Eckhardt,

1
@ ALX23z Hai qualche fonte per questa affermazione. Non riesco a immaginare quanto sia più costoso copiare un puntatore che copiare un'intera regione di memoria.
Sebastian Hoffmann,

1
@SebastianHoffmann è stato menzionato nel recente CppCon non sono sicuro su quale discorso, comunque. Il fatto è std::stringche l'ottimizzazione delle stringhe corte. Ciò significa che esiste una logica non banale nella copia e nello spostamento e non solo nello scambio di puntatori e inoltre la maggior parte delle volte lo spostamento implica la copia - per non avere a che fare con stringhe piuttosto lunghe. La differenza statistica era comunque piccola e in generale varia sicuramente a seconda del tipo di elaborazione della stringa eseguita.
ALX23z,

Risposte:


18

Stai facendo un comportamento indefinito, usando const_castper modificare una constvariabile. Non farlo. Il motivo è che le constmappe sono ordinate in base alle loro chiavi. Quindi la modifica di una chiave sul posto sta rompendo il presupposto sottostante su cui è costruita la mappa.

Non si dovrebbe mai usare const_castper rimuovere constda una variabile e modificarla.

Detto questo, C ++ 17 ha la soluzione al tuo problema: std::mapla extractfunzione:

#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 non farlo using namespace std;. :)


Grazie, ci proverò! Ma sei sicuro che non posso fare come ho fatto io? Voglio dire map, non mi lamento se non chiamo i suoi metodi (cosa che non faccio) e forse l'ordinamento interno non è importante nel suo distruttore?
phinz,

2
Vengono create le chiavi della mappa const. La mutazione di un constoggetto è un UB istantaneo, indipendentemente dal fatto che qualcosa gli acceda successivamente o meno.
HTNW,

Questo metodo ha due problemi: (1) Poiché desidero estrarre tutti gli elementi, non voglio estrarre per chiave (ricerca inefficiente) ma per iteratore. Ho visto che anche questo è possibile, quindi va bene. (2) Correggimi se sbaglio ma per estrarre tutti gli elementi ci sarà un enorme sovraccarico (per riequilibrare la struttura interna dell'albero ad ogni estrazione)?
phinz,

2
@phinz Come puoi vedere su cppreference extract quando si usano iteratori come argomento ha ammortizzato la complessità costante. Un certo sovraccarico è inevitabile, ma probabilmente non sarà abbastanza significativo da importare. Se hai requisiti speciali non coperti da questo, dovrai implementare il tuo mapsoddisfacendo questi requisiti. I stdcontenitori sono pensati per un'applicazione di uso generale comune e non sono ottimizzati per casi d'uso specifici.
noce,

@HTNW Sei sicuro che le chiavi siano state create const? In questo caso puoi indicare dove sono sbagliate le mie argomentazioni .
phinz,

4

Il tuo codice tenta di modificare gli constoggetti, quindi ha un comportamento indefinito, come sottolinea correttamente la risposta di druckermanly .

Alcune altre risposte ( phinz e Deuchie ) sostengono che la chiave non deve essere memorizzata come constoggetto perché l'handle del nodo derivante dall'estrazione di nodi dalla mappa consente il non constaccesso alla chiave. Questa inferenza può sembrare inizialmente plausibile, ma P0083R3 , il documento che ha introdotto le extractfunzionalità), ha una sezione dedicata su questo argomento che invalida questo argomento:

preoccupazioni

Diverse preoccupazioni sono state sollevate su questo progetto. Li affronteremo qui.

Comportamento indefinito

La parte più difficile di questa proposta da una prospettiva teorica è il fatto che l'elemento estratto mantiene il suo tipo di chiave const. Questo impedisce di uscirne o cambiarlo. Per risolvere questo, abbiamo fornito la chiave funzione di accesso, che fornisce un accesso non const alla chiave nell'elemento tenuto dalla maniglia nodo. Questa funzione richiede un'implementazione "magica" per garantire che funzioni correttamente in presenza di ottimizzazioni del compilatore. Un modo per farlo è con un'unione di pair<const key_type, mapped_type> e pair<key_type, mapped_type>. La conversione tra questi può essere effettuata in modo sicuro usando una tecnica simile a quella usata per std::launderl'estrazione e il reinserimento.

Non riteniamo che ciò ponga alcun problema tecnico o filosofico. Uno dei motivi della libreria standard esiste è di scrivere codice non portabile e magico che il cliente non può scrivere nel portatile C ++ (per esempio <atomic>, <typeinfo>, <type_traits>, etc.). Questo è solo un altro esempio. Tutto ciò che serve ai venditori di compilatori per implementare questa magia è che non sfruttano comportamenti indefiniti nei sindacati per scopi di ottimizzazione - e attualmente i compilatori lo promettono già (nella misura in cui viene sfruttato qui).

Ciò impone una restrizione al client che, se si utilizzano queste funzioni, std::pairnon può essere specializzato in modo tale da pair<const key_type, mapped_type>avere un layout diverso da quello pair<key_type, mapped_type>. Riteniamo che la probabilità che qualcuno voglia davvero farlo sia effettivamente zero e nella formulazione formale limitiamo qualsiasi specializzazione di queste coppie.

Si noti che la funzione membro chiave è l'unico posto in cui tali trucchi sono necessari e che non sono necessarie modifiche ai contenitori o alla coppia.

(enfatizzare il mio)


Questa è in realtà una parte della risposta alla domanda originale, ma posso accettarne solo una.
phinz,

0

Non penso che la const_castmodifica porti a comportamenti indefiniti in questo caso, ma per favore commenta se questa argomentazione è corretta.

Questa risposta afferma che

In altre parole, ottieni UB se modifichi un oggetto const originariamente, e altrimenti no.

Quindi la riga v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));nella domanda non porta a UB se e solo se l' stringoggetto p.firstnon è stato creato come oggetto const. Ora nota che il riferimento sugliextract stati

L'estrazione di un nodo invalida gli iteratori sull'elemento estratto. I puntatori e i riferimenti all'elemento estratto rimangono validi, ma non possono essere utilizzati mentre l'elemento è di proprietà di un handle di nodo: diventano utilizzabili se l'elemento viene inserito in un contenitore.

Quindi, se io extractil node_handlecorrispondente a p, pcontinua a vivere nella sua posizione di archiviazione. Ma dopo l'estrazione, mi è permesso di moveeliminare le risorse di pcome nel codice della risposta di druckermanly . Ciò significa che pe quindi anche l' stringoggetto nonp.first è stato creato come oggetto const in origine.

Pertanto, penso che la modifica delle mapchiavi dell '' '' 'non porti a UB e dalla risposta di Deuchie , sembra che anche la struttura ad albero ora corrotta (ora più stesse chiavi di stringa vuote) mapnon introduca problemi nel distruttore. Quindi il codice nella domanda dovrebbe funzionare bene almeno in C ++ 17 dove extractesiste il metodo (e l'istruzione sui puntatori che rimangono validi è valida).

Aggiornare

Ora sono del parere che questa risposta sia sbagliata. Non lo sto eliminando perché è indicato da altre risposte.


1
Siamo spiacenti, phinz, la tua risposta è sbagliata. Scriverò una risposta per spiegare questo - ha qualcosa a che fare con i sindacati e std::launder.
LF

-1

EDIT: questa risposta è sbagliata. I commenti gentili hanno sottolineato gli errori, ma non lo sto eliminando perché è stato menzionato in altre risposte.

@druckermanly ha risposto alla tua prima domanda, affermando che la modifica mapforzata delle chiavi interrompe l'ordine su cui mapè costruita la struttura dei dati interna (albero rosso-nero). Ma è sicuro usare il extractmetodo perché fa due cose: spostare la chiave fuori dalla mappa e quindi eliminarla, quindi non influisce affatto sull'ordine della mappa.

L'altra domanda che hai posto, sul fatto che possa causare problemi durante la decostruzione, non è un problema. Quando una mappa si decostruisce, chiamerà il decostruttore di ciascuno dei suoi elementi (mapped_types ecc.) E il movemetodo garantisce che sia sicuro decostruire una classe dopo che è stata spostata. Quindi non preoccuparti. In poche parole, è l'operazione moveche assicura che sia sicuro eliminare o riassegnare un nuovo valore alla classe "spostata". In particolare per string, il movemetodo può impostare il puntatore carattere su nullptr, quindi non elimina i dati effettivi che sono stati spostati quando è stato chiamato il decostruttore della classe originale.


Un commento mi ha ricordato il punto che stavo trascurando, fondamentalmente aveva ragione, ma c'è una cosa che non sono totalmente d'accordo: const_castprobabilmente non è un UB. constè solo una promessa tra il compilatore e noi. gli oggetti annotati come constsono ancora un oggetto, uguale a quelli che non hanno const, in termini di tipi e rappresentazioni in forma binaria. Quando constviene eliminato, dovrebbe comportarsi come se fosse una normale classe mutabile. Per quanto riguarda move, Se vuoi usarlo, devi passare un &invece di un const &, quindi come vedo che non è un UB, semplicemente interrompe la promessa conste sposta i dati.

Ho anche fatto due esperimenti, usando MSVC 14.24.28314 e Clang 9.0.0 rispettivamente, e hanno prodotto lo stesso risultato.

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

produzione:

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

Ora possiamo vedere che la stringa '2' è vuota, ma ovviamente abbiamo rotto l'ordine della mappa perché la stringa vuota dovrebbe essere ricollocata in primo piano. Se proviamo a inserire, trovare o eliminare alcuni nodi specifici della mappa, potrebbe causare una catastrofe.

Ad ogni modo, possiamo concordare sul fatto che non è mai una buona idea manipolare i dati interni di qualsiasi classe aggirando le loro interfacce pubbliche. La find, insert, removefunzioni e così via affidano loro correttezza sulla regolarità della struttura dati interna, e questo è il motivo per cui dovremmo stare lontano dal pensiero di sbirciare all'interno.


2
"se possa causare problemi durante la decostruzione, non è un problema". Tecnicamente corretto, poiché il comportamento indefinito (modifica di un constvalore) si è verificato in precedenza. Tuttavia, l'argomento "la move[funzione] garantisce che sia sicuro decostruire [un oggetto di] una classe dopo che è stata spostata" non regge: non è possibile spostarsi in modo sicuro da un constoggetto / riferimento, poiché ciò richiede una modifica, che constimpedisce. Puoi provare a ovviare a questa limitazione usando const_cast, ma a quel punto, nella migliore delle ipotesi, entrerai profondamente nel comportamento specifico dell'implementazione, se non UB.
hoffmale

@hoffmale Grazie, ho trascurato e fatto un grosso errore. Se non sei stato tu a segnalarlo, la mia risposta qui potrebbe fuorviare qualcun altro. In realtà dovrei dire che la movefunzione prende un &invece di un const&, quindi se uno insiste sul fatto che sposta una chiave fuori da una mappa, deve usare const_cast.
Deuchie,

1
"Gli oggetti indicati come const sono ancora oggetti, uguali a quelli che non hanno const, in termini di tipi e rappresentazioni in forma binaria" No. Gli oggetti const possono essere messi in sola lettura. Inoltre, const consente al compilatore di ragionare sul codice e memorizzare nella cache il valore invece di generare codice per letture multiple (il che può fare una grande differenza in termini di prestazioni). Quindi l'UB causato da const_castsarà odioso. Potrebbe funzionare la maggior parte delle volte, ma spezzare il codice in modi sottili.
LF

Ma qui penso che possiamo essere sicuri che gli oggetti non vengono messi in sola lettura nella memoria perché dopo extract, ci è permesso di spostarci dallo stesso oggetto, giusto? (vedi la mia risposta)
phinz

1
Spiacenti, l'analisi nella tua risposta è errata. Scriverò una risposta per spiegare questo - ha qualcosa a che fare con i sindacati estd::launder
LF
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.