在基于C ++ 11范围的for循环中查找元素的位置?


78

假设我有以下代码:

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

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


17
这不是基于范围的目的(嘿,是双关语吗?)
jrok 2012年

2
除非使用std::find或其他一些过大的功能,否则这在STL容器中是不可能的。您不能从包含的元素中得出迭代器。为什么不维护迭代器?
Eitan T 2012年

2
有两个原因。第一个是我要做的(在这种情况下)是看我是否在最后一个元素:),第二个是编译器必须维护一个,为什么我不能访问它?“ this”是一个范围由编译器维护的变量,为什么不在这里?或者提供一种替代(但仍很方便)的语法,就像javascript一样,设置一个变量,该变量会随着循环的进行而变化。for(auto&index:list)
Fred Finkle,2012年

1
@FredFinkle您实际上是正确的,有一个迭代器,但是使用基于范围的for循环时,它是编译器内部的名称,因此不能在您的代码中使用。因此,如果您真的想知道自己是否位于最后一个元素,则应使用for(;;)循环。
iFreilicht 2014年

Answers:


62

是的,您可以,只需按摩一下;)

诀窍是使用组合:您无需直接在容器上进行迭代,而是在整个过程中使用索引对其进行“压缩”。

专门的拉链代码:

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范围,但是我确实找到了一个perl :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无穷”开始并计入无穷大(或者,其类型的最大值...)的“假”范围。


这提供了大量有用的信息。谢谢。我会玩弄代码。如上所述,“索引”代码是我希望提供的语言。
Fred Finkle 2012年

3
如何counting_range(或boost::counting_iterator)+ boost::zip_iterator
ildjarn 2012年

@ildjarn:是的,Boost.Iterators具有构建块(似乎),但是没有相应的范围,这很烦人。
Matthieu M.

注意,你可以改变可能你Indexer也正确接受和保持右值参数,通过改变类型_container的值类型,如果原来的说法是一个右值和std::move/std::forward在参数中。
XEO

我对代码进行了编辑,以使其有望与右值一起使用,可惜我无法完全测试该代码,但它应该可以工作。有关说明,请参见此处。如果您不喜欢它,或者发现任何错误或其他错误,请随时回滚。:)
Xeo 2012年

30

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]); 

}

但这显然是一个坏习惯,因为它会使代码变得模糊并且使它更加脆弱(如果有人更改容器类型,使&操作员超载或将“ auto&”替换为“ auto”,它很容易中断。祝您调试愉快!)

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


6
是的,它在标准中说明。vector在C ++ 03arraystringC ++ 11中保证连续性。
Nicol Bolas 2012年

1
如果有人...容易使&操作员崩溃...重载操作员std::addressof。:-]
ildjarn 2012年

你是对的。因此,&过载证明版本为:int pos = addressof(elem)-addressof(list [0]); ....马修·米的迭代器包装是更好的方式:)
弗雷德里克Terrazzoni

不知道可以保证连续性。不想在这里使用它,但是很高兴知道。
Fred Finkle 2012年

5
为什么不使用std :: distance找出位置?
Michael van der Westhuizen

19

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


15

根据@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;
}

你可以试试看

它的工作原理很像提到的“理想世界解决方案”,语法漂亮,简洁明了。请注意,el在这种情况下,的类型类似于boost::foobar<const std::string&, int>,因此它在那里处理引用,并且不执行复制。它甚至是非常高效的: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;
}

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

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

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

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

更换&foostd::addressof(foo)是在安全方面的通用代码。


确实很优雅!
Matthieu M.

我添加了2个备选方案,并对生成的代码进行了godbolt比较,以确保完整性,并且还解决了OP(在注释中)检测最后一个元素的需求
Flamefire '18

10

如果您具有支持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的原因是因为autolambda函数中的参数。


1
std::function使用昂贵的类型擦除。为什么不使用,template<typename T, typename Callable> void for_enum(T& container, Callable op)这样就不必为擦除类型付费?
NathanOliver


4

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

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

1
(idx等于“维护单独的迭代器”)
user66081 '16

2

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

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
}

请注意(在不合理的否决之前)问题的作者是在检测容器的有序循环中的最后一个元素的情况下提出此问题的。因此,我认为没有理由进行比较,&elem并且&*std::prev(std::end(list))不起作用或不实用。我同意另一个答案,即基于迭代器的for更适合于此,但仍然如此。
2014年

int i=c.size();在循环和测试之前进行声明似乎更容易if(--i==0)
Marc Glisse 2014年

@MarcGlisse,int i代码只是一个例子。我将其删除以避免混淆。即使size在循环之前使用,您也将需要一个计数器。
alfC 2014年

2

Tobias Widlund写了一个不错的麻省理工学院许可的Python样式标头,仅枚举(尽管是C ++ 17):

的GitHub

博客文章

真的很好用:

std::vector<int> my_vector {1,3,3,7};

for(auto [i, my_element] : en::enumerate(my_vector))
{
    // do stuff
}

1

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

#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 !

1

如果要避免在索引变量位于循环本地的同时避免编写辅助函数,则可以将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;
    });
}

0

这是一个使用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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.