【问题标题】:How to find the index of current object in range-based for loop?如何在基于范围的for循环中找到当前对象的索引?
【发布时间】:2012-06-13 07:21:27
【问题描述】:

假设我有以下代码:

vector<int> list;
for(auto& elem:list) {
    int i = elem;
}

我可以在不维护单独的迭代器的情况下找到elem 在向量中的位置吗?

【问题讨论】:

  • 这不是基于范围的用途(嘿,这是双关语吗?)
  • 这在 STL 容器中是不可能的,除非使用 std::find 或其他一些过度杀伤功能。您不能从包含的元素中得出迭代器。为什么不维护一个迭代器?
  • 有两个原因。第一个是我想做的(在这种情况下)是看看我是否在最后一个元素:) 第二个是编译器必须维护一个,为什么我不能访问它? “this”是一个由编译器维护的变量,为什么不在这里?或者提供一种替代(但仍然方便)的语法,就像 javascript 一样,设置一个变量,该变量会随着您通过循环而改变。 for(auto& index:list)
  • @FredFinkle 您实际上是正确的,there is an iterator,但是当使用基于范围的for 循环时,它是编译器内部名称,因此不能在您的代码中使用。所以如果你真的想知道你是否在最后一个元素,你应该使用for(;;)循环。

标签: c++ iterator


【解决方案1】:

是的,你可以,只是需要一些按摩;)

诀窍是使用组合:不是直接迭代容器,而是沿途使用索引“压缩”它。

专业邮编:

template <typename T>
struct iterator_extractor { typedef typename T::iterator type; };

template <typename T>
struct iterator_extractor<T const> { typedef typename T::const_iterator type; };


template <typename T>
class Indexer {
public:
    class iterator {
        typedef typename iterator_extractor<T>::type inner_iterator;

        typedef typename std::iterator_traits<inner_iterator>::reference inner_reference;
    public:
        typedef std::pair<size_t, inner_reference> reference;

        iterator(inner_iterator it): _pos(0), _it(it) {}

        reference operator*() const { return reference(_pos, *_it); }

        iterator& operator++() { ++_pos; ++_it; return *this; }
        iterator operator++(int) { iterator tmp(*this); ++*this; return tmp; }

        bool operator==(iterator const& it) const { return _it == it._it; }
        bool operator!=(iterator const& it) const { return !(*this == it); }

    private:
        size_t _pos;
        inner_iterator _it;
    };

    Indexer(T& t): _container(t) {}

    iterator begin() const { return iterator(_container.begin()); }
    iterator end() const { return iterator(_container.end()); }

private:
    T& _container;
}; // class Indexer

template <typename T>
Indexer<T> index(T& t) { return Indexer<T>(t); }

并使用它:

#include <iostream>
#include <iterator>
#include <limits>
#include <vector>

// Zipper code here

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto p: index(v)) {
        std::cout << p.first << ": " << p.second << "\n";
    }
}

你可以在 ideone 看到它,虽然它缺乏 for-range 循环支持,所以它不太漂亮。

编辑:

只是记得我应该更频繁地检查 Boost.Range。不幸的是没有zip 范围,但我确实找到了一颗珍珠:boost::adaptors::indexed。但是,它需要访问迭代器才能提取索引。羞耻:x

否则counting_range 和通用zip 我相信它可以做一些有趣的事情......

在我想象的理想世界中:

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto tuple: zip(iota(0), v)) {
        std::cout << tuple.at<0>() << ": " << tuple.at<1>() << "\n";
    }
}

zip 自动创建一个视图作为引用元组的范围,iota(0) 只需创建一个从0 开始的“假”范围,并且只计入无穷大(或者说是其类型的最大值。 ..)。

【讨论】:

  • counting_range(或boost::counting_iterator)+boost::zip_iterator怎么样?
  • @ildjarn:是的,Boost.Iterators 有构建块(看起来),但是没有对应的范围,这很烦人。
  • @Xeo 您的版本适用于左值(确实如您所说,不会发生复制)。但是,对于右值,存在一些问题。我还没有发现它,但我明天会继续研究它。基本上,当我像这样for (auto x : index(std::vector&lt;int&gt;{2, 4, 6})) { ... } 使用index 时,我得到这个错误:error: no matching function for call to ‘Indexer&lt;std::vector&lt;int, std::allocator&lt;int&gt; &gt; &gt;::iterator::iterator(std::vector&lt;int, std::allocator&lt;int&gt; &gt;::const_iterator)’。我用的是g++-4.7。
  • @betabandido:是的,这就是为什么我还没有回滚并要求 Matthieu 和我一起在休息室讨论这个确切的问题。 beginendconst,如果原始参数是右值,_container 是值类型,也是 const,使得 _container.begin()_container.end() 返回 const_iterators 而不是通缉iterators。一种解决方案是将非const beginend 函数添加到Indexer
  • @Xeo:抱歉,我的时间似乎与你的略有不同。事实上,在这种情况下,我认为从beginend 中删除const 是正确的做法。
【解决方案2】:

jrok 是对的:基于范围的 for 循环不是为此目的而设计的。

但是,在您的情况下,可以使用指针算法来计算它,因为vector 连续存储其元素 (*)

vector<int> list;
for(auto& elem:list) { 
    int i = elem;
    int pos = &elem-&list[0]; // pos contains the position in the vector 

    // also a &-operator overload proof alternative (thanks to ildjarn) :
    // int pos = addressof(elem)-addressof(list[0]); 

}

但这显然是一种不好的做法,因为它会混淆代码并使其更加脆弱(如果有人更改容器类型、重载 &amp; 运算符或将 'auto&' 替换为 'auto',它很容易中断。祝你好运调试它!)

注意:在 C++03 中保证向量的连续性,在 C++11 标准中保证数组和字符串的连续性。

【讨论】:

  • 是的,标准中有说明。保证 C++03 中的 vector 和 C++11 中的 arraystring 的连续性。
  • "如果有人...重载 &amp; 运算符,它很容易中断" 这就是 std::addressof 的用途。 :-]
  • 你是对的。所以 &-overload 证明版本将是: int pos = addressof(elem)- addressof(list[0]); .... Matthieu M. 的迭代器包装器更好:)
  • 不知道连续性得到保证。不想在这里使用它,但很高兴知道。
  • 为什么不使用 std::distance 来计算位置?
【解决方案3】:

不,你不能(至少,不费力气)。如果您需要元素的位置,则不应使用基于范围的 for。请记住,它只是最常见情况的便利工具:为每个元素执行一些代码。在需要元素位置的不太常见的情况下,您必须使用不太方便的常规 for 循环。

【讨论】:

    【解决方案4】:

    根据@Matthieu 的回答,使用提到的boost::adaptors::indexed 有一个非常优雅的解决方案:

    std::vector<std::string> strings{10, "Hello"};
    int main(){
        strings[5] = "World";
        for(auto const& el: strings| boost::adaptors::indexed(0))
          std::cout << el.index() << ": " << el.value() << std::endl;
    }
    

    You can try it

    这很像上面提到的“理想世界解决方案”,语法漂亮且简洁。请注意,在这种情况下el 的类型类似于boost::foobar&lt;const std::string&amp;, int&gt;,因此它处理那里的引用并且不执行复制。它甚至非常高效:https://godbolt.org/g/e4LMnJ(代码相当于保留一个自己的计数器变量,它尽可能好)

    为了完整起见,替代方案:

    size_t i = 0;
    for(auto const& el: strings) {
      std::cout << i << ": " << el << std::endl;
      ++i;
    }
    

    或者使用向量的连续属性:

    for(auto const& el: strings) {
      size_t i = &el - &strings.front();
      std::cout << i << ": " << el << std::endl;
    }
    

    第一个生成与 boost 适配器版本相同的代码(最佳),最后一个是 1 条指令:https://godbolt.org/g/nEG8f9

    注意:如果你只想知道,如果你有你可以使用的最后一个元素:

    for(auto const& el: strings) {
      bool isLast = &el == &strings.back();
      std::cout << isLast << ": " << el << std::endl;
    }
    

    这适用于每个标准容器,但必须使用 auto&amp;/auto const&amp;(与上面相同),但无论如何还是建议这样做。根据输入,这也可能非常快(尤其是当编译器知道向量的大小时)

    &amp;foo 替换为std::addressof(foo) 以确保通用代码的安全。

    【讨论】:

    • 我添加了 2 个备选方案,并对生成的代码进行了完整性比较,还解决了 OP(在 cmets 中)检测最后一个元素的需求
    【解决方案5】:

    如果你有一个支持 C++14 的编译器,你可以用函数式的方式来做:

    #include <iostream>
    #include <string>
    #include <vector>
    #include <functional>
    
    template<typename T>
    void for_enum(T& container, std::function<void(int, typename T::value_type&)> op)
    {
        int idx = 0;
        for(auto& value : container)
            op(idx++, value);
    }
    
    int main()
    {
        std::vector<std::string> sv {"hi", "there"};
        for_enum(sv, [](auto i, auto v) {
            std::cout << i << " " << v << std::endl;
        });
    }
    

    适用于 clang 3.4 和 gcc 4.9(不适用于 4.8);两者都需要设置-std=c++1y。您需要 c++14 的原因是因为 lambda 函数中的 auto 参数。

    【讨论】:

    • std::function 使用昂贵的类型擦除。为什么不使用template&lt;typename T, typename Callable&gt; void for_enum(T&amp; container, Callable op) 这样您就不必为类型擦除付费?
    【解决方案6】:

    如果您坚持使用基于范围的 for 并了解索引,则维护索引非常简单,如下所示。 我认为基于范围的 for 循环没有更清洁/更简单的解决方案。但真的为什么不使用标准 for(;;) 呢?这可能会让你的意图和代码最清晰。

    vector<int> list;
    int idx = 0;
    for(auto& elem:list) {
        int i = elem;
        //TODO whatever made you want the idx
        ++idx;
    }
    

    【讨论】:

    • (idx 相当于“维护一个单独的迭代器”)
    【解决方案7】:

    有一种非常简单的方法可以做到这一点

    vector<int> list;
    for(auto& elem:list) {
        int i = (&elem-&*(list.begin()));
    }
    

    i 将是您所需的索引。

    这利用了C++ vectors are always contiguous这一事实。

    【讨论】:

      【解决方案8】:

      我从您的 cmets 中了解到,您想知道索引的一个原因是要知道该元素是否是序列中的第一个/最后一个。如果是这样,你可以这样做

      for(auto& elem:list) {
      //  loop code ...
          if(&elem == &*std::begin(list)){ ... special code for first element ... }
          if(&elem == &*std::prev(std::end(list))){ ... special code for last element ... }
      //  if(&elem == &*std::rbegin(list)){... (C++14 only) special code for last element ...}
      //  loop code ... 
      }
      

      编辑: 例如,这会打印一个跳过最后一个元素中的分隔符的容器。适用于我能想象到的大多数容器(包括数组),(在线演示http://coliru.stacked-crooked.com/a/9bdce059abd87f91):

      #include <iostream>
      #include <vector>
      #include <list>
      #include <set>
      using namespace std;
      
      template<class Container>
      void print(Container const& c){
        for(auto& x:c){
          std::cout << x; 
          if(&x != &*std::prev(std::end(c))) std::cout << ", "; // special code for last element
        }
        std::cout << std::endl;
      }
      
      int main() {
        std::vector<double> v{1.,2.,3.};
        print(v); // prints 1,2,3
        std::list<double> l{1.,2.,3.};
        print(l); // prints 1,2,3
        std::initializer_list<double> i{1.,2.,3.};
        print(i); // prints 1,2,3
        std::set<double> s{1.,2.,3.};
        print(s); // print 1,2,3
        double a[3] = {1.,2.,3.}; // works for C-arrays as well
        print(a); // print 1,2,3
      }
      

      【讨论】:

      • 请注意(在不合理的否决之前),问题的作者是在检测容器的 for-ranged 循环中的最后一个元素的上下文中提出这个问题的。为此,我认为没有理由比较 &amp;elem&amp;*std::prev(std::end(list)) 不起作用或不实用。我同意另一个答案,即基于迭代器的 for 更适合于此,但仍然如此。
      • 在循环之前声明int i=c.size();并测试if(--i==0)似乎更容易。
      • @MarcGlisse,int i 代码只是一个示例。我将删除它以避免混淆。即使你在循环之前使用size,你也需要一个计数器。
      【解决方案9】:

      这是一个使用 c++20 的非常漂亮的解决方案:

      #include <array>
      #include <iostream>
      #include <ranges>
      
      template<typename T>
      struct EnumeratedElement {
          std::size_t index;
          T& element;
      };
      
      auto enumerate(std::ranges::range auto& range) 
          -> std::ranges::view auto 
      {
          return range | std::views::transform(
              [i = std::size_t{}](auto& element) mutable {
                  return EnumeratedElement{i++, element};
              }
          );
      }
      
      auto main() -> int {
          auto const elements = std::array{3, 1, 4, 1, 5, 9, 2};
          for (auto const [index, element] : enumerate(elements)) {
              std::cout << "Element " << index << ": " << element << '\n';
          }
      }
      

      这里使用的主要特性是 c++20 范围、c++20 概念、c++11 可变 lambda、c++14 lambda 捕获初始化器和 c++17 结构化绑定。有关任何这些主题的信息,请参阅 cppreference.com。

      请注意,结构化绑定中的element 实际上是一个引用,而不是元素的副本(在这里并不重要)。这是因为 auto 周围的任何限定符只会影响从中提取字段的临时对象,而不影响字段本身。

      生成的代码与由此生成的代码相同(至少 gcc 10.2):

      #include <array>
      #include <iostream>
      #include <ranges>
      
      auto main() -> int {
          auto const elements = std::array{3, 1, 4, 1, 5, 9, 2};
          for (auto index = std::size_t{}; auto& element : elements) {
              std::cout << "Element " << index << ": " << element << '\n';
              index++;
          }
      }
      

      证明:https://godbolt.org/z/a5bfxz

      【讨论】:

      • 天啊,我长大的 C/C++ 发生了什么?这几乎是不可理解的。
      • C++98 与 C++20 不是同一种语言。只知道 C 的人无法理解 Rust。
      • 也许我用 C、C++03(以及最近的 C++11)编程太久了,但是这些 lambdas,新的晦涩的 auto main() -&gt; int 语法,使用 @987654327 进行类型推导@ 等等正在把曾经干净而美丽的语言变成鲁布·戈德堡的烂摊子。非常聪明,超级令人印象深刻......而且几乎难以理解。
      • 这是你习惯的问题。这对我来说更容易理解,因为这是我过去一年编写的代码。我已经选择了要使用的功能以及何时完全基于对安全性和实用性的推理。对我来说,这就像学习一门具有更好性能、安全性和简单性(抽象性)潜力的新语言。
      • 为什么在您的示例中向容器添加视图过滤器会导致输出索引变为135791113(而不是0123456)?即使是无所事事的过滤器也有这种效果。例如:enumerate(elements) | std::views::filter([](auto const &amp;) { return true; })
      【解决方案10】:

      Tobias Widlund 编写了一个很好的 MIT 许可的 Python 样式标头,仅枚举(虽然是 C++17):

      GitHub

      Blog Post

      真的很好用:

      std::vector<int> my_vector {1,3,3,7};
      
      for(auto [i, my_element] : en::enumerate(my_vector))
      {
          // do stuff
      }
      

      【讨论】:

      • 一个更流行(且功能丰富)的版本是CPP-Itertools
      【解决方案11】:

      如果您想避免在编写辅助函数的同时 循环本地的索引变量,您可以使用带有可变变量的 lambda。:

      int main() {
          std::vector<char> values = {'a', 'b', 'c'};
          std::for_each(begin(values), end(values), [i = size_t{}] (auto x) mutable {
              std::cout << i << ' ' << x << '\n';
              ++i;
          });
      }
      

      【讨论】:

        【解决方案12】:

        这是一个基于宏的解决方案,在简单性、编译时间和代码生成质量方面可能优于大多数其他解决方案:

        #include <iostream>
        
        #define fori(i, ...) if(size_t i = -1) for(__VA_ARGS__) if(i++, true)
        
        int main() {
            fori(i, auto const & x : {"hello", "world", "!"}) {
                std::cout << i << " " << x << std::endl;
            }
        }
        

        结果:

        $ g++ -o enumerate enumerate.cpp -std=c++11 && ./enumerate 
        0 hello
        1 world
        2 !
        

        【讨论】:

          猜你喜欢
          • 2013-02-15
          • 2014-02-08
          • 2022-06-26
          • 2016-10-31
          • 1970-01-01
          • 2013-01-04
          • 1970-01-01
          • 1970-01-01
          • 2014-12-06
          相关资源
          最近更新 更多