Lea
Lea

Reputation: 221

Accessing elements from map<key, set<datatype>>

I am working with a data structure that looks like this:

map<string, set<string>> data;

By now, I had no problems with working with the map by using foreach cycle, however, now I need to print out the data from the map like this:

KEY: elem1, elem2, elem3
KEY2: elem1, elem2, elem3

Because of the missing comma at the end, I cannot quite use the foreach cycle anymore (can I?). Since I am new to C++, C++11 and all the fun it offers, I am quite lost. I came up with:

for ( auto i : data )
{
    cout << i.first << ": ";
    for ( size_t i = 0; i < /* size of the set */ - 1; i ++ )
        cout << j << ", ";

    cout << /* the last element of the set */ << endl;
}

I know what I want, I just have no idea about syntax and the C++ reference is not helping much. Thanks for answers, meanwhile I am going to browse the C++ reference myself.

Upvotes: 5

Views: 341

Answers (6)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

template<typename R>
struct not_last_t {
  R r;
  not_last_t(R&& in):r(std::forward<R>(in)){}
  not_last_t(not_last_t&&)=default;
  void operator=(not_last_t&&)=delete;
  // C++14 because lazy.  Can be written in C++11:
  auto begin() { using std::begin; return begin(r); }
  auto end() {
    using std::end; auto ret=end(r); 
    if(ret==begin()) return ret;
    return std::next(ret,-1);
  }
};
template<typename R>
not_last_t<R> not_last(R&&r){return {sts::forward<R>(r);}}

use:

for(auto x:not_last(s))
  std::cout << x << ", ";
if (!s.empty())
  std::cout << s.back();

efficient and clear at point of use. Bulky in library.

Uses some C++14 features, because I am lazy, and probably full of typos. If there is interest I can clean the code up.

Upvotes: 0

Matthieu M.
Matthieu M.

Reputation: 299810

A pattern I often use (with BOOST_FOREACH), is:

bool first = true;
for (auto const& e: collection) {
    if (first) { first = false; } else { out << ", "; }
    ...
}

There is a STL way of doing this though, using ostream_iterator:

std::copy(collection.begin(), collection.end(),
          std::ostream_iterator<value_type>(out, ", "));

and so your example becomes:

for (auto const& pair: map) {
    out << pair.first << ": ";

    std::copy(pair.second.begin(), pair.second.end(),
              std::ostream_iterator<std::string>(out, ", "));
}

but honestly, I still feel like using the bool first = true approach is more readable.

Upvotes: 4

Mr.C64
Mr.C64

Reputation: 42934

You can consider code like this (which uses a flag to print the first string in the set in a different way than the following strings, that are instead prefixed with ", ").

Note that since you are observing the elements in the containers, you may want to use const auto& in the range-for loops.

#include <iostream>
#include <map>
#include <set>
#include <string>
using namespace std;

int main() {
    // Fill the data structure with some test data
    map<string, set<string>> data;
    data["Key1"].emplace("hello");
    data["Key1"].emplace("world");
    data["Key1"].emplace("test");
    data["Key2"].emplace("Ciao");
    data["Key2"].emplace("Mondo");

    // For each item in the outer map
    for (const auto& x : data) {
        // Print item's key
        cout << x.first << ": ";

        // Special output for the first string in the set
        bool first = true;

        // Note: x.first is the key, x.second is the associated set.

        // For each string in the set:
        for (const auto& s : x.second) {
            // Prefix every string except the first one with ", "
            if (!first) {
                cout << ", ";
            } else {
                first = false;
            }

            // Print current string in the set
            cout << s;
        }

        cout << endl;
    }

    return 0;
}

Live on Ideone

Output:

Key1: hello, test, world
Key2: Ciao, Mondo

Upvotes: 2

Christian Hackl
Christian Hackl

Reputation: 27528

You won't find a "perfect" solution. Basically, you want to do the same thing for every element (by virtue of the range-based for loop) but still handle one case in a different way. That doesn't go together. One thing you could do is use an additional flag to handle the first element differently:

bool first_element = true;
for ( auto i : data )
{
    if ( !first_element)
    {
        // print ","
    }
    // print i;    

    first_element = false;
}

Generally, don't worry too much about it if this is just a small piece of code in some isolated place somewhere in your project.

Upvotes: 1

user703016
user703016

Reputation: 37945

I typically (across several languages) use a delimiter that I initialize to an empty string and then change in the loop:

for (auto& i : data) {
    std::cout << i.first << ": ";

    std::string delim;     
    for (auto& s : i.second) {
        std::cout << delim << s;

        delim = ", ";
    }

    std::cout << std::endl;
}

Output (live demo on coliru):

KEY: elem1, elem2, elem3
KEY1: elem1, elem2, elem3

Another solution is to use ostream iterators

Upvotes: 2

nosid
nosid

Reputation: 50044

I typically use iterators to join strings.

for (auto& i : data) {
    cout << i.first << ':';
    auto p = i.second.begin();
    auto q = i.second.end();
    if (p != q) {
        cout << ' ' << *p;
        for (++p; p != q; ++p) {
            cout << ", " << *p;
        }
    }
    cout << '\n';
}

Upvotes: 1

Related Questions