如何使用 C++17 的结构化绑定检测 std::map 的最后一次迭代?

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

如何使用结构化绑定检测地图的最后一次迭代?

这是一个具体的例子:我有以下简单代码,我使用 C++17 中的结构化绑定从 std::map 中打印元素:

#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;
}

问题是,这将导致尾随逗号,即

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

受这个问题的启发:How can I detect the last iteration in a loop over std::map?,我可以用迭代器编写代码来打印出 std::map 内容而不带尾随逗号,即

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 << ", ";
    }
}

如何用 for (auto const& [key, value] : mymap) 做到这一点?如果我知道 std::map 中的最后一个键,我可以为它写一个条件;但是有没有不求助于 iter 的另一种方法?

简短的回答,你不能。 (这不是范围循环的用途。) 你可以做的是将原始范围分成两个子范围:

    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

或更实用一点:

    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;
    }

或者您可以“索引”范围内的元素。 (STD 范围仍然缺少 zip 来做到这一点。)

#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

(令人惊讶的是,Boost.Ranges 与结构化绑定一起工作,就好像它是全新的一样。)

如您所见,重点是您通过强迫自己使用 range-for 循环来对抗该语言。 使用基于迭代器的循环看起来更有吸引力。

当我需要位置和数据时,我编写了一个 iterators_of 适配器,它接受一个范围,returns 它的迭代器范围。

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

的迭代器很短。

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;
};

然后将其扩充为完整的迭代器或直接编写

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)}};
}

代码不多。

我发现这比使用迭代器迭代手动 futzing 更好。

从 C++20 开始,您可以使用 init 语句并将其编写为:

#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;
}

在已经看到三种可能的解决方案之后,这里是第四种(而且相当简单)......

因此,我将分隔符移到了输出的开头,并注意在第二次迭代之前修改它:

#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;
}

输出:

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

Demo on coliru

Yakk 的回答启发我使用 Ranges 编写 iterators_of 而没有引入新的 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

对于除第一个键和值对之外的所有键和值对,在开头打印 , 怎么样?

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

我们可以使用布尔值来处理第一个键值对。

 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;
 }

输出:Key: a, Value: 1, Key: b, Value: 1, Key: c, Value: 1, Key: d, Value: 49