Як змусити мій тип користування працювати з "діапазоном на основі циклів"?


252

Як і багато людей, в наші дні я пробую різні функції, які приносить C ++ 11. Один з моїх улюблених - це "діапазон для петель".

Я розумію, що:

for(Type& v : a) { ... }

Еквівалентний:

for(auto iv = begin(a); iv != end(a); ++iv)
{
  Type& v = *iv;
  ...
}

А це begin()просто повертається a.begin()для стандартних контейнерів.

Але що робити, якщо я хочу зробити свій власний тип типу "діапазон на основі циклу" ?

Чи варто просто спеціалізуватися begin()і end()?

Якщо мій тип власності належить до простору імен xml, чи слід визначити xml::begin()або std::begin()?

Коротше кажучи, які вказівки для цього робити?


Це можливо або шляхом визначення члена begin/endабо друга, статичного або вільного begin/end. Тільки будьте обережні , в якому простір імен ви кладете вільну функцію: stackoverflow.com/questions/28242073 / ...
alfC

Може хто - небудь будь ласка , напишіть відповідь з прикладом діапазону значень з плаваючою точкою , яка не є контейнером: for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }. Мені цікаво, як ви обходите той факт, що `` оператор! = () `` Важко визначити. А як щодо дереференції ( *__begin) у цьому випадку? Я думаю , що це було б великим внеском , якщо хто - то показав нам , як що робиться!
BitTickler

Відповіді:


183

Стандарт був змінений з моменту, коли питання (і більшість відповідей) були розміщені у вирішенні цього звіту про дефекти .

Спосіб зробити for(:)циклічну роботу над вашим типом Xтепер є одним із двох способів:

  • Створіть член X::begin()і X::end()поверніть те, що діє як ітератор

  • Створіть вільну функцію begin(X&)і end(X&)поверніть щось, що діє як ітератор, у тому ж просторі імен, що і ваш тип X

І подібні для constваріацій. Це буде працювати як над компіляторами, які впроваджують зміни звіту про дефекти, так і компіляторами, які цього не роблять.

Повернені об’єкти насправді не повинні бути ітераторами. for(:)Петля, в відміну від більшої частини C ++ стандарту, як вказана для розширення до чого - то , еквівалентному :

for( range_declaration : range_expression )

стає:

{
  auto && __range = range_expression ;
  for (auto __begin = begin_expr,
            __end = end_expr;
            __begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

де змінні, що починаються з __, призначені лише для експозиції, begin_exprі end_exprє магією, яка викликає begin/ end

Вимоги до початкового / кінцевого значення повернення прості: Ви повинні перевантажити попередньо ++, переконатися, що вирази ініціалізації є дійсними, двійкові, !=які можна використовувати в булевому контексті, одинарні, *що повертають щось, з чим можна призначити-ініціалізувати range_declaration, та викрити загальнодоступне руйнівник.

Це робити таким чином, що не сумісний з ітератором, є, мабуть, поганою ідеєю, оскільки майбутні ітерації C ++ можуть бути відносно непристойними щодо порушення коду, якщо ви це зробите.

Що стосується відхилення, цілком ймовірно, що подальша редакція стандарту дозволить end_exprповернути інший тип begin_expr. Це корисно тим, що воно дозволяє оцінювати "ледачий кінець" (як виявлення нульового припинення), який легко оптимізувати, щоб бути таким же ефективним, як рукописний цикл C, та інші подібні переваги.


¹ Зауважте, що for(:)петлі зберігають будь-яке тимчасове у auto&&змінній і передають його вам як значення. Ви не можете визначити, чи повторюєтесь ви через тимчасове (або інше значення); така перевантаження не буде викликатися for(:)циклом. Див. [Stmt.ranged] 1.2-1.3 від n4527.

² Або зателефонуйте на begin/ endметод або пошук вільної функції begin/ лише для ADL end, або магію підтримки масиву в стилі C. Зверніть увагу, що std::beginне викликається, якщо не range_expressionповертає об'єкт типу namespace stdабо не залежить від нього.


В діапазон вираження оновлено

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

з типами __beginі __endбули роз'єднані.

Це дозволяє кінцевому ітератору не бути того ж типу, що і початковий. Вашим кінцевим ітератором може бути "дозорний", який підтримує лише !=тип ітератора початку.

Практичний приклад, чому це корисно, полягає в тому, що ваш кінцевий ітератор може прочитати "перевірити, char*чи не вказує він '0'", коли ==з a char*. Це дозволяє діапазону C ++ для вираження генерувати оптимальний код під час ітерації через char*буфер, що закінчується нулем .

struct null_sentinal_t {
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(Rhs const& ptr, null_sentinal_t) {
    return !*ptr;
  }
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
    return !(ptr==null_sentinal_t{});
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(null_sentinal_t, Lhs const& ptr) {
    return !*ptr;
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(null_sentinal_t, Lhs const& ptr) {
    return !(null_sentinal_t{}==ptr);
  }
  friend bool operator==(null_sentinal_t, null_sentinal_t) {
    return true;
  }
  friend bool operator!=(null_sentinal_t, null_sentinal_t) {
    return false;
  }
};

живий приклад у компіляторі без повної підтримки C ++ 17; forпетлю розширено вручну.


Якщо на основі діапазону використовується інший механізм пошуку, можливо, можливо організувати цей діапазон для отримання іншої пари beginта endфункцій, ніж це доступно у звичайному коді. Можливо, тоді вони могли б бути дуже спеціалізованими, щоб поводитись інакше (тобто швидше, ігноруючи кінцевий аргумент, щоб отримати максимальну оптимізацію.) Але я недостатньо хороший з просторами імен, щоб бути впевненим, як це зробити.
Аарон Макдейд

@AaronMcDaid не дуже практичний. Ви легко отримаєте дивовижні результати, тому що деякі засоби викликати початок / кінець закінчуються діапазоном для початку / кінця, а інші - ні. Нечесні зміни (з боку клієнта) отримали б зміни поведінки.
Якк - Адам Невраумон

1
Вам не потрібно begin(X&&). Тимчасовий призупиняється в midair auto&&в діапазоні, заснованому на, і beginзавжди викликається значенням lvalue ( __range).
ТК

2
Ця відповідь справді отримає користь із прикладу шаблону, який можна скопіювати та реалізувати.
Томаш Зато -

Я вважаю за краще наголосити на властивості типу ітератора (*, ++,! =). Я б попросив вас перефразувати цю відповідь, щоб покращити характеристики ітератора.
Red.Wave

62

Я пишу свою відповідь, тому що деякі люди можуть бути більше задоволені простим прикладом із реального життя, не включає STL.

У мене чомусь є власна реалізація масиву даних просто, і я хотів використовувати діапазон, заснований на циклі. Ось моє рішення:

 template <typename DataType>
 class PodArray {
 public:
   class iterator {
   public:
     iterator(DataType * ptr): ptr(ptr){}
     iterator operator++() { ++ptr; return *this; }
     bool operator!=(const iterator & other) const { return ptr != other.ptr; }
     const DataType& operator*() const { return *ptr; }
   private:
     DataType* ptr;
   };
 private:
   unsigned len;
   DataType *val;
 public:
   iterator begin() const { return iterator(val); }
   iterator end() const { return iterator(val + len); }

   // rest of the container definition not related to the question ...
 };

Тоді приклад використання:

PodArray<char> array;
// fill up array in some way
for(auto& c : array)
  printf("char: %c\n", c);

2
У прикладі є методи початку () і кінця (), а також є базовий (легкий для розуміння) приклад ітератор класу, який легко можна налаштувати для будь-якого користувальницького типу контейнера. Порівнювати std :: array <> та будь-яку можливу альтернативну реалізацію - це інше питання, і, на мою думку, не має нічого спільного з діапазоном для циклу на основі діапазону.
csjpeter

Це дуже стисла і практична відповідь! Це було саме те, що я шукав! Дякую!
Зак Тейлор

1
Чи було б доцільніше видалити const класифікатор повернення для користувача const DataType& operator*()та дозволити користувачеві вибрати, const auto&чи використовувати auto&? Спасибі все одно, чудова відповідь;)
Рік

53

Відповідна частина стандарту 6.5.4 / 1:

якщо _RangeT - тип класу, некваліфіковані ідентифікатори починаються та закінчуються в області класу _RangeT, як якщо б пошук доступу до класу (3.4.5), і якщо будь-яке (або обидва) знайде хоча б одне оголошення, почніть - expr і end-expr є __range.begin()і __range.end(), відповідно;

- в іншому випадку, start-expr і end-expr є, begin(__range)і end(__range)відповідно, де початок і кінець розглядаються залежно від аргументів пошуку (3.4.2). Для цілей пошуку цього імені std простір імен - це пов'язаний простір імен.

Отже, ви можете зробити будь-що з наступного:

  • визначити beginта endфункції члена
  • визначте beginта endвільні функції, які знайде ADL (спрощена версія: розмістіть їх у тому ж просторі імен, що й клас)
  • спеціалізуються std::beginіstd::end

std::beginв begin()будь-якому випадку викликає функцію члена, тож якщо ви реалізуєте лише одне з вищезазначених, результати повинні бути однаковими незалежно від того, який з них ви виберете. Це ті самі результати для діапазону на основі циклів, а також той самий результат для простого смертного коду, який не має власних магічних правил дозволу назви, тому просто using std::begin;слідує за некваліфікованим викликом до begin(a).

Якщо ви реалізуєте членські функції та функції ADL, тоді діапазон для циклів повинен викликати функції члена, тоді як прості смертні називатимуть функції ADL. Найкраще переконайтесь, що вони роблять те саме в цьому випадку!

Якщо річ, яку ви пишете, реалізує інтерфейс контейнера, тоді він буде мати begin() і end()функції члена, яких повинно бути достатньо. Якщо це діапазон, який не є контейнером (це було б хорошою ідеєю, якщо він незмінний або якщо ви не знаєте розміру наперед), ви можете вибрати.

З варіантів, які ви викладаєте, зауважте, що ви не повинні перевантажуватись std::begin(). Вам дозволяється спеціалізувати стандартні шаблони для визначеного користувачем типу, але окрім цього, додавання визначень до std простору імен є невизначеним поведінкою. Але як би там не було, спеціалізація стандартних функцій - це поганий вибір хоча б тому, що відсутність часткової спеціалізації означає, що ви можете це робити лише для одного класу, а не для шаблону класу.


Чи не є певні вимоги, яким ітератор сильно відповідає? тобто бути ForwardIterator або щось подібне.
Паббі

2
@Pubby: Дивлячись на 6.5.4, я думаю, що InputIterator є достатнім. Але насправді я не думаю, що тип, що повертається, взагалі не повинен бути ітератором для діапазону. Оператор визначається в стандарті тим, що йому еквівалентно, тому достатньо реалізувати лише вирази, використані в коді в стандарті: оператори !=, префікс ++та унар *. Мабуть, нерозумно виконувати begin()і end()функції членів, або функції, що не належать до ADL, які повертають що-небудь, крім ітератора, але я думаю, що це законно. std::beginЯ думаю, що спеціалізація повернення неітератора - це UB.
Стів Джессоп

Ви впевнені, що не повинні перевантажувати std :: begin? Я запитую, тому що стандартна бібліотека робить це в кількох випадках сама.
ThreeBit

@ThreeBit: так, я впевнений. Правила для стандартних реалізацій бібліотеки відрізняються від правил для програм.
Стів Джессоп

3
Це потрібно оновити для open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1442 .
ТК

34

Чи варто просто спеціалізуватися на початку () та закінченні ()?

Наскільки я знаю, цього достатньо. Ви також повинні переконатися, що приріст покажчика отримає від початку до кінця.

Наступний приклад (у ньому відсутня версія const початку та кінця) складається і працює чудово.

#include <iostream>
#include <algorithm>

int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }
    int * begin()
    {
        return &v[0];
    }
    int * end()
    {
        return &v[10];
    }

    int v[10];
};

int main()
{
    A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}

Ось ще один приклад функції "start / end" як функції. Вони повинні знаходитися в тому ж просторі імен, що і для класу, через ADL:

#include <iostream>
#include <algorithm>


namespace foo{
int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }

    int v[10];
};

int *begin( A &v )
{
    return &v.v[0];
}
int *end( A &v )
{
    return &v.v[10];
}
} // namespace foo

int main()
{
    foo::A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}

1
@ereOn У тому ж просторі імен, де визначено клас. Дивіться другий приклад
BЈович

2
Вітаємо також :) Можливо, варто згадати умови Argument Dependent Lookup (ADL) або Koenig Lookup для другого прикладу (щоб пояснити, чому вільна функція повинна знаходитися в тому ж просторі імен, що і клас, в якому вона працює).
Матьє М.

1
@ereOn: насправді ти цього не робиш. ADL збирається розширити сфери на пошук, щоб автоматично включати простори імен, до яких належать аргументи. Існує хороша стаття ACCU про роздільну здатність перевантаження, яка, на жаль, пропускає частину пошуку імені. Пошук імен включає в себе збір функції кандидатів, ви починаєте з перегляду поточного діапазону + сфери аргументів. Якщо не знайдено жодного імені, яке відповідає, ви переходите до вищої області поточного діапазону і знову шукаєте ..., поки не досягнете глобальної області.
Матьє М.

1
@ BЈовић вибачте, але з якої причини в результаті () ви повертаєте небезпечний покажчик? Я знаю, що це працює, але я хочу зрозуміти логіку цього. Кінець масиву v [9], чому б вам коли-небудь повертати v [10]?
gedamial

1
@gedamial Я згоден. Я думаю, що так і має бути return v + 10. &v[10]відміняє місце пам'яті біля повз масив.
Міллі Сміт

16

Якщо ви хочете створити резервну копію ітерації класу безпосередньо з його std::vectorабо std::mapчленом, ось код для цього:

#include <iostream>
using std::cout;
using std::endl;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <map>
using std::map;


/////////////////////////////////////////////////////
/// classes
/////////////////////////////////////////////////////

class VectorValues {
private:
    vector<int> v = vector<int>(10);

public:
    vector<int>::iterator begin(){
        return v.begin();
    }
    vector<int>::iterator end(){
        return v.end();
    }
    vector<int>::const_iterator begin() const {
        return v.begin();
    }
    vector<int>::const_iterator end() const {
        return v.end();
    }
};

class MapValues {
private:
    map<string,int> v;

public:
    map<string,int>::iterator begin(){
        return v.begin();
    }
    map<string,int>::iterator end(){
        return v.end();
    }
    map<string,int>::const_iterator begin() const {
        return v.begin();
    }
    map<string,int>::const_iterator end() const {
        return v.end();
    }

    const int& operator[](string key) const {
        return v.at(key);
    }
    int& operator[](string key) {
        return v[key];
    } 
};


/////////////////////////////////////////////////////
/// main
/////////////////////////////////////////////////////

int main() {
    // VectorValues
    VectorValues items;
    int i = 0;
    for(int& item : items) {
        item = i;
        i++;
    }
    for(int& item : items)
        cout << item << " ";
    cout << endl << endl;

    // MapValues
    MapValues m;
    m["a"] = 1;
    m["b"] = 2;
    m["c"] = 3;
    for(auto pair: m)
        cout << pair.first << " " << pair.second << endl;
}

2
Варто відзначити , що const_iteratorтакож можна отримати в auto(C ++ 11) -соместімим способом через cbegin, cendі т.д.
underscore_d

2

Тут я ділюся найпростішим прикладом створення користувацького типу, який буде працювати з " діапазоном на основі циклу ":

#include<iostream>
using namespace std;

template<typename T, int sizeOfArray>
class MyCustomType
{
private:
    T *data;
    int indx;
public:
    MyCustomType(){
        data = new T[sizeOfArray];
        indx = -1;
    }
    ~MyCustomType(){
        delete []data;
    }
    void addData(T newVal){
        data[++indx] = newVal;
    }

    //write definition for begin() and end()
    //these two method will be used for "ranged based loop idiom"
    T* begin(){
        return &data[0];
    }
    T* end(){
        return  &data[sizeOfArray];
    }
};
int main()
{
    MyCustomType<double, 2> numberList;
    numberList.addData(20.25);
    numberList.addData(50.12);
    for(auto val: numberList){
        cout<<val<<endl;
    }
    return 0;
}

Сподіваюся, це буде корисно для такого початківця розробника, як я: p :)
Дякую.


чому б не виділити один додатковий елемент, щоб уникнути перенаправлення недійсної пам'яті у вашому кінцевому методі?
AndersK

@Anders Тому що практично всі кінцеві точки до ітераторів після закінчення їх містить структури. Сама end()функція, очевидно, не переносить неправильне розташування пам’яті, оскільки вона займає лише «адресу» цього місця пам'яті. Додавання додаткового елемента означає, що вам знадобиться більше пам’яті, і використання your_iterator::end()будь-якого способу, який би відмежував це значення, так і не працював би з будь-якими іншими ітераторами, оскільки вони побудовані так само.
Qqwy

@Qqwy його кінцевий метод дезахистується - return &data[sizeofarray]IMHO він повинен просто повернути дані адреси + sizeofarray, але що я знаю,
AndersK

@Anders Ви маєте рацію. Дякую, що тримаєте мене гостро :-). Так, це data + sizeofarrayбув би правильний спосіб написати це.
Qqwy

1

Відповідь Кріса Редфорда також працює для контейнерів Qt (звичайно). Ось адаптація (зауважую, що я повертаю a constBegin()відповідно constEnd()з методів const_iterator):

class MyCustomClass{
    QList<MyCustomDatatype> data_;
public:    
    // ctors,dtor, methods here...

    QList<MyCustomDatatype>::iterator begin() { return data_.begin(); }
    QList<MyCustomDatatype>::iterator end() { return data_.end(); }
    QList<MyCustomDatatype>::const_iterator begin() const{ return data_.constBegin(); }
    QList<MyCustomDatatype>::const_iterator end() const{ return data_.constEnd(); }
};

0

Я хотів би розробити деякі частини відповіді @Steve Jessop, за якими спочатку я не розумів. Сподіваюся, це допомагає.

std::beginв begin()будь-якому випадку викликає функцію члена, тож якщо ви реалізуєте лише одне з вищезазначених, результати повинні бути однаковими незалежно від того, який з них ви виберете. Це ті самі результати для діапазону на основі циклів, а також той самий результат для простого смертного коду, який не має власних магічних правил дозволу назви, тому просто using std::begin;слідує за некваліфікованим викликом до begin(a).

Якщо ви реалізуєте членські функції та функції ADL , тоді діапазон для циклів повинен викликати функції члена, тоді як прості смертні називатимуть функції ADL. Найкраще переконайтесь, що вони роблять те саме в цьому випадку!


https://en.cppreference.com/w/cpp/language/range-for :

  • Якщо ...
  • Якщо range_expressionце вираз типу класу, Cякий має імені членаbegin і члена end(незалежно від типу або доступності такого члена), тоді begin_exprє __range.begin() і end_exprє__range.end() ;
  • Інакше begin_expr є begin(__range)і end_exprє end(__range), які знаходять за допомогою аргументованого пошуку (пошук не ADL не виконується).

Для циклу на основі діапазону спочатку вибираються функції члена.

Крім

using std::begin;
begin(instance);

Функції ADL вибираються спочатку.


Приклад:

#include <iostream>
#include <string>
using std::cout;
using std::endl;

namespace Foo{
    struct A{
        //member function version
        int* begin(){
            cout << "111";
            int* p = new int(3);  //leak I know, for simplicity
            return p;
        }
        int *end(){
            cout << "111";
            int* p = new int(4);
            return p;
        }
    };

    //ADL version

    int* begin(A a){
        cout << "222";
        int* p = new int(5);
        return p;
    }

    int* end(A a){
        cout << "222";
        int* p = new int(6);
        return p;
    }

}

int main(int argc, char *args[]){
//    Uncomment only one of two code sections below for each trial

//    Foo::A a;
//    using std::begin;
//    begin(a);  //ADL version are selected. If comment out ADL version, then member functions are called.


//      Foo::A a;
//      for(auto s: a){  //member functions are selected. If comment out member functions, then ADL are called.
//      }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.