¿Robar recursos de std :: claves del mapa permitidas?

15

En C ++, ¿está bien robar recursos de un mapa que ya no necesito? Más precisamente, suponga que tengo un std::mapcon std::stringclaves y quiero construir un vector a partir de él robando los recursos de las mapteclas 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 mappero 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 mapporque lo accedí de una manera que std::mapno 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.

phinz
fuente
3
¿Cuál es el propósito de este "robo"? Para eliminar el elemento del mapa? Entonces, ¿por qué no simplemente hacer eso (borrar el elemento del mapa)? Además, modificar un constvalor siempre es UB.
Algún tipo programador
¡aparentemente conducirá a errores serios!
Rezaebrh
1
No es una respuesta directa a su pregunta, pero: ¿Qué sucede si no devuelve un vector sino un rango o un par de iteradores? Eso evitaría copiar por completo. En cualquier caso, necesita puntos de referencia para realizar un seguimiento del progreso de la optimización y un generador de perfiles para encontrar puntos de acceso.
Ulrich Eckhardt
1
@ ALX23z ¿Tiene alguna fuente para esta declaración? No puedo imaginar cómo copiar un puntero es más costoso que copiar una región entera de memoria.
Sebastian Hoffmann
1
@SebastianHoffmann se mencionó en CppCon reciente, no estoy seguro de qué charla, aunque. La cosa es que std::stringtiene 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.
ALX23z

Respuestas:

18

Estás haciendo un comportamiento indefinido, usando const_castpara modificar una constvariable. No hagas eso. La razón constes 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_castpara eliminar constde una variable y modificar esa variable.

Dicho esto, C ++ 17 tiene la solución para su problema: std::mapla extractfunción 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())));
}

Y no lo hagas using namespace std;. :)

druckermanly
fuente
¡Gracias, intentaré esto! ¿Pero estás seguro de que no puedo hacer lo que hice? Quiero decir mapque 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.
phinz
2
Se crean las claves del mapa const. La mutación de un constobjeto es UB instantáneo, ya sea que algo realmente acceda a él o no después.
HTNW
Este método tiene dos problemas: (1) Como quiero extraer todos los elementos, no quiero extraerlos por clave (búsqueda ineficiente) sino por iterador. He visto que esto también es posible, así que está bien. (2) Corríjame si me equivoco, pero para extraer todos los elementos habrá una sobrecarga enorme (para reequilibrar la estructura interna del árbol en cada extracción).
phinz
2
@phinz Como puede ver en cppreference 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 propios maprequisitos. Los stdcontenedores están destinados a una aplicación común de uso general. No están optimizados para casos de uso específicos.
nogal
@HTNW ¿Está seguro de que se crean las claves const? En este caso, ¿puede indicar dónde está mal mi argumentación ?
phinz
4

Su código intenta modificar constobjetos, 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 constobjeto porque el identificador de nodo resultante de la extracción de nodos fuera del mapa permite el no constacceso a la clave. Esta inferencia puede parecer plausible al principio, pero P0083R3 , el documento que introdujo las extractfuncionalidades), tiene una sección dedicada sobre este tema que invalida este argumento:

Preocupaciones

Se han planteado varias preocupaciones sobre este diseño. Los abordaremos aquí.

Comportamiento indefinido

La parte más difícil de esta propuesta desde una perspectiva teórica es el hecho de que el elemento extraído conserva su tipo de clave constante. Esto evita salir de él o cambiarlo. Para resolver esto, hemos proporcionado la función de acceso de clave , que proporciona acceso no constante a la clave en el elemento que posee el identificador de nodo. Esta función requiere una implementación "mágica" para garantizar que funcione correctamente en presencia de optimizaciones del compilador. Una forma de hacerlo es con una unión de pair<const key_type, mapped_type> y pair<key_type, mapped_type>. La conversión entre estos puede realizarse de forma segura utilizando una técnica similar a la utilizada std::launderen la extracción y reinserción.

No creemos que esto plantee ningún problema técnico o filosófico. Una de las razones por las que existe la Biblioteca estándar es para escribir código mágico y no portátil que el cliente no puede escribir en C ++ portátil (p <atomic>. Ej <typeinfo>. <type_traits>, Etc.). Este es solo otro ejemplo de este tipo. Todo lo que se requiere de los proveedores de compiladores para implementar esta magia es que no exploten el comportamiento indefinido en los sindicatos con fines de optimización, y actualmente los compiladores ya lo prometen (en la medida en que se está aprovechando aquí).

Esto impone una restricción al cliente que, si se utilizan estas funciones, std::pairno puede especializarse de modo que pair<const key_type, mapped_type>tenga un diseño diferente que pair<key_type, mapped_type>. Creemos que la probabilidad de que alguien realmente quiera hacer esto es efectivamente cero, y en la redacción formal restringimos cualquier especialización de estos pares.

Tenga en cuenta que la función miembro clave es el único lugar donde tales trucos son necesarios, y que no se requieren cambios en los contenedores o pares.

(énfasis mío)

LF
fuente
Esto es en realidad una parte de la respuesta a la pregunta original, pero solo puedo aceptar una.
Phinz
0

No creo que eso const_casty la modificación conduzcan a un comportamiento indefinido en este caso, pero comente si esta argumentación es correcta.

Esta respuesta afirma que

En otras palabras, obtienes UB si modificas un objeto const original, y de lo contrario no.

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 el stringobjeto p.firstno se creó como un objeto constante. Ahora tenga en cuenta que la referencia sobre losextract estados

Extraer un nodo invalida los iteradores al elemento extraído. Los punteros y las referencias al elemento extraído siguen siendo válidos, pero no se pueden usar mientras el elemento es propiedad de un identificador de nodo: se pueden usar si el elemento se inserta en un contenedor.

Entonces, si extractel node_handlecorrespondiente a p, pcontinúa viviendo en su ubicación de almacenamiento. Pero después de la extracción, se me permite moveeliminar los recursos pcomo en el código de la respuesta de druckermanly . Esto significa que py por lo tanto también el stringobjeto p.firstfue no creó como objeto const originalmente.

Por lo tanto, creo que la modificación de las mapclaves 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) mapno introduce problemas en el destructor. Por lo tanto, el código en la pregunta debería funcionar bien al menos en C ++ 17 donde extractexiste 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.

phinz
fuente
1
Lo siento, Phinz, tu respuesta es incorrecta. Escribiré una respuesta para explicar esto: tiene algo que ver con los sindicatos y std::launder.
LF
-1

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 mappor la fuerza rompe el orden en el que mapse construye la estructura interna de datos (árbol Rojo-Negro). Pero es seguro usar el extractmé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 movemé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 lo moveque garantiza que sea seguro eliminar o reasignar algún valor nuevo a la clase "movida". Específicamente para string, el movemétodo puede establecer su puntero de caracteres en nullptr, 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_castprobablemente no sea un UB. constes solo una promesa entre el compilador y nosotros. Los objetos señalados como consttodavía son un objeto, igual que los que no lo son const, en términos de sus tipos y representaciones en forma binaria. Cuando constse descarta, debe comportarse como si fuera una clase mutable normal. Con respecto a move, si desea usarlo, debe pasar un en &lugar de un const &, por lo que, como veo, no es un UB, simplemente rompe la promesa consty aleja los datos.

También hice dos experimentos, usando MSVC 14.24.28314 y Clang 9.0.0 respectivamente, y arrojaron el mismo 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';
}

salida:

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

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, removefunciones, 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.

Deuchie
fuente
2
"En cuanto a si causaría problemas al deconstruir, no es un problema". Técnicamente correcto, ya que el comportamiento indefinido (cambio de un constvalor) ocurrió antes. Sin embargo, el argumento "la move[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 un constobjeto / referencia, ya que eso requiere una modificación, que constpreviene Puede intentar evitar esa limitación mediante el uso const_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.
hoffmale
@hoffmale Gracias, lo pasé por alto y cometí un gran error. Si no fue usted quien lo señaló, mi respuesta aquí puede inducir a error a otra persona. En realidad, debería decir que la movefunción toma una en &lugar de una const&, por lo que si uno insiste en que mueve una tecla fuera de un mapa, debe usarla const_cast.
Deuchie
1
"los objetos señalados como const siguen siendo un objeto, igual que los que no tienen const, en términos de sus tipos y representaciones en forma binaria" No. los objetos const pueden colocarse en la memoria de solo lectura. Además, const permite al compilador razonar sobre el código y almacenar en caché el valor en lugar de generar código para múltiples lecturas (lo que puede marcar una gran diferencia en términos de rendimiento). Entonces, la UB causada por const_castva a ser desagradable. Puede funcionar la mayor parte del tiempo, pero rompa su código de manera sutil.
LF
Pero aquí creo que podemos estar seguros de que los objetos no se colocan en la memoria de solo lectura porque después extract, se nos permite movernos desde el mismo objeto, ¿verdad? (ver mi respuesta)
phinz
1
Lo sentimos, el análisis en su respuesta es incorrecto. Escribiré una respuesta para explicar esto: tiene algo que ver con los sindicatos ystd::launder
LF