Знайти позицію елемента в циклі на основі діапазону С ++ 11?


79

Припустимо, у мене є такий код:

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

Чи можу я знайти положення elemвектора у векторі, не підтримуючи окремого ітератора?


17
Це не те, що залежить від діапазону, для (хе, це каламбур?)
jrok

2
Це неможливо в контейнерах STL, за винятком випадків, коли використовується std::findякась інша функція надмірного використання. Ви не можете укласти ітератори з вміщених елементів. Чому б не підтримувати ітератор?
Eitan T

2
З двох причин. Перше - це все, що я хочу зробити (в даному випадку) - це побачити, чи перебуваю я в останньому елементі чи ні :), а друге - це те, що компілятор повинен підтримувати його, чому я не можу отримати до нього доступ? "this" - це змінна з обсягом, яку підтримує компілятор, чому б не тут? Або надайте альтернативний (але все ще зручний) синтаксис, який, як це робить javascript, встановлює змінну, яка змінюється під час проходження циклу. для (auto & index: list)
Fred Finkle

1
@FredFinkle ви насправді правильні, є ітератор , але при використанні forциклу на основі діапазону це внутрішнє ім'я компілятора, і тому його не можна використовувати у вашому коді. Отже, якщо ви дійсно хочете знати, чи перебуваєте ви на останньому елементі, вам слід скористатися for(;;)циклом.
iFreilicht

Відповіді:


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 , хоча йому не вистачає підтримки циклу для діапазону, тому він менш гарний.

РЕДАГУВАТИ:

Просто згадав, що мені слід частіше перевіряти Boost.Range. На жаль, немає zipдіапазону, але я знайшов perl:boost::adaptors::indexed . Однак для витягування індексу потрібен доступ до ітератора. Сором: х

В іншому випадку з 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і просто враховується до нескінченності (або, максимум, свого типу ...).


1
Це забезпечує безліч корисної інформації. Дякую. Я пограю з кодом. Як я вже згадував вище, "індексний" код - це те, що я хотів би, щоб мова надавала.
Fred Finkle

3
Як щодо counting_range(або boost::counting_iterator) + boost::zip_iterator?
ildjarn

@ildjarn: Так, Boost.Iterators має будівельні блоки (здається), проте немає відповідного діапазону, що дратує.
Матьє М.

Зверніть увагу, що ви могли б змінити свої, Indexerщоб також належним чином приймати та утримувати аргументи rvalue, змінивши тип на тип _containerзначення, якщо вихідний аргумент є значенням r std::move/ і / std::forward- аргументом у.
Xeo

Я відредагував код, щоб він сподівався працювати з rvalues, на жаль, я не можу повністю перевірити код, але він повинен працювати. Пояснення див. Тут . Якщо вам це не подобається або ви виявили помилки чи щось інше, сміливо відкочуйтесь :)
Xeo

30

jrok має рацію: цикли на основі діапазону не призначені для цієї мети.

Однак у вашому випадку це можна обчислити за допомогою арифметики покажчика, оскільки 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 ++ 03, так arrayі stringв C ++ 11.
Nicol Bolas,

1
" це легко ламається, якщо хтось ... перевантажує &оператора " Ось для чого std::addressof. : -]
ildjarn

Ти маєш рацію. Отже, версія для перевірки & -loadload буде такою: int pos = addressof (elem) - addressof (list [0]); .... Обгортка ітератора Matthieu M. набагато краща :)
Frédéric Terrazzoni

Не знав, що суміжність гарантована. Не хотів би використовувати його тут, але добре знати.
Fred Finkle

5
Чому б не використати std :: distance для з’ясування позиції?
Michael van der Westhuizen

19

Ні, не можна (принаймні, не без зусиль). Якщо вам потрібно розташування елемента, не слід використовувати для діапазону. Пам’ятайте, що це найзручніший інструмент для найпоширенішого випадку: виконайте певний код для кожного елемента. У менш поширених обставинах, коли вам потрібно розташування елемента, вам доведеться використовувати менш зручний регулярний forцикл.


15

На основі відповіді від @Matthieu є дуже елегантне рішення із використанням згаданого boost :: adapters :: 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;
}

Перший генерує той самий код, що і версія адаптера підсилення (оптимальний), а останній довший на 1 інструкцію: https://godbolt.org/g/nEG8f9

Примітка: Якщо ви хочете лише знати, якщо у вас є останній елемент, ви можете використовувати:

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

Це працює для кожного стандартного контейнера, але auto&/ auto const&повинен бути використаний (так само, як вище), але це все одно рекомендується. Залежно від вводу це також може бути досить швидким (особливо, коли компілятор знає розмір вашого вектора)

Замініть &fooby, std::addressof(foo)щоб бути в безпеці для загального коду.


Це справді елегантно!
Matthieu M.

Я додав 2 альтернативи з порівнянням godbolt згенерованого коду для повноти, а також звернувся до потреби OP (у коментарях) для виявлення останнього елемента
Flamefire

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, полягає у autoпараметрах у лямбда-функції.


2
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

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

EDIT: Наприклад, це друкує контейнер, який пропускає роздільник в останньому елементі. Працює для більшості контейнерів, які я можу собі уявити (включаючи масиви), (онлайн-демонстрація 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))не буде працювати або бути практичним. Я погоджуюсь з іншою відповіддю, що для цього більше підходить ітератор, але все ж.
alfC

Здається, простіше оголосити int i=c.size();перед циклом і тестом if(--i==0).
Марк Глісс,

@MarcGlisse, int iкод був лише прикладом. Я видалю його, щоб уникнути плутанини. Навіть якщо ви використовуєте sizeдо циклу, вам знадобиться лічильник.
alfC

2

Тобіас Відлунд написав приємний ліцензований MIT заголовок стилю 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

Якщо ви хочете уникнути необхідності писати допоміжну функцію, маючи при цьому змінну індексу, локальну до циклу, ви можете використовувати лямбда з змінною змінною .:

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, ініціалізатори захоплення лямбда-зондів c ++ 14 та структуровані прив'язки 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.