Как заставить мой пользовательский тип работать с «диапазонами для циклов»? - PullRequest
221 голосов
/ 17 ноября 2011

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

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

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

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

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

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

Но что, если я хочу сделать мой пользовательский тип "на основе диапазона для цикла" -aware ?

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

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

Короче говоря, каковыруководство для этого?

Ответы [ 7 ]

150 голосов
/ 16 июля 2015

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

Способ заставить цикл 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, или magic для поддержки массивов в стиле 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 петля расширена вручную.

52 голосов
/ 17 ноября 2011

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

если _RangeT является типом класса, неполные идентификаторы начало и конец посмотрел в области видимости класса _RangeT как будто при доступе члена класса lookup (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 является связанным пространством имен.

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

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

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

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

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

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

37 голосов
/ 08 марта 2015

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

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

Вот еще один пример с функциями 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;
    }
}
13 голосов
/ 12 мая 2014

Если вы хотите поддержать итерацию класса напрямую с помощью члена 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;
}
1 голос
/ 07 февраля 2018

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...