EB2127
EB2127

Reputation: 1858

How to detect the last iteration of std::map using structured bindings from C++17?

How could I detect the last iteration of a map using structured bindings?

Here's a concrete example: I have the following simple code whereby I print elements from an std::map using structured bindings from C++17:

#include <iostream>
#include <map>
#include <string>

int main() {
    
    std::map<std::string, size_t> mymap {{"a", 0}, {"b", 1}, {"c", 2}, {"d", 3}};
   
    // using structured bindings, C++17

    for (auto const& [key, value] : mymap) {
        std::cout << "key: " << key << ", value: " << value << ", ";
    }

    return 0;
}

The problem is, this will result in a trailing comma, i.e.

key: a, value: 0, key: b, value: 1, key: c, value: 2, key: d, value: 3, 

Inspired by this question: How can I detect the last iteration in a loop over std::map?, I can write code with an iterator to print out the std::map contents without the trailing comma, i.e.

for (auto iter = mymap.begin(); iter != mymap.end(); ++iter){
    // detect final element
    auto last_iteration = (--mymap.end());
    if (iter==last_iteration) {
        std::cout << "\"" << iter->first << "\": " << iter->second;
    } else {
        std::cout << "\"" << iter->first << "\": " << iter->second << ", ";
    }
}

How does one do this with for (auto const& [key, value] : mymap)? If I know the final key in std::map, I could write an conditional for it; but is there another way without resorting to iter?

Upvotes: 1

Views: 619

Answers (6)

alfC
alfC

Reputation: 16242

Yakk's answer inspired me to write an iterators_of using Ranges and without introducing a new class:

#include <iostream>
#include <map>
#include <string>
#include <ranges>

template<class Range>
auto iterators_of(Range&& r){
    return std::ranges::views::iota(std::begin(r), std::end(r));
}

int main() {
    
    std::map<std::string, size_t> mymap {{"a", 0}, {"b", 1}, {"c", 2}, {"d", 3}};

    for(auto it : iterators_of(mymap) ) {
        auto const& [key, value] = *it;
        if(std::next(it) != mymap.end()) {std::cout << "key: " << key << ", value: " << value << ", ";}
        else                             {std::cout << "key: " << key << ", value: " << value;}
    }
    return 0;
}

https://godbolt.org/z/rx15eMnWh

Upvotes: 2

Ashutosh Aswal
Ashutosh Aswal

Reputation: 575

What about printing , in the beginning for all key and value pairs except the first one.

IF first_key_value_pair:
    print(key, value)
ELSE:
    print(',' + (key, value))

We can use a boolean to take care of the first key-value pair.

 std::map<char, int> m = {{'a', 1}, {'b', 1}, {'c', 1}, {'d', '1'}};
 bool first = true; 

 for(const auto& [key, value] : m){
    if(first) first = false; else std::cout << ", ";
    std::cout << "Key: " << key << ", Value: " << value;
 }

Output: Key: a, Value: 1, Key: b, Value: 1, Key: c, Value: 1, Key: d, Value: 49

Upvotes: 2

alfC
alfC

Reputation: 16242

Short answer, you can't. (That is not what range loops are for.) What you can do is divide the original range in two subranges:

    for(auto const& [key, value] : std::ranges::subrange(mymap.begin(), std::prev(mymap.end())) ) {
        std::cout << "key: " << key << ", value: " << value << ", ";
    }
    for(auto const& [key, value] : std::ranges::subrange(std::prev(mymap.end()), mymap.end()) ) {
        std::cout << "key: " << key << ", value: " << value;
    }

https://godbolt.org/z/4EWYjMrqG

or a bit more functional:

    for(auto const& [key, value] : std::ranges::views::take(mymap, mymap.size() - 1)) {
        std::cout << "key: " << key << ", value: " << value << ", ";
    }
    for(auto const& [key, value] : std::ranges::views::take(std::ranges::views::reverse(mymap), 1)) {
        std::cout << "key: " << key << ", value: " << value;
    }

or you can "index" the elements of the range. (STD Ranges still lacks zip to do this.)

#include <iostream>
#include <map>
#include <string>

#include <boost/range/adaptor/indexed.hpp>

int main() {
    
    std::map<std::string, size_t> mymap {{"a", 0}, {"b", 1}, {"c", 2}, {"d", 3}};

    for(auto const& [index, kvp] : mymap |  boost::adaptors::indexed(0) ) {
        auto const& [key, value] = kvp;
        if(index != mymap.size() - 1) {std::cout << "key: " << key << ", value: " << value << ", ";}
        else                          {std::cout << "key: " << key << ", value: " << value;}
    }
    return 0;
}

https://godbolt.org/z/f74Pj1Gsz

(Amazing that Boost.Ranges works with structured bindings as if it were brand new.)

The whole point is, as you see, you are fighting against the language by forcing yourself to use a range-for loop. It looks more attractive to use the iterator based loop.

Upvotes: 3

Scheff&#39;s Cat
Scheff&#39;s Cat

Reputation: 20141

After having already seen three possible solutions, here a fourth (and admittedly rather simple)…

Thereby, I moved the separator to the begin of the output and take care that it's modified before the second iteration:

#include <iostream>
#include <map>
#include <string>

int main() {
    
    std::map<std::string, size_t> mymap {{"a", 0}, {"b", 1}, {"c", 2}, {"d", 3}};
   
    // using structured bindings, C++17
    const char* sep = "";
    for ( auto const& [key, value] : mymap) {
        std::cout << sep << "key: " << key << ", value: " << value;
        sep = ", ";
    }

    return 0;
}

Output:

key: a, value: 0, key: b, value: 1, key: c, value: 2, key: d, value: 3

Demo on coliru

Upvotes: 2

Pepijn Kramer
Pepijn Kramer

Reputation: 12891

From C++20 onward you can use an init-statement and write it like this:

#include <iostream>
#include <map>
#include <string>

int main() 
{
    std::map<std::size_t, std::string> map{ {11, "a"}, {13, "b"}, {22, "c"}, {32, "d"} };
    std::size_t end = map.size() - 1;

    for (std::size_t n{ 0 }; auto const& [key, value] : map) 
    {
        std::cout << "key: " << key << ", value: " << value; 
        if ((n++) != end) std::cout << ", ";
    }

    return 0;
}

Upvotes: 1

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275405

When I need location as well as data, I write an iterators_of adapter that takes a range, and returns a range of its iterators.

for( auto it:iterators_of(foo) ){
  auto const&[key,value]=*it;
  // blah
  if (std:next(it)!=foo.end())
    std::cout<<',';
}

iterators of is short.

template<class T>
struct index{
  T t;
  T operator*()const{return t;}
  index& operator++(){++t; return *this;}
  index operator++(int)&{auto self=*this; ++*this; return self;}
  bool operator==(index const&)=default;
  auto operator<=>(index const&)=default;
};

then augment that to be a full iterator or just write

template<class It, class Sent=It>
struct range{
  It b;
  Sent e;
  It begin()const{return b;}
  Sent end()const{return e;}
};
template<class R>
auto iterators_of(R&& r){
  using std::begin; using std::end;
  return range{index{begin(r)},index{end(r)}};
}

which isn't much code.

I find this is better than manually futzing with iterator iteration.

Upvotes: 2

Related Questions