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

Is it possible in C++ to do std::map<> "for element : container" iteration with named variables (eg, key and value) instead of .first and .second?

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

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

// 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 很好,但如果可能,最好避免使用 boost。

我得到的最接近的是

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

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

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

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

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

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

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

  3. 转换
  4. 编写一个 to_kv 转换器,它接受一个 std::pair<K, V> cv& 和 return 一个 struct kv_t { K cv& key; V cv& value; }

  5. 将 3 接 2 接 1 并命名为 as_kv。它需要一个范围的对,returns 一个范围的键值。

您最终得到的语法是:

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 显然只存储引用。可能有一个奇怪的迭代器 returning 临时对象的情况会破坏这个 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

有一些关于 first-class 元组(和因此对)的建议可能会给你类似的东西,但我不知道这些建议的状态。

如果进入 C++,您可能最终得到的语法如下所示:

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

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

所以 ->* 被用作来自 Haskell 的 operator bind (连同隐式元组解包),我们正在为它提供一个 lambda 来获取包含的数据在地图内,return无效。 (Haskell-esque) return 类型成为 void(无)上的映射,我将其省略为 void。

技术有问题:你失去了 break;continue;,这很糟糕。

一个不太受 hackkey Haskell 启发的变体会期望 lambda return 类似于 void | std::experimental::expected<break_t|continue_t, T>,如果 Tvoid return 没什么,如果 T 是一个元组类型 return 一个映射,如果 T 是一个映射加入 returned 映射类型。它还会根据 lambda 想要的内容(SFINAE 样式检测)解压或不解压包含的元组。

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

您可以编写一个 class 模板:

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_mapconst,那也行不通。您必须执行以下操作:

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

最接近它使用的东西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;
}

只是为了提供另一种方法几乎做你想做的事,我前段时间写了这篇文章以避免.first.second 在我的代码中:

auto pair2params = [](auto&& f)
{
    return [f](auto&& p) {
        f(p.first, p.second);
    };
};

现在你可以这样写(假设基于范围 for_each):

int main()
{
    auto values = map<int, string>{
        {0, "hello"},
        {1, "world!"}
    };

    for_each(values, pair2params([](int key, const string& value) {
        cout << key << ": " << value << "\n";
    });
}

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

我通常更喜欢 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";
}

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__)

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

 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,或者通过 continue 退出,如果当前迭代通过 break.
  5. 退出,则为 false
  6. 我们总是跳转到下面的 FOREACH_BODY 标签。
  7. 这是我们拦截控制流并检查 FOREACH_CONTINUE 标志以确定我们如何退出当前迭代的地方。
  8. 如果 FOREACH_CONTINUEfalse,我们知道我们是通过 break 退出的,所以我们跳转到 FOREACH_BREAK
  9. 否则,FOREACH_CONTINUEtrue,我们 break 跳出 for (;;) 循环,进入下一次迭代。
  10. (混乱中途)。
  11. 我们总是从(5)跳到这里。
  12. 设置 FOREACH_ONCE 仅用于执行 for 循环,该循环声明 KEY 恰好一次。
  13. 声明KEY.
  14. 正确转发元素。
  15. 使用FOREACH_ONCE确保这个循环恰好执行一次。
  16. 声明VALUE.
  17. 正确转发元素。
  18. 使用FOREACH_CONTINUE来确保这个循环恰好执行一次,并表明循环是否通过break退出。

注意:使用std::get 也允许支持从序列中出现的std::tuplestd::array。例如,std::vector<std::tuple<int, int>>

Ideone Demo

对于现代 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