【问题标题】:range-based for loop for private map values私有地图值的基于范围的 for 循环
【发布时间】:2018-12-14 01:12:07
【问题描述】:

我有以下代码:

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

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

【问题讨论】:

  • 是否使用 Boost 选项?如果是这样,可以使用 Boost.Range 及其boost::iterator_range
  • 请注意,在 C++ 中,“迭代器”是一个范围内的 位置。我将函数命名为GetStrings
  • 这完全是想多了。 RETURNTYPE 应该只是 auto &amp;IMPLEMENTATION 应该是 return m_items;。任务完成。
  • @SamVarshavchik 应该吗?这能实现吗?有没有可能你没有考虑到这个问题?如果 OP 要求迭代这些值,因为......也许...... OP 只想迭代这些值怎么办?

标签: c++ c++11 for-loop generator


【解决方案1】:

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

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

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&amp; 不是R&amp;&amp;;在这里为r 取一个右值是危险的。

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

完成了。

将其转换为 很痛苦。您必须折腾 std::functions 来代替 lambda(或编写执行任务的函数对象而不是 lambda),将 decltype(auto) 替换为 auto 和尾随返回类型,给出 auto&amp;&amp; 参数的确切类型到 lambdas 等。你最终会得到大约 25%-50% 的代码,其中大部分是模糊的类型追逐。

这基本上是 boost::adaptors::map_values 所做的,但这是手工制作的,因此您可以了解它是如何工作的,并且不依赖于 boost。

【讨论】:

    【解决方案2】:

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

    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
    }
    

    请注意,如果地图是一个实现细节,则应使用@Barry 提出的答案,因为它只迭代地图的值,而不是键。

    【讨论】:

    • 我觉得这并不能真正回答这个问题。 OP 要求一种方法来迭代地图的值。此解决方案遍历整个地图。也许 OP 对此感到满意,但也许地图只是一个实现细节?
    • 确实不是完整的解决方案,但你回答了我的另一个问题,所以还是谢谢你:)
    • 哪些版本的 C++ 允许在 for 循环中使用 [key,value] 语法? VS2015 似乎不支持它。
    • @jkokorian 这是 c++17
    【解决方案3】:

    您可以使用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;
    }
    

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

    任何一种解决方案都会在地图的值上返回一个视图。这是一个不拥有任何底层元素并且复制成本低的对象 - 即 O(1)。我们不是在复制地图,或者它的任何底层元素。

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

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

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

    【讨论】:

    • 在 C++ 14 中,您甚至可以在 GetStringIterator 声明中删除返回类型。
    猜你喜欢
    • 2016-10-31
    • 2021-08-25
    • 1970-01-01
    • 2015-11-03
    • 2014-12-06
    • 2015-10-26
    • 2014-01-12
    • 2013-01-04
    相关资源
    最近更新 更多