如何使我的自定义类型与“基于范围的循环”工作?

像许多人一样,这些天我一直在尝试c++ 11带来的不同特性。我最喜欢的是“基于范围的for循环”。

我明白:

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

等价于:

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

begin()只是返回标准容器的a.begin()

但是如果我想使我的自定义类型“基于范围的for循环”感知呢?

我应该特殊化begin()end()吗?

如果我的自定义类型属于命名空间xml,我应该定义xml::begin()还是std::begin() ?

简而言之,这样做的指导方针是什么?

129688 次浏览

我应该专门化begin()和end()吗?

据我所知,这就足够了。您还必须确保对指针进行递增可以从开始到结束。

下一个示例(它缺少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作为函数的例子。它们必须与类在相同的命名空间中,因为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;
}
}

标准的相关部分为6.5.4/1:

如果_RangeT是类类型,则unqualified-ids开始和结束为 在类_RangeT的作用域中查找,就像通过类成员访问一样 查找(3.4.5),如果其中一个(或两个)找到至少一个声明, begin- expr和end-expr分别是__range.begin()__range.end(), 分别;< / p >

-否则,begin-expr和end-expr为begin(__range)end(__range),分别用于查找begin和end 依赖参数的查找(3.4.2)。为了这个名字的目的 查找,命名空间STD是一个关联的命名空间

所以,你可以做以下任何一件事:

  • 定义beginend成员函数
  • 定义ADL可以找到的beginend自由函数(简化版本:将它们放在与类相同的命名空间中)
  • 专门std::beginstd::end

std::begin无论如何都会调用begin()成员函数,所以如果你只实现了上面的一个,那么无论你选择哪一个,结果都应该是相同的。对于基于范围的for循环也是一样的结果,对于没有自己神奇的名称解析规则的普通代码也是一样的结果,因此只执行using std::begin;,然后对begin(a)进行非限定调用。

如果你实现成员函数而且 ADL函数,那么基于范围的for循环应该调用成员函数,而凡人将调用ADL函数。最好确保他们在这种情况下做同样的事情!

如果你正在编写的东西实现了容器接口,那么它已经有begin()end()成员函数,这应该足够了。如果它是一个不是容器的范围(如果它是不可变的,或者如果你不知道前面的大小,这将是一个好主意),你可以自由选择。

在你布局的选项中,注意你不得重载了std::begin()。允许为用户定义的类型专门化标准模板,但除此之外,向名称空间std添加定义是未定义的行为。但无论如何,专门化标准函数是一个糟糕的选择,因为缺乏部分函数专门化意味着您只能为单个类而不是类模板这样做。

如果你想用类的std::vectorstd::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;
}

我写下我的答案是因为有些人可能更喜欢简单的现实生活的例子,没有STL包含。

出于某种原因,我有自己的纯数据数组实现,我想使用基于范围的for循环。以下是我的解决方案:

 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);

自问题(和大多数答案)发布在此缺陷报告的决议中以来,标准已经改变。

使for(:)循环在你的类型X上工作的方法现在是以下两种方法之一:

  • 创建成员X::begin()X::end(),返回类似迭代器的东西

  • 创建一个自由函数begin(X&)end(X&),返回一些类似迭代器的东西,与你的类型X.¹相同的命名空间

类似的const变体。这既适用于实现缺陷报告更改的编译器,也适用于不实现缺陷报告更改的编译器。

返回的对象不一定是迭代器。for(:)循环,

for( range_declaration : range_expression )

与c++标准的大多数部分不同,是指定扩展为等价的东西:

{
auto && __range = range_expression ;
for (auto __begin = begin_expr,
__end = end_expr;
__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}

其中以__开头的变量仅用于说明,而begin_exprend_expr是调用begin/end.²的魔法

对开始/结束返回值的要求很简单:必须重载pre-++,确保初始化表达式有效,二进制!=可用于布尔上下文,一元*可返回可用于赋值-初始化range_declaration的值,并公开公共析构函数。

以一种与迭代器不兼容的方式这样做可能是一个坏主意,因为如果您这样做了,c++未来的迭代可能相对不会破坏您的代码。

顺便说一句,标准的未来修订很可能会允许end_expr返回与begin_expr不同的类型。这是有用的,因为它允许&;lazy-end&;计算(如检测空终止),易于优化,与手写的C循环一样高效,以及其他类似的优点。


¹注意,for(:)循环将任何临时变量存储在auto&&变量中,并将其作为左值传递给你。你无法检测你是否正在迭代一个临时值(或其他右值);这样的重载将不会被for(:)循环调用。看到[支撑。范围从n4527的1.2-1.3。

²调用begin/end方法,或仅adl查找自由函数begin/end魔术以支持c风格数组。注意,除非range_expression返回一个类型为namespace std或依赖于same的对象,否则不会调用std::begin


中,range-for表达式已被更新

{
auto && __range = range_expression ;
auto __begin = begin_expr;
auto __end = end_expr;
for (;__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}

__begin__end类型已经解耦。

这允许结束迭代器与开始迭代器的类型不同。你的结束迭代器类型可以是"sentinel"它只支持带有begin迭代器类型的!=

为什么这是有用的一个实际例子是,你的结束迭代器可以读取“检查你的char*,看看它是否指向__abc1”;当==带有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;
}
};

生活的例子的这个。

最小测试代码是:

struct cstring {
const char* ptr = 0;
const char* begin() const { return ptr?ptr:""; }// return empty string if we are null
null_sentinal_t end() const { return {}; }
};


cstring str{"abc"};
for (char c : str) {
std::cout << c;
}
std::cout << "\n";

这里有一个简单的例子。

namespace library_ns {
struct some_struct_you_do_not_control {
std::vector<int> data;
};
}

你的代码:

namespace library_ns {
int* begin(some_struct_you_do_not_control& x){ return x.data.data(); }
int* end(some_struct_you_do_not_control& x){ return x.data.data()+x.data.size(); }
int const* cbegin(some_struct_you_do_not_control const& x){ return x.data.data(); }
int* cend(some_struct_you_do_not_control const& x){ return x.data.data()+x.data.size(); }
int const* begin(some_struct_you_do_not_control const& x){ return cbegin(x); }
int const* end(some_struct_you_do_not_control const& x){ return cend(x); }
}

这是一个例子,你可以将一个你无法控制的类型增强为可迭代的。

在这里,我返回指针作为迭代器,隐藏了我有一个向量的事实。

对于你拥有的类型,你可以添加方法:

struct egg {};
struct egg_carton {
auto begin() { return eggs.begin(); }
auto end() { return eggs.end(); }
auto cbegin() const { return eggs.begin(); }
auto cend() const { return eggs.end(); }
auto begin() const { return eggs.begin(); }
auto end() const { return eggs.end(); }
private:
std::vector<egg> eggs;
};

这里我重用了vector的迭代器。为了简洁起见,我使用auto;在中,我必须更详细。

下面是一个快速且肮脏的可迭代范围视图:

template<class It>
struct range_t {
It b, e;
It begin() const { return b; }
It end() const { return e; }


std::size_t size() const
// C++20 only line: (off C++20 it generates a hard error)
requires std::random_access_iterator<It>
{
return end()-begin(); // do not use distance: O(n) size() is toxic
}


bool empty() const { return begin()==end(); }
 

range_t without_back() const {
if(emptty()) return *this;
return {begin(), std::prev(end())};
}


range_t without_back( std::size_t n ) const
// C++20 only line: (see below)
requires !std::random_access_iterator<It>
{
auto r=*this;
while(n-->0 && !r.empty())
r=r.without_back();
return r;
}


range_t without_front() const {
if(empty()) return *this;
return {std::next(begin()), end()};
}


range_t without_front( std::size_t n ) const
// C++20 only line: (see below)
requires !std::random_access_iterator<It>
{
auto r=*this;
while(n-->0 && !r.empty())
r=r.without_front();
return r;
}


// C++20 section:
range_t without_back( std::size_t n ) const
requires std::random_access_iterator<It>
{
n = (std::min)(n, size());
return {b, e-n};
}
range_t without_front( std::size_t n ) const
requires std::random_access_iterator<It>
{
n = (std::min)(n, size());
return {b+n, e};
}
// end C++20 section




decltype(auto) front() const { return *begin(); }
decltype(auto) back() const { return *(std::prev(end())); }
};
template<class It>
range_t(It,It)->range_t<It>;
template<class C>
auto make_range( C&& c ) {
using std::begin; using std::end;
return range_t{ begin(c), end(c) };
}

使用模板类演绎。

std::vector<int> v{1,2,3,4,5};
for (auto x : make_range(v).without_front(2) ) {
std::cout << x << "\n";
}

指纹3 4 5,跳过前2。

在这里,我分享一个创建自定义类型的最简单的例子,它将与"基于范围的for循环"一起工作:

#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:)
谢谢你。< / p >

Chris Redford的答案当然也适用于Qt容器。下面是一个改编(注意我从const_iterator方法中分别返回constBegin()constEnd()):

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(); }
};

我想详细解释一下@Steve Jessop回答的一些部分,一开始我不明白。希望能有所帮助。

std::begin无论如何都会调用begin()成员函数,所以如果你 只实现上面的一个,那么结果应该是相同的 不管你选择哪一个。结果是一样的 基于范围的for循环,对于普通代码也是同样的结果 它没有自己神奇的名称解析规则

. using std::begin;后面是对begin(a)的非限定调用 如果你实现了成员函数 而且 ADL函数, 那么基于范围的for循环应该调用成员函数,而 凡人将调用ADL函数。最好确保他们做了 在这种情况下也是一样!< / p >

https://en.cppreference.com/w/cpp/language/range-for:

  • 如果……
  • 如果range_expression是类类型C的表达式,它同时有一个名为begin的成员和一个名为end的成员 该成员的类型或可访问性),则begin_expr__range.begin()和end_expr__range.end();
  • 否则,begin_exprbegin(__range)end_exprend(__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.
//      }
}

受到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在优化编译时都产生非常合理的代码(即GCC的-Os或以上-O1或Clang的-O2)。

我想我没有什么要解释的,因为答案已经说明了这一点。但我可能不得不引用标准(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是一个表达式,则它被视为 如果用圆括号括起来(这样逗号操作符就不能

.

(1.2) range、begin和end是仅为说明而定义的变量;而且

(3.1) begin-expr和end-expr确定如下:

(1.3.1)如果for-range-initializer是数组类型R的表达式, begin-expr和end-expr分别为range和range+N,其中N 是数组的边界。如果R是一个未知边界的数组或者 不完整类型,程序格式错误;

(1.3.2)如果for-range-initializer是类类型C的表达式, 和[class.member。在C的作用域查找开始和 结束每个查找至少一个声明,begin-expr和End -expr是

. Range.begin()和range.end()

(1.3.3)否则begin-expr和end-expr为begin(range)和 End(范围),分别表示开始和结束经历的地方 依赖参数的查找([basic.lookup.argdep]).


请注意,字符串、数组和所有STL容器都是可迭代的数据结构,因此已经可以使用基于范围的for循环对它们进行迭代。为了使数据结构可迭代,它必须类似于现有的STL迭代器:

1-必须有beginend方法对该结构进行操作,可以作为成员,也可以作为独立函数,并返回结构的开始和结束的迭代器。

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));
//}
}

这个程序的输出是:

< p >第一 第二个 第三 第四个< / p >