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

Как и многие люди, в эти дни я пытаюсь использовать разные функции, которые приносит C + 11. Одним из моих фаворитов является "диапазон для циклов".

Я понимаю, что:

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

Является эквивалентным:

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

И что begin() просто возвращает a.begin() для стандартных контейнеров.

Но что, если я хочу сделать свой собственный тип "range-based for loop" -aware?

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

Если мой пользовательский тип принадлежит пространству имен xml, я должен определить xml::begin() или std::begin()?

Короче говоря, каковы рекомендации для этого?

Ответ 1

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

Способ заставить цикл 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

Требования к возвращаемому значению начала/конца просты: необходимо перегрузить pre- ++, убедиться, что выражения инициализации допустимы, двоичный !=, который может использоваться в логическом контексте, унарный *, который возвращает что-то, что вы можно назначить-инициализировать 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 или зависит от него.


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

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

Ответ 2

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

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

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

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

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

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

Однако, если вы реализуете функции-члены и функции ADL, тогда циклы, основанные на диапазонах, должны вызывать функции-члены, тогда как простые смертные будут вызывать функции ADL. Лучше убедитесь, что они делают то же самое в этом случае!

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

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

Ответ 3

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

Ответ 4

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

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

Следующий пример (отсутствует версия 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;
    }
}

Вот еще один пример с функциями begin/end as. Они должны находиться в том же пространстве имен, что и класс, из-за 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;
    }
}

Ответ 5

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

Ответ 6

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

#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:)
Спасибо.

Ответ 7

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