Знайти позицію елемента в циклі на основі діапазону С ++ 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.