Как сделать мой пользовательский тип для работы с"диапазоном на основе циклов"?
как и многие люди в эти дни я пробовал различные функции, которые приносит 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 ответов:
стандарт был изменен с момента публикации вопроса (и большинства ответов)в разрешении этого отчета о дефекте.
как сделать
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-terminatedchar*
буфера.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(); } };