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

Как и многие люди в эти дни, я пробовал различные функции, которые дает 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()?

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

10 ответов

Решение

Стандарт был изменен с тех пор, как вопрос (и большинство ответов) были опубликованы в решении этого отчета о дефектах.

Способ сделать 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 метод, или ADL-только поиск свободной функции begin/endили магия для поддержки массивов в стиле C. Обратите внимание, что std::begin не называется, если range_expression возвращает объект типа в namespace std или зависит от того же.


В C++17 выражение для диапазона было обновлено

{
  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 цикл вручную расширен.

Я пишу свой ответ, потому что некоторые люди могут быть более довольны простым примером из жизни без включенных 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);

Соответствующая часть стандарта - 6.5.4/1:

если _RangeT является типом класса, неполные идентификаторы начала и конца ищутся в области видимости класса _RangeT, как если бы путем поиска доступа к члену класса (3.4.5), и если любой (или оба) находит хотя бы одно объявление, начинаются - expr и end-expr __range.begin() а также __range.end() соответственно;

- иначе, begin-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, то циклы for, основанные на диапазоне, должны вызывать функции-члены, тогда как простые смертные будут вызывать функции ADL. Лучше убедитесь, что они делают то же самое в этом случае!

Если то, что вы пишете, реализует интерфейс контейнера, то оно будет иметь begin() а также end() функции-члены уже, что должно быть достаточно. Если это диапазон, который не является контейнером (что было бы неплохо, если бы он был неизменным или если вы не знаете размер заранее), вы можете выбирать.

Из опций, которые вы выкладываете, обратите внимание, что вы не должны перегружать std::begin(), Вам разрешено специализировать стандартные шаблоны для определенного пользователем типа, но помимо этого добавление определений в пространство имен std является неопределенным поведением. Но в любом случае, специализация стандартных функций - плохой выбор хотя бы потому, что отсутствие частичной специализации функций означает, что вы можете сделать это только для одного класса, а не для шаблона класса.

Должен ли я просто специализироваться на 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;
    }
}

Вот еще один пример с функциями begin / 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;
    }
}

В случае, если вы хотите поддержать итерацию класса напрямую 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;
}

Вдохновленный комментарием BitTickler о том, как заставить его работать с не-контейнерными типами, вот минимальный пример того, что работает для doubles:

class dranged {
    double start, stop, step, cur;
    int index;

public:
    dranged(double start, double stop, double step) :
        start(start), stop(stop), step(step),
        cur(start), index(0) {}

    auto begin() { return *this; }
    auto end() { return *this; }

    double operator*() const { return cur; }

    auto& operator++() {
        index += 1;
        cur = start + step * index;
        return *this;
    }

    bool operator!=(const dranged &rhs) const {
        return cur < rhs.stop;
    }
};

Обратите внимание, что использование < в != оператор поддерживает правильный инвариант, но, очевидно, предполагает stepявляется положительным и не будет подходить везде, где будет более общий диапазон. Я использовал целое число index чтобы предотвратить распространение ошибки с плавающей запятой, но в противном случае стремились к простоте.

Это можно использовать как:

double sum() {
    double accum = 0;
    for (auto val : dranged(0, 6.28, 0.1)) {
        accum += val;
    }
    return accum;
}

GCC и Clang создают очень разумный код при компиляции с оптимизацией (т. Е. Либо -Os или выше -O1 для GCC или -O2 для Clang).

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

#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:)
Благодарю вас.

Ответ Криса Редфорда также работает для контейнеров 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(); }
};

Я думаю, мне нечего объяснять, так как ответы уже делают это. Но я, возможно, должен привести эту цитату из стандарта (N4885):

[stmt.ranged]/1: (выделение мое)

Оператор for на основе диапазона

       for ( init-statement(opt) for-range-declaration :
      for-range-initializer 
    ) statement(possibly curly-braced)

эквивалентно:

          { // starts namespace scope of for-range-initializer

       init-statement; (opt)
       auto &&range = for-range-initializer ;
       auto begin = begin-expr ;
       auto end = end-expr ;
       for ( ; begin != end; ++begin ) 
       {
          for-range-declaration = * begin ;
          statement ;   
       }

    } // ends namespace scope of for-range-initializer

куда

(1.1) если for-range-initializer является выражением, оно рассматривается так, как если бы оно было заключено в круглые скобки (так что оператор-запятая не может быть интерпретирован как разграничивающий два init-декларатора);

(1.2) диапазон, начало и конец являются переменными, определенными только для описания; а также

(3.1) begin-expr и end-expr определяются следующим образом:

(1.3.1) если for-range-initializer является выражением типа массива R, то begin-expr и end-expr являются диапазоном и диапазоном+N соответственно, где N — граница массива. Если R представляет собой массив с неизвестной границей или массив неполного типа, программа имеет неверный формат;

(1.3.2) если for-range-initializer является выражением типа класса C, а [class.member.lookup] в области видимости C для имен begin и end каждого найти хотя бы одно объявление, begin-expr и end-expr — это range.begin() и range.end() соответственно;

(1.3.3) в противном случае begin-expr и end-expr являются begin(range) и end(range) соответственно, где begin и end подвергаются поиску, зависящему от аргумента ([basic.lookup.argdep]).


Обратите внимание, что строки, массивы и все контейнеры STL являются итерируемыми структурами данных, поэтому они уже могут быть итерированы с помощью цикла for на основе диапазона. Чтобы сделать структуру данных итерируемой, она должна работать аналогично существующим итераторам STL:

1- Должен быть beginа также endметоды, которые работают с этой структурой либо как члены, либо как автономные функции, и которые возвращают итераторы в начало и конец структуры.

2- Сам итератор должен поддерживать operator*()метод, operator !=()метод и operator++(void)либо как члены, либо как автономные функции.


      #include <iostream>
#include <vector>
#define print(me) std::cout << me << std::endl

template <class T>
struct iterator
{
    iterator(T* ptr) : m_ptr(ptr) {};
    bool operator!=(const iterator& end) const { return (m_ptr != end.m_ptr); }
    T operator*() const { return *m_ptr; }
    const iterator& operator++()
    {
        ++m_ptr;
        return *this;
    }

private:
    T* m_ptr;
};

template <class T, size_t N>
struct array
{
    typedef iterator<T> iterator;

    array(std::initializer_list<T> lst)
    {

        m_ptr = new T[N]{};
        std::copy(lst.begin(), lst.end(), m_ptr);
    };

    iterator begin() const { return iterator(m_ptr); }
    iterator end() const { return iterator(m_ptr + N); }

    ~array() { delete[] m_ptr; }

private:
    T* m_ptr;
};

int main()
{
    array<std::vector<std::string>, 2> str_vec{ {"First", "Second"}, {"Third", "Fourth"} };
    for(auto&& ref : str_vec)
        for (size_t i{}; i != ref.size(); i++) 
            print(ref.at(i));

      //auto &&range = str_vec;
      //auto begin = range.begin();
      //auto end = range.end();
      //for (; begin != end; ++begin)
      //{
         // auto&& ref = *begin;
         // for (size_t i{}; i != ref.size(); i++) 
         //     print(ref.at(i));
      //}
}

Вывод этой программы:

Первый второй третий четвертый

Я хотел бы уточнить некоторые части ответа @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 не выполняется).

Для цикла for на основе диапазона сначала выбираются функции-члены.

Но для

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.
//      }
}
Другие вопросы по тегам