1

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?

EB2127
  • 1,788
  • 3
  • 22
  • 43

6 Answers6

2

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.

alfC
  • 14,261
  • 4
  • 67
  • 118
2

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    Very nice, `iterators_of` can be implemented with `range::iota`, see my new answer. – alfC Sep 19 '21 at 08:47
2

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

Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56
  • 1
    @alfC No, not at all. It's a pointer assignment and that the pointee (in the pointer type) is const is necessary because string literals _are_ const. – Scheff's Cat Sep 19 '21 at 07:12
  • Ah, great. You can use initializer in `for` also: `for( const char* sep = ""; auto const& [key, value] : mymap) { ... } `. – alfC Sep 19 '21 at 08:50
  • @alfC _You can use initializer in `for` also_ This seems to be supported since [C++20](https://en.cppreference.com/w/cpp/language/range-for). With C++17 (to which the OP like me seems to be stuck to), it's not yet supported but it doesn't change much. ;-) – Scheff's Cat Sep 19 '21 at 09:42
2

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

alfC
  • 14,261
  • 4
  • 67
  • 118
  • 2
    Nice, iota's weakly incrmentable requirement is pretty much the subset of properties common to iterators and integers needed here. Makes sense, as my index I also used to make integer ranges, that an integer range tool in ranges would work as welll. – Yakk - Adam Nevraumont Sep 19 '21 at 13:14
  • @Yakk-AdamNevraumont Yes, that is right, iterators are weaker concept than integers (wrt to arithmetic). Interestingly, this kind of use opens a can of weird but possibly consistent semantic meaning IMO. The reason is that, in the Ranges syntax, if this works `iota(std::begin(r), std::end(r));` this should work also `iota(r);`. Now `iota(r)` already means 'increment "r"' with no bound. – alfC Sep 20 '21 at 04:25
  • @Yakk-AdamNevraumont, That means that `iota(n) -> n, n + 1, n + 2...`. It turn this means that `begin(n)` should be `n` (e.g. integer), and also `*n -> n`, and by extension `n[m] -> n + m`. Also `end(n) -> numeric_limit::max()`. Mind boggling. (Stepanov was right, dereferencing should return self *by default* for any non-pointer thing). – alfC Sep 20 '21 at 04:25
  • I really like this answer using `range::iota`. I wish I could give points to @Yakk-AdamNevraumont (and the other good answers) too for the accepted answer. – EB2127 Sep 20 '21 at 22:29
  • @EB2127 . Yes, I agree, credits should go to Yakk-AdamNevraumont . If you feel about it strongly, you can give bounties. – alfC Sep 20 '21 at 22:33
  • 1
    @EB2127 Don't worry about it. I have approximately infinite internet points already. – Yakk - Adam Nevraumont Sep 20 '21 at 23:12
1

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;
}
Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19
1

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

Ashutosh Aswal
  • 494
  • 3
  • 12
  • 2
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-ask). – Community Sep 20 '21 at 06:29