c++ c++11 dictionary iteration readability

¿Es posible en C++ realizar la iteración std:: map<> “para elemento: contenedor” con variables nombradas(por ejemplo, clave y valor) en lugar de.first y.second?



c++11 dictionary (7)

No estaba seguro de qué buscar. Encontré Renombrar primero y segundo de un iterador de mapa, pero no es exactamente lo que quiero hacer.

Esto es lo que me gustaría hacer [vea a continuación el código C ++ sin sentido]. ¿Es algo cercano a esto posible? De lo contrario, solo tendré que ir con la opción de "adaptar" el iterador como la primera línea dentro del bucle, supongo.

// what I want to do: std::map<int, std::string> my_map; // ... populate my_map for(auto key, auto & value: my_map){ // do something with integer key and string value }

C ++ 11 está bien, pero evita el refuerzo si es posible.

Lo más cerca que he estado es

// TODO, can this be templated? struct KeyVal{ int & id; std::string & info; template <typename P> KeyVal(P & p) : id(p.first) , info(p.second) { } }; //... for ( KeyVal kv : my_map ){ std::cout << kv.info; }

Pero eso significa escribir una clase de adaptador para cada mapa :(

// slightly joke answer/"what could possibly go wrong?" #define key first #define value second


Con c ++ 17 moderno, esto es ahora posible con enlaces estructurados .

#include <map> #include <string> #include <iostream> using namespace std; int main() { map<int, string> my_map; my_map[0] = "hello"; my_map[1] = "world"; for (auto&& [key, value] : my_map) { cout << key << "," << value << "/n"; } return 0; }

Constrúyelo:

$ clang++ -std=c++17 test.cpp -o program

Salida:

$ ./program 0,hello 1,world


En Apache Mesos , usamos una macro llamada foreachpair que se puede usar así:

foreachpair (const Key& key, const Value& value, elems) { /* ... */ }

Por supuesto, puede reemplazar la Key y el Value con auto , junto con los calificadores que quiera usar allí. También es compatible con break y continue .

Mi última implementación se ve así:

#define FOREACH_PREFIX BOOST_PP_CAT(foreach_, __LINE__) #define FOREACH_BODY BOOST_PP_CAT(FOREACH_PREFIX, _body__) #define FOREACH_BREAK BOOST_PP_CAT(FOREACH_PREFIX, _break__) #define FOREACH_CONTINUE BOOST_PP_CAT(FOREACH_PREFIX, _continue__) #define FOREACH_ELEM BOOST_PP_CAT(FOREACH_PREFIX, _elem__) #define FOREACH_ONCE BOOST_PP_CAT(FOREACH_PREFIX, _once__)

Las macros anteriores proporcionan nombres únicos a varios componentes que se utilizan en la macro foreachpair al incluir el número __LINE__ .

1 #define foreachpair(KEY, VALUE, ELEMS) / 2 for (auto&& FOREACH_ELEM : ELEMS) / 3 if (false) FOREACH_BREAK: break; /* set up the break path */ / 4 else if (bool FOREACH_CONTINUE = false) {} /* var decl */ / 5 else if (true) goto FOREACH_BODY; /* skip the loop exit checks */ / 6 else for (;;) /* determine whether we should break or continue. */ / 7 if (!FOREACH_CONTINUE) goto FOREACH_BREAK; /* break */ / 8 else if (true) break; /* continue */ / 9 else / 10 FOREACH_BODY: / 11 if (bool FOREACH_ONCE = false) {} /* var decl */ / 12 else for (KEY = std::get<0>( / 13 std::forward<decltype(FOREACH_ELEM)>(FOREACH_ELEM)); / 14 !FOREACH_ONCE; FOREACH_ONCE = true) / 15 for (VALUE = std::get<1>( / 16 std::forward<decltype(FOREACH_ELEM)>(FOREACH_ELEM)); / 17 !FOREACH_CONTINUE; FOREACH_CONTINUE = true)

Voy a caminar a través de esta línea por línea.

  1. (Inicio del lío).
  2. Rango de bucle for basado en ELEMS sobre ELEMS .
  3. Creamos la etiqueta FOREACH_BREAK . Saltamos a esta etiqueta para salir de este bucle.
  4. Configuramos el indicador de flujo de control FOREACH_CONTINUE . Esto es true si la iteración actual se cerró normalmente, o por medio de continue , y false si la iteración actual se cerró por break .
  5. Siempre FOREACH_BODY a la etiqueta FOREACH_BODY , a continuación.
  6. Aquí es donde interceptamos el flujo de control e inspeccionamos el indicador FOREACH_CONTINUE para determinar cómo salimos de la iteración actual.
  7. Si FOREACH_CONTINUE es false , sabemos que salimos a través de la break así que FOREACH_BREAK a FOREACH_BREAK .
  8. De lo contrario, FOREACH_CONTINUE es true y salimos del bucle for (;;) que nos lleva a la siguiente iteración.
  9. (A mitad del camino).
  10. Siempre saltamos aquí desde (5).
  11. Configure FOREACH_ONCE que solo se utiliza para ejecutar el bucle for que declara KEY exactamente una vez.
  12. Declarar KEY .
  13. Adelante el elemento correctamente.
  14. Use FOREACH_ONCE para asegurarse de que este bucle se ejecute exactamente una vez.
  15. Declarar VALUE .
  16. Adelante el elemento correctamente.
  17. Use FOREACH_CONTINUE para asegurarse de que este bucle se ejecute exactamente una vez, y para indicar si el bucle se salió por break o no.

NOTA : El uso de std::get permite el soporte de std::tuple o std::array que sale de la secuencia también. por ejemplo, std::vector<std::tuple<int, int>>

Demo de Ideone


Lo más cercano a usar std::tie :

std::map<int, std::string> my_map; int key; std::string value; for(auto&& p: my_map) { std::tie(key, value) = p; std::cout << key << ": " << value << std::endl; }

Por supuesto, las expresiones no se pueden colocar en el bucle for range, por lo que, en lugar de eso, se podría usar una macro para permitir la expresión:

#define FOREACH(var, cont) / for(auto && _p:cont) / if(bool _done = false) {} / else for(var = std::forward<decltype(_p)>(_p); !_done; _done = true)

Entonces, std::tie puede usarse directamente en el bucle:

std::map<int, std::string> my_map; int key; std::string value; FOREACH(std::tie(key, value), my_map) { std::cout << key << ": " << value << std::endl; }


Normalmente prefiero un enfoque KISS para esto:

template<typename KeyValuePair> typename KeyValuePair::first_type& key(KeyValuePair& kvp) { return kvp.first; } template<typename KeyValuePair> const typename KeyValuePair::first_type& key(const KeyValuePair& kvp) { return kvp.first; } template<typename KeyValuePair> void key(const KeyValuePair&& kvp) = delete; template<typename KeyValuePair> typename KeyValuePair::second_type& value(KeyValuePair& kvp) { return kvp.second; } template<typename KeyValuePair> const typename KeyValuePair::second_type& value(const KeyValuePair& kvp) { return kvp.second; } template<typename KeyValuePair> void value(const KeyValuePair&& kvp) = delete;

con ejemplo de uso como este:

for(auto& kvp : my_map) { std::cout << key(kvp) << " " << value(kvp) << "/n"; }


Podrías escribir una plantilla de clase:

template <class K, class T> struct MapElem { K const& key; T& value; MapElem(std::pair<K const, T>& pair) : key(pair.first) , value(pair.second) { } };

con la ventaja de poder escribir key y value pero con la desventaja de tener que especificar los tipos:

for ( MapElem<int, std::string> kv : my_map ){ std::cout << kv.key << " --> " << kv.value; }

Y eso tampoco funcionará si my_map fuera const . Tendrías que hacer algo como:

template <class K, class T> struct MapElem { K const& key; T& value; MapElem(std::pair<K const, T>& pair) : key(pair.first) , value(pair.second) { } MapElem(const std::pair<K const, std::remove_const_t<T>>& pair) : key(pair.first) , value(pair.second) { } }; for ( MapElem<int, const std::string> kv : my_map ){ std::cout << kv.key << " --> " << kv.value; }

Es un desastre. Lo mejor por ahora es simplemente acostumbrarse a escribir .second y .second y esperar que se .second la propuesta de enlaces estructurados, lo que permitiría lo que realmente desea:

for (auto&& [key, value] : my_map) { std::cout << key << " --> " << value; }


Solo por proporcionar otra forma de hacer casi lo que quieres, escribí esto hace un tiempo para evitar tener .second y .second todo mi código:

auto pair2params = [](auto&& f) { return [f](auto&& p) { f(p.first, p.second); }; };

Ahora puedes escribir algo como (asumiendo un rango basado en for_each ):

int main() { auto values = map<int, string>{ {0, "hello"}, {1, "world!"} }; for_each(values, pair2params([](int key, const string& value) { cout << key << ": " << value << "/n"; }); }

Ejemplo de ejecución: http://ideone.com/Bs9Ctm


Un enfoque inspirado por Barry a continuación sería escribir un adaptador de rango.

Hacer esto sin boost o un soporte de biblioteca similar es un dolor, pero:

  1. Escribe una plantilla de rango. Almacena 2 class iterator y tiene los métodos begin() y end() (y lo que quieras).

  2. Escribe un adaptador iterador de transformación. Toma un iterador y lo envuelve para que su tipo de valor sea transformado por algún objeto de función F.

  3. Escriba un transformador to_kv que tome un std::pair<K, V> cv& y devuelva una struct kv_t { K cv& key; V cv& value; } struct kv_t { K cv& key; V cv& value; } struct kv_t { K cv& key; V cv& value; } .

  4. Conecta 3 en 2 en 1 y llámalo as_kv . Toma un rango de pares y devuelve un rango de valores-clave.

La sintaxis con la que terminas es:

std::map<int, std::string> m; for (auto kv : as_kv(m)) { std::cout << kv.key << "->" << kv.value << "/n"; }

lo cual es bueno.

Aquí hay una solución minimalista que en realidad no crea iteradores legales, pero admite for(:) :

template<class Key, class Value> struct kv_t { Key&& key; Value&& value; }; // not a true iterator, but good enough for for(:) template<class Key, class Value, class It> struct kv_adapter { It it; void operator++(){ ++it; } kv_t<Key const, Value> operator*() { return {it->first, it->second}; } friend bool operator!=(kv_adapter const& lhs, kv_adapter const& rhs) { return lhs.it != rhs.it; } }; template<class It, class Container> struct range_trick_t { Container container; range_trick_t(Container&&c): container(std::forward<Container>(c)) {} It begin() { return {container.begin()}; } It end() { return {container.end()}; } }; template<class Map> auto as_kv( Map&& m ) { using std::begin; using iterator = decltype(begin(m)); // no extra (())s using key_type = decltype((begin(m)->first)); // extra (())s on purpose using mapped_type = decltype((begin(m)->second)); // extra (())s on purpose using R=range_trick_t< kv_adapter<key_type, mapped_type, iterator>, Map >; return R{std::forward<Map>(m)}; } std::map<int, std::string> m() { return {{0, "Hello"}, {2, "World"}}; }

Lo cual es muy mínimo, pero funciona. Por lo general, no recomendaría este tipo de pseudo iteradores a medias para for(:) loops; usar iteradores reales es solo un costo adicional modesto, y no sorprende a las personas más adelante.

ejemplo vivo

(Ahora con soporte de mapas temporal. No es compatible con matrices C planas ... todavía)

Range-trick almacena un contenedor (posiblemente una referencia) para copiar contenedores temporales en el objeto almacenado durante el ciclo for(:) . Container no temporales el tipo de Container es un Foo& de algún tipo, por lo que no hace una copia redundante.

Por otro lado, kv_t obviamente solo almacena referencias. Podría haber un caso extraño de iteradores que devuelven temporarios que rompen esta implementación de kv_t , pero no estoy seguro de cómo evitarlo en general sin sacrificar el rendimiento en casos más comunes.

Si no te gusta el kv. parte de lo anterior, podemos hacer algunas soluciones, pero no son tan limpias.

template<class Map> struct for_map_t { Map&& loop; template<class F> void operator->*(F&& f)&&{ for (auto&& x:loop) { f( decltype(x)(x).first, decltype(x)(x).second ); } } }; template<class Map> for_map_t<Map> map_for( Map&& map ) { return {std::forward<Map>(map)}; }

entonces:

map_for(m)->*[&](auto key, auto& value) { std::cout << key << (value += " ") << ''/n''; };

¿suficientemente cerca?

ejemplo vivo

Hay algunas propuestas en torno a las tuplas de primera clase (y, por lo tanto, pares) que pueden ofrecerle algo así, pero no conozco el estado de las propuestas.

La sintaxis que puede terminar si esto llega a C ++ sería algo como esto:

for( auto&& [key, value] : container )

Comentarios sobre la abominación ->* arriba:

Por lo tanto, el ->* se está utilizando como un operator bind de Haskell (junto con el desempaquetado implícito de la tupla), y le estamos suministrando un lambda que toma los datos contenidos en el mapa y devuelve el vacío. El tipo de retorno (Haskell-esque) se convierte en un mapa sobre el vacío (nada), que elijo en vacío.

La técnica tiene un problema: se pierde break; y continue; que chupan.

Una variante menos hackey inspirada en Haskell esperaría que la lambda devuelva algo así como void | std::experimental::expected<break_t|continue_t, T> void | std::experimental::expected<break_t|continue_t, T> , y si T es void , no devuelve nada, si T es un tipo de tupla devuelve un mapa, y si T es un mapa se une al tipo de mapa devuelto. También desempaquetaría o no desempaquetaría la tupla contenida según lo que quiera la lambda (detección al estilo SFINAE).

Pero eso es demasiado para una respuesta SO; esta digresión señala que el estilo de programación anterior no es un callejón sin salida completo. Sin embargo, es poco convencional en C ++.