在C ++中可以做std :: map<> " for element:container"使用命名变量(例如,键和值)而不是.first和.second进行迭代?

时间:2016-04-07 15:46:21

标签: c++ c++11 dictionary iteration readability

我不确定要搜索什么。 我找到了Renaming first and second of a map iterator,但这并不是我想做的事情。

这就是我想做的事情[请参阅下面的无意义C ++代码]。有可能接近这个吗?否则将只需选择"适应"我认为迭代器是循环内的第一行。

// what I want to do:
std::map<int, std::string> my_map;
// ... populate my_map
for(auto key, auto & value: my_map){
    // do something with integer key and string value
}

C ++ 11很好,但如果可能,请避免提升。

我最接近的是

// TODO, can this be templated?
struct KeyVal{
    int & id;
    std::string & info;

    template <typename P>
    KeyVal(P & p)
        : id(p.first)
        , info(p.second)
    {
    }
};

//...
for ( KeyVal kv : my_map ){
    std::cout << kv.info;
}

但这意味着为每个地图编写一个适配器类:(

// slightly joke answer/"what could possibly go wrong?"
#define key first
#define value second

7 个答案:

答案 0 :(得分:17)

以下Barry启发的方法是编写一个范围适配器。

在没有boost或类似的库支持的情况下执行此操作很痛苦,但是:

  1. 编写范围模板。它存储了2 class iterator个,并有begin()end()方法(以及您想要的任何其他方法)。

  2. 编写转换迭代器适配器。它需要一个迭代器,并将其包装起来,以便它的值类型由一些函数对象F转换。

  3. 撰写一个to_kv变换器,其中std::pair<K, V> cv&并返回struct kv_t { K cv& key; V cv& value; }

  4. 将3连接到2到1并将其称为as_kv。它需要一系列对,并返回一系列键值。

  5. 您最终得到的语法是:

    std::map<int, std::string> m;
    
    for (auto kv : as_kv(m)) {
      std::cout << kv.key << "->" << kv.value << "\n";
    }
    

    这很好。

    这是一个极简主义的解决方案,它实际上并没有创建合法的迭代器,但支持for(:)

    template<class Key, class Value>
    struct kv_t {
      Key&& key;
      Value&& value;
    };
    
    // not a true iterator, but good enough for for(:)
    template<class Key, class Value, class It>
    struct kv_adapter {
      It it;
      void operator++(){ ++it; }
      kv_t<Key const, Value> operator*() {
        return {it->first, it->second};
      }
      friend bool operator!=(kv_adapter const& lhs, kv_adapter const& rhs) {
        return lhs.it != rhs.it;
      }
    };
    template<class It, class Container>
    struct range_trick_t {
      Container container;
      range_trick_t(Container&&c):
        container(std::forward<Container>(c))
      {}
      It begin() { return {container.begin()}; }
      It end() { return {container.end()}; }
    };
    template<class Map>
    auto as_kv( Map&& m ) {
      using std::begin;
      using iterator = decltype(begin(m)); // no extra (())s
      using key_type = decltype((begin(m)->first)); // extra (())s on purpose
      using mapped_type = decltype((begin(m)->second)); // extra (())s on purpose
      using R=range_trick_t<
        kv_adapter<key_type, mapped_type, iterator>,
        Map
      >;
      return R{std::forward<Map>(m)};
    }
    std::map<int, std::string> m() { return {{0, "Hello"}, {2, "World"}}; }
    

    非常最小,但有效。我通常不会鼓励这种用于for(:)循环的半迭代伪迭代器;使用真实的迭代器只是一个适度的额外成本,并不会让人们后来感到惊讶。

    live example

    (现在支持临时地图。不支持平面C阵列......)

    范围技巧存储容器(可能是引用),以便将临时容器复制到for(:)循环期间存储的对象中。 Container类型的非临时容器是某种类型的Foo&,因此它不会制作冗余副本。

    另一方面,kv_t显然只存储引用。可能会有一个奇怪的迭代器返回临时数据来破坏这个kv_t实现,但我不确定如何在不牺牲性能的情况下避免使用它。

    如果你不喜欢上面的kv.部分,我们可以做一些解决方案,但它们并不干净。

    template<class Map>
    struct for_map_t {
      Map&& loop;
      template<class F>
      void operator->*(F&& f)&&{
        for (auto&& x:loop) {
          f( decltype(x)(x).first, decltype(x)(x).second );
        }
      }
    };
    template<class Map>
    for_map_t<Map> map_for( Map&& map ) { return {std::forward<Map>(map)}; }
    

    然后:

    map_for(m)->*[&](auto key, auto& value) {
      std::cout << key << (value += " ") << '\n';
    };
    

    足够接近?

    live example

    有一些关于一流元组(以及因此对)的建议可能会给你这样的东西,但我不知道提案的状态。

    如果进入C ++,你可能会得到的语法如下所示:

    for( auto&& [key, value] : container )
    

    对上述->*憎恶的评论:

    因此,->*被用作来自Haskell的operator bind(与隐式元组解包一起),我们正在为它提供一个lambda,它获取地图中包含的数据并返回void 。 (Haskell-esque)返回类型变成了void(无)的映射,我将其视为无效。

    该技术存在一个问题:您丢失了break;continue;

    一个不那么hackey Haskell启发的变体会期望lambda返回类似void | std::experimental::expected<break_t|continue_t, T>的内容,如果Tvoid则返回任何内容,如果T是元组 - type返回一个map,如果T是map,则加入返回的map-type。根据lambda想要的内容(SFINAE风格的检测),它也可以解包或不解包所包含的元组。

    但对于答案而言,这有点多了;这个题外话指出,上述编程风格并不是一个完整的死胡同。然而,它在C ++中是非常规的。

答案 1 :(得分:6)

您可以编写一个类模板:

template <class K, class T>
struct MapElem {
    K const& key;
    T& value;

    MapElem(std::pair<K const, T>& pair)
        : key(pair.first)
        , value(pair.second)
    { }
};

具有能够编写keyvalue的优点,但缺点是必须指定类型:

for ( MapElem<int, std::string> kv : my_map ){
    std::cout << kv.key << " --> " << kv.value;
}

如果my_map也是const,这将无效。你必须做类似的事情:

template <class K, class T>
struct MapElem {
    K const& key;
    T& value;

    MapElem(std::pair<K const, T>& pair)
        : key(pair.first)
        , value(pair.second)
    { }

    MapElem(const std::pair<K const, std::remove_const_t<T>>& pair)
        : key(pair.first)
        , value(pair.second)
    { }
};

for ( MapElem<int, const std::string> kv : my_map ){
    std::cout << kv.key << " --> " << kv.value;
}

这是一团糟。现在最好的事情就是习惯于写.first.second,并希望结构化绑定提案通过,这将允许你真正想要的东西:

for (auto&& [key, value] : my_map) {
    std::cout << key << " --> " << value;
}

答案 2 :(得分:3)

最接近使用的std::tie

std::map<int, std::string> my_map;
int key;
std::string value;
for(auto&& p: my_map)
{
    std::tie(key, value) = p;
    std::cout << key << ": " << value << std::endl;
}

当然,表达式不能放在for range循环中,所以可以使用宏来表示表达式:

#define FOREACH(var, cont) \
    for(auto && _p:cont) \
        if(bool _done = false) {} \
        else for(var = std::forward<decltype(_p)>(_p); !_done; _done = true)

那么std::tie可以直接在循环中使用:

std::map<int, std::string> my_map;
int key;
std::string value;
FOREACH(std::tie(key, value), my_map)
{
    std::cout << key << ": " << value << std::endl;
}

答案 3 :(得分:3)

使用现代c ++ 17,现在可以使用structured bindings

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

using namespace std;

int main() {
    map<int, string> my_map;

    my_map[0] = "hello";
    my_map[1] = "world";

    for (auto&& [key, value] : my_map) {
        cout << key << "," << value << "\n";
    }

    return 0;
}

构建它:

$ clang++ -std=c++17 test.cpp -o program

输出:

$ ./program
0,hello
1,world

答案 4 :(得分:1)

为了提供几乎做你想要的另一种方式,我前一段时间写了这篇文章,以避免我的代码中出现.first

.second

现在你可以写一些像(假设基于范围的auto pair2params = [](auto&& f) { return [f](auto&& p) { f(p.first, p.second); }; }; ):

for_each

正在运行示例:http://ideone.com/Bs9Ctm

答案 5 :(得分:1)

我通常更喜欢KISS方法:

template<typename KeyValuePair>
typename KeyValuePair::first_type& key(KeyValuePair& kvp)
{
    return kvp.first;
}

template<typename KeyValuePair>
const typename KeyValuePair::first_type& key(const KeyValuePair& kvp)
{
    return kvp.first;
}

template<typename KeyValuePair>
void key(const KeyValuePair&& kvp) = delete;

template<typename KeyValuePair>
typename KeyValuePair::second_type& value(KeyValuePair& kvp)
{
    return kvp.second;
}

template<typename KeyValuePair>
const typename KeyValuePair::second_type& value(const KeyValuePair& kvp)
{
    return kvp.second;
}

template<typename KeyValuePair>
void value(const KeyValuePair&& kvp) = delete;

使用如下的用法示例:

for(auto& kvp : my_map) {
    std::cout << key(kvp) << " " << value(kvp) << "\n";
}

答案 6 :(得分:1)

Apache Mesos中,我们使用名为foreachpair的宏,可以像这样使用:

foreachpair (const Key& key, const Value& value, elems) {
  /* ... */
}

您当然可以将KeyValue替换为auto,以及您希望在那里使用的任何限定符。它还支持breakcontinue

我的最新实现如下:

#define FOREACH_PREFIX   BOOST_PP_CAT(foreach_, __LINE__)

#define FOREACH_BODY     BOOST_PP_CAT(FOREACH_PREFIX, _body__)
#define FOREACH_BREAK    BOOST_PP_CAT(FOREACH_PREFIX, _break__)
#define FOREACH_CONTINUE BOOST_PP_CAT(FOREACH_PREFIX, _continue__)
#define FOREACH_ELEM     BOOST_PP_CAT(FOREACH_PREFIX, _elem__)
#define FOREACH_ONCE     BOOST_PP_CAT(FOREACH_PREFIX, _once__)

上面的宏通过包含foreachpair数字为__LINE__宏中使用的各种组件提供了唯一的名称。

 1 #define foreachpair(KEY, VALUE, ELEMS)                                     \
 2   for (auto&& FOREACH_ELEM : ELEMS)                                        \
 3     if (false) FOREACH_BREAK: break; /* set up the break path */           \
 4     else if (bool FOREACH_CONTINUE = false) {} /* var decl */              \
 5     else if (true) goto FOREACH_BODY; /* skip the loop exit checks */      \
 6     else for (;;) /* determine whether we should break or continue. */     \
 7       if (!FOREACH_CONTINUE) goto FOREACH_BREAK; /* break */               \
 8       else if (true) break; /* continue */                                 \
 9       else                                                                 \
10         FOREACH_BODY:                                                      \
11         if (bool FOREACH_ONCE = false) {} /* var decl */                   \
12         else for (KEY = std::get<0>(                                       \
13                       std::forward<decltype(FOREACH_ELEM)>(FOREACH_ELEM)); \
14                   !FOREACH_ONCE; FOREACH_ONCE = true)                      \
15           for (VALUE = std::get<1>(                                        \
16                    std::forward<decltype(FOREACH_ELEM)>(FOREACH_ELEM));    \
17                !FOREACH_CONTINUE; FOREACH_CONTINUE = true)

我将逐行完成这一行。

  1. (乱七八糟的开始)。
  2. 基于范围的for循环迭代ELEMS
  3. 我们设置了标签 FOREACH_BREAK。我们跳转到此循环中的break标签。
  4. 我们设置了控制流标志 FOREACH_CONTINUE。如果当前迭代正常,或通过true退出,则continuefalse,如果当前迭代通过break退出,则为FOREACH_BODY
  5. 我们总是跳转到下面的FOREACH_CONTINUE标签。
  6. 这是我们拦截控制流并检查FOREACH_CONTINUE标志以确定我们如何退出当前迭代的地方。
  7. 如果falsebreak,我们知道我们是通过FOREACH_BREAK退出的,因此我们跳转到FOREACH_CONTINUE
  8. 否则,truebreak,我们for (;;)退出FOREACH_ONCE循环,将我们带到下一次迭代。
  9. (混乱的一半)。
  10. 我们总是从(5)跳到这里。
  11. 设置for,它仅用于执行KEY循环,只声明KEY一次。
  12. 声明FOREACH_ONCE
  13. 正确转发元素。
  14. 使用VALUE确保此循环只执行一次。
  15. 声明FOREACH_CONTINUE
  16. 正确转发元素。
  17. 使用break确保此循环仅执行一次,并指示循环是否已通过std::get退出。
  18. 注意:使用std::tuple也可以支持来自序列的std::arraystd::vector<std::tuple<int, int>>。例如,var files = DriveApp.getFiles(); while (files.hasNext()) { var file = files.next(); }

    Ideone Demo