专用地图值的基于范围的 for 循环

range-based for loop for private map values

本文关键字:范围 for 循环 于范围 地图 专用      更新时间:2023-10-16

我有以下代码:

#include "stdafx.h"
#include <map>
#include <string>
#include <iostream>
class MyObject
{
public:
MyObject()
: m_Items{ { 1, "one" },{ 2, "two" },{ 3, "three" } }
{}
RETURNTYPE GetStringIterator() const
{
IMPLEMENTATION
}
private:
std::map<int, std::string> m_Items;
};

int main()
{
MyObject o;
for (auto& s : o.GetStringIterator())
{
std::cout << s;
}
}

为了允许任何MyObject客户端(在本例中为main()函数(在不复制任何数据的情况下迭代m_Items映射的值,应该RETURNTYPEIMPLEMENTATION是什么?似乎这应该可以通过基于 c++11 范围的循环和迭代器来实现。但我一直无法弄清楚如何。

基于范围的迭代可以像这样实现:

class MyObject
{
public:
MyObject()
: m_Items{ { 1, "one" },{ 2, "two" },{ 3, "three" } }
{}
auto begin()       { return m_Items.begin(); }
auto begin() const { return m_Items.begin(); }
auto end()       { return m_Items.end(); }
auto end() const { return m_Items.end(); }
private:
std::map<int, std::string> m_Items;
};

复制或不复制值取决于代码在调用站点的编写方式:

MyObject a;
for(auto [key,value] : a) {} // copies are made
for(auto & [key,value] : a) {} // no copy
for(auto const & [key,value] : a) {} // no copy

您可以通过删除beginend的非常量版本来禁用映射值的修改:

class MyObject
{
public:
MyObject()
: m_Items{ { 1, "one" },{ 2, "two" },{ 3, "three" } }
{}
auto begin() const { return m_Items.begin(); }
auto end() const { return m_Items.end(); }
private:
std::map<int, std::string> m_Items;
};

然后,尝试修改 range-for 循环中的值将导致编译错误:

MyObject a;
for(auto & [key,value] : a) {
//value.push_back('a');      // Not OK 
}
for(auto & [key,value] : a) {
cout << value;             // OK
}

请注意,如果 map 是实现细节,则应使用 @Barry 提出的答案,因为它仅迭代映射的值,而不是键。

您可以使用boost::adaptors::map_values,它适用于C++11:

auto GetStringIterator() const
// NB: have the move the declaration of m_Items ahead of this function for this to work
-> decltype(m_Items | boost::adaptors::map_values)
{
return m_Items | boost::adaptors::map_values;
}

或其范围 v3 等效物,view::values.两者都可以像values(m)而不是m | values一样使用,如果你喜欢的话。

这两种解决方案都会返回地图值的视图。这是一个不拥有任何底层元素的对象,复制成本很低 - 即 O(1(。我们不是在腼腆地绘制地图或其任何潜在元素。

您可以像使用任何其他范围一样使用它:

for (std::string const& s : o.GetStringIterator()) {
// ...
}

此循环不复制任何字符串。每个s都直接引用map存储的相应string

我将首先在 c++14 中回答这个问题。

下面是一个最小的映射迭代器:

template<class F, class It>
struct iterator_mapped {
decltype(auto) operator*() const {
return f(*it);
}
iterator_mapped( F f_in, It it_in ):
f(std::move(f_in)),
it(std::move(it_in))
{}
iterator_mapped( iterator_mapped const& ) = default;
iterator_mapped( iterator_mapped && ) = default;
iterator_mapped& operator=( iterator_mapped const& ) = default;
iterator_mapped& operator=( iterator_mapped && ) = default;
iterator_mapped& operator++() {
++it;
return *this;
}
iterator_mapped operator++(int) {
auto copy = *this;
++*this;
return copy;
}
friend bool operator==( iterator_mapped const& lhs, iterator_mapped const& rhs ) {
return lhs.it == rhs.it;
}
friend bool operator!=( iterator_mapped const& lhs, iterator_mapped const& rhs ) {
return !(lhs==rhs);
}
private:
F f;
It it;
};

从技术上讲,它不是迭代器,但它有资格进行for(:)循环。

template<class It>
struct range_t {
It b, e;
It begin() const { return b; }
It end() const { return e; }
};
template<class It>
range_t<It> range( It b, It e ) {
return {std::move(b), std::move(e)};
}

以上是可以for(:)迭代的绝对最小迭代器范围类型。

template<class F, class R>
auto map_range( F&& f, R& r ) {
using std::begin; using std::end;
auto b = begin(r);
auto e = end(r);
using it = iterator_mapped<std::decay_t<F>, decltype(b)>;
return range( it( f, b ), it( f, e ) );
}

请注意,R&不是R&&;在这里取右值表示r是危险的。

auto GetStringIterator() const
{
return map_range( [](auto&& pair)->decltype(auto){
return pair.second;
}, m_Items );
}

并完成。

将其转换为 c++11 是一种痛苦。 你必须折腾std::functions 来代替 lambda(或者编写执行任务的函数对象而不是 lambda(,用auto和尾随返回类型替换decltype(auto),为 lambda 提供确切类型的auto&&参数等。 你最终会得到大约 25%-50% 的代码,其中大部分是晦涩的类型追逐。

这基本上是boost::adaptors::map_values所做的,但这是手工滚动的,因此您可以了解它的工作原理并且没有 boost 依赖项。