Как заставить мой пользовательский тип работать с «петлями на основе диапазона»?

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()?

Короче говоря, каковы руководящие принципы, чтобы сделать это?

ereOn
источник
Это возможно либо путем определения члена 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&&переменной и передают его вам как lvalue. Вы не можете определить, выполняете ли вы итерацию по временному (или другому значению); такая перегрузка не будет вызвана 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'», когда ==с 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функций, чем в обычном коде. Возможно, тогда они могли бы быть очень специализированными, чтобы вести себя по-другому (то есть быстрее, игнорируя аргумент end, чтобы получить максимально возможную оптимизацию). Но я недостаточно хорош с пространствами имен, чтобы быть уверенным, как это сделать.
Аарон МакДейд
@AaronMcDaid не очень практично. Вы легко получите неожиданные результаты, потому что некоторые средства вызова begin / end будут в конечном итоге основаны на диапазоне для начала / конца, а другие - нет. Безобидные изменения (со стороны клиента) приведут к изменению поведения.
Якк - Адам Невраумонт
1
Вам не нужно begin(X&&). Временная задержка в воздухе 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);
csjpeter
источник
2
В примере есть методы begin () и end (), а также имеется базовый (простой для понимания) пример класса итератора, который можно легко настроить для любого пользовательского типа контейнера. Сравнение std :: array <> и любой возможной альтернативной реализации - это другой вопрос, и, на мой взгляд, не имеет ничего общего с циклическим циклом for.
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()соответственно;

- иначе, begin-expr и end-expr находятся begin(__range)и end(__range), соответственно, где begin и end ищутся с помощью аргумент-зависимого поиска (3.4.2). Для целей этого поиска имени пространство имен std является связанным пространством имен.

Итак, вы можете сделать любое из следующего:

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

std::beginв begin()любом случае вызывает функцию-член, поэтому, если вы реализуете только одно из вышеперечисленных, результаты должны быть одинаковыми, независимо от того, какой вы выберете. Это те же результаты для циклических циклов for, а также тот же результат для простого смертного кода, который не имеет своих собственных правил разрешения магических имен, поэтому просто 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 .
TC
34

Должен ли я просто специализироваться на begin () и end ()?

Насколько я знаю, этого достаточно. Вы также должны убедиться, что увеличение указателя дойдет от начала до конца.

Следующий пример (отсутствует константная версия начала и конца) компилируется и работает нормально.

#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;
    }
}

Вот еще один пример с началом / концом в качестве функции. Они должны быть в том же пространстве имен, что и класс, из-за 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;
    }
}
BЈовић
источник
1
@ereOn В том же пространстве имен, где определен класс. Смотрите второй пример
BЈовић
2
Также поздравляем :) Возможно, стоит упомянуть термины Argument Dependent Lookup (ADL) или Koenig Lookup для второго примера (чтобы объяснить, почему функция free должна находиться в том же пространстве имен, что и класс, с которым она работает).
Матье М.
1
@ereOn: на самом деле, вы не делаете. ADL - это расширение областей поиска для автоматического включения пространств имен, к которым принадлежат аргументы. Есть хорошая статья ACCU о разрешении перегрузки, которая, к сожалению, пропускает часть поиска имени. Поиск имени включает в себя функцию сбора кандидатов, вы начинаете с поиска в текущей области + области действия аргументов. Если не найдено ни одного совпадения, вы переходите к родительской области текущей области и снова выполняете поиск ... пока не достигнете глобальной области.
Матье М.
1
@ BЈовић извините, но по какой причине в функции end () вы возвращаете опасный указатель? Я знаю, что это работает, но я хочу понять логику этого. Конец массива - это 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 :)
Спасибо.

RajibTheKing
источник
почему бы не выделить один дополнительный элемент, чтобы избежать разыменования недопустимой памяти в вашем методе end?
AndersK
@Anders Потому что почти все конечные итераторы указывают после конца своей содержащей структуры. end()Сама функция , очевидно , не разыменовывает неподходящее место памяти, так как он принимает только «Адреса» этой ячейка памяти. Добавление дополнительного элемента будет означать, что вам потребуется больше памяти, и использование your_iterator::end()любого способа, который будет разыменовывать это значение, не будет работать с любыми другими итераторами, так как они построены одинаково.
Qqwy
@Qqwy его метод end de-refences - return &data[sizeofarray]IMHO, он должен просто возвращать адресные данные + sizeofarray, но что я знаю,
AndersK
@ Андерс, ты прав. Спасибо, что держали меня в тонусе :-). Да, data + sizeofarrayбыл бы правильный способ написать это.
Qqwy
1

Ответ Криса Редфорда также работает для контейнеров Qt (конечно). Вот адаптация (обратите внимание, я возвращаю 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(); }
};
user2366975
источник
0

Я хотел бы разработать некоторые части ответа @Steve Jessop, для которых я сначала не понял. Надеюсь, поможет.

std::beginв begin()любом случае вызывает функцию-член, поэтому, если вы реализуете только одно из вышеперечисленных, результаты должны быть одинаковыми, независимо от того, какой вы выберете. Это те же результаты для циклических циклов for, а также тот же результат для простого смертного кода, который не имеет своих собственных правил разрешения магических имен, поэтому просто 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.
//      }
}
стог
источник