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


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

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

7 180

7 ответов:

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

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

  • создать и X::end() это возвращает что-то, что действует как итератор

  • создать свободную функцию begin(X&) и end(X&) что вернуть что-то, что действует как итератор, в том же пространстве имен, что и ваш тип X.1

и аналогично для 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.2

требования к возвращаемому значению begin/end просты: вы должны перегрузить pre -++, убедитесь, что выражения инициализации действительны, binary != который может быть использован в логическом контексте, унарный * что возвращает то, что вы можете назначить-инициализировать range_declaration С, и выставить публичный деструктор.

делать это таким образом, который не совместим с итератором, вероятно, плохая идея, так как будущие итерации C++ могут быть относительно бесцеремонными о взломе вашего кода, если вы это сделаете.

в стороне, вполне вероятно, что будущий пересмотр стандарта позволит end_expr чтобы вернуть другой тип, чем begin_expr. Это полезно в том, что он позволяет" ленивый конец " оценки (например, обнаружение null-termination), который легко оптимизировать, чтобы быть таким же эффективным, как рукописный цикл C, и другие подобные преимущества.


1 Обратите внимание, что for(:) петли хранить любые временные в auto&& переменная, и передать его вам в качестве значения lvalue. Вы не можете определить, выполняете ли вы итерацию по временному (или другому rvalue); такая перегрузка не будет вызвана for(:) петли. Смотрите [полу.ranged] 1.2-1.3 от n4527.

2 либо позвонить 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 были развязаны.

это позволяет конечному итератору не быть того же типа, что и begin. Ваш тип конечного итератора может быть "sentinel", который поддерживает только != С типом итератора begin.

практический пример того, почему это полезно, заключается в том, что ваш конечный итератор может читать "проверьте ваш char* чтобы увидеть, если он указывает '0'" когда == С char*. Это позволяет c++ range-for expression генерировать оптимальный код при итерации по null-terminated 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 петля вручную расширена.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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 (конечно). Вот адаптация (обратите внимание, что я возвращаю 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(); }
};