En C ++, ¿está bien robar recursos de un mapa que ya no necesito? Más precisamente, suponga que tengo un std::map
con std::string
claves y quiero construir un vector a partir de él robando los recursos de las map
teclas s usando std::move
. Tenga en cuenta que dicho acceso de escritura a las claves corrompe la estructura de datos interna (orden de las claves) del map
pero no la usaré después.
Pregunta : ¿Puedo hacer esto sin ningún problema o esto conducirá a errores inesperados, por ejemplo, en el destructor del map
porque lo accedí de una manera que std::map
no estaba destinada?
Aquí hay un programa de ejemplo:
#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 la salida:
key before stealing: aLongString
key after stealing:
key before stealing: anotherLongString
key after stealing:
EDITAR: Me gustaría hacer esto para todo el contenido de un mapa grande y guardar asignaciones dinámicas mediante este robo de recursos.
fuente
const
valor siempre es UB.std::string
tiene una optimización de cadena corta. Lo que significa que existe una lógica no trivial en la copia y el movimiento y no solo el intercambio de punteros y, además, la mayoría de las veces el movimiento implica la copia, para que no trabaje con cadenas bastante largas. La diferencia estadística fue pequeña de todos modos y, en general, seguramente varía según el tipo de procesamiento de cadena que se realice.Respuestas:
Estás haciendo un comportamiento indefinido, usando
const_cast
para modificar unaconst
variable. No hagas eso. La razónconst
es porque los mapas están ordenados por sus claves. Por lo tanto, modificar una clave en el lugar está rompiendo la suposición subyacente en la que se basa el mapa.Nunca debe usar
const_cast
para eliminarconst
de una variable y modificar esa variable.Dicho esto, C ++ 17 tiene la solución para su problema:
std::map
laextract
función de:Y no lo hagas
using namespace std;
. :)fuente
map
que no se quejarán si no llamo a sus métodos (que no hago) y tal vez el orden interno no es importante en su destructor.const
. La mutación de unconst
objeto es UB instantáneo, ya sea que algo realmente acceda a él o no después.extract
cuando se usan iteradores como argumento, se ha amortizado la complejidad constante. Algunos gastos generales son inevitables, pero probablemente no serán lo suficientemente importantes como para importar. Si tiene requisitos especiales que no están cubiertos por esto, deberá implementar sus propiosmap
requisitos. Losstd
contenedores están destinados a una aplicación común de uso general. No están optimizados para casos de uso específicos.const
? En este caso, ¿puede indicar dónde está mal mi argumentación ?Su código intenta modificar
const
objetos, por lo que tiene un comportamiento indefinido, como señala correctamente la respuesta de druckermanly .Algunas otras respuestas (de Phinz y Deuchie ) argumentan que la clave no debe almacenarse como un
const
objeto porque el identificador de nodo resultante de la extracción de nodos fuera del mapa permite el noconst
acceso a la clave. Esta inferencia puede parecer plausible al principio, pero P0083R3 , el documento que introdujo lasextract
funcionalidades), tiene una sección dedicada sobre este tema que invalida este argumento:(énfasis mío)
fuente
No creo que eso
const_cast
y la modificación conduzcan a un comportamiento indefinido en este caso, pero comente si esta argumentación es correcta.Esta respuesta afirma que
Entonces, la línea
v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));
en la pregunta no conduce a UB si y solo si elstring
objetop.first
no se creó como un objeto constante. Ahora tenga en cuenta que la referencia sobre losextract
estadosEntonces, si
extract
elnode_handle
correspondiente ap
,p
continúa viviendo en su ubicación de almacenamiento. Pero después de la extracción, se me permitemove
eliminar los recursosp
como en el código de la respuesta de druckermanly . Esto significa quep
y por lo tanto también elstring
objetop.first
fue no creó como objeto const originalmente.Por lo tanto, creo que la modificación de las
map
claves de 's no conduce a UB y, por la respuesta de Deuchie , parece que también la estructura de árbol ahora corrupta (ahora múltiples claves de cadena vacías)map
no introduce problemas en el destructor. Por lo tanto, el código en la pregunta debería funcionar bien al menos en C ++ 17 dondeextract
existe el método (y la declaración sobre los punteros que siguen siendo válidos).Actualizar
Ahora soy de la opinión de que esta respuesta es incorrecta. No lo estoy eliminando porque está referenciado por otras respuestas.
fuente
std::launder
.EDITAR: Esta respuesta es incorrecta. Los comentarios amables han señalado los errores, pero no los estoy eliminando porque se ha mencionado en otras respuestas.
@druckermanly respondió a su primera pregunta, que decía que cambiar las claves
map
por la fuerza rompe el orden en el quemap
se construye la estructura interna de datos (árbol Rojo-Negro). Pero es seguro usar elextract
método porque hace dos cosas: mover la llave fuera del mapa y luego eliminarlo, para que no afecte el orden del mapa en absoluto.La otra pregunta que planteó, en cuanto a si causaría problemas al deconstruir, no es un problema. Cuando un mapa se deconstruye, llamará al deconstructor de cada uno de sus elementos (mapped_types, etc.), y el
move
método garantiza que sea seguro deconstruir una clase después de que se haya movido. Entonces no te preocupes. En pocas palabras, es la operación de lomove
que garantiza que sea seguro eliminar o reasignar algún valor nuevo a la clase "movida". Específicamente parastring
, elmove
método puede establecer su puntero de caracteres ennullptr
, por lo que no elimina los datos reales que se movieron cuando se llamó al deconstructor de la clase original.Un comentario me recordó el punto que estaba pasando por alto, básicamente tenía razón, pero hay una cosa que no estoy totalmente de acuerdo:
const_cast
probablemente no sea un UB.const
es solo una promesa entre el compilador y nosotros. Los objetos señalados comoconst
todavía son un objeto, igual que los que no lo sonconst
, en términos de sus tipos y representaciones en forma binaria. Cuandoconst
se descarta, debe comportarse como si fuera una clase mutable normal. Con respecto amove
, si desea usarlo, debe pasar un en&
lugar de unconst &
, por lo que, como veo, no es un UB, simplemente rompe la promesaconst
y aleja los datos.También hice dos experimentos, usando MSVC 14.24.28314 y Clang 9.0.0 respectivamente, y arrojaron el mismo resultado.
salida:
Podemos ver que la cadena '2' está vacía ahora, pero obviamente hemos roto el orden del mapa porque la cadena vacía debe reubicarse en el frente. Si intentamos insertar, encontrar o eliminar algunos nodos específicos del mapa, puede causar una catástrofe.
De todos modos, podemos estar de acuerdo en que nunca es una buena idea manipular los datos internos de ninguna clase sin pasar por sus interfaces públicas. El
find
,insert
,remove
funciones, etc. confían su corrección en el orden de la estructura de datos interna, y esa es la razón por la que debe mantenerse alejado de la idea de mirar a escondidas en su interior.fuente
const
valor) ocurrió antes. Sin embargo, el argumento "lamove
[función] asegura que sea seguro deconstruir [un objeto de] una clase después de que se haya movido" no se cumple: no se puede mover con seguridad desde unconst
objeto / referencia, ya que eso requiere una modificación, queconst
previene Puede intentar evitar esa limitación mediante el usoconst_cast
, pero en ese punto, en el mejor de los casos, está entrando en un comportamiento específico de implementación profunda, si no UB.move
función toma una en&
lugar de unaconst&
, por lo que si uno insiste en que mueve una tecla fuera de un mapa, debe usarlaconst_cast
.const_cast
va a ser desagradable. Puede funcionar la mayor parte del tiempo, pero rompa su código de manera sutil.extract
, se nos permite movernos desde el mismo objeto, ¿verdad? (ver mi respuesta)std::launder