如何使我的自定义类型与"range-based for loops"一起使用?
How to make my custom type to work with "range-based for loops"?
像现在的许多人一样,我一直在尝试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()
?
简而言之,这样做的指导方针是什么?
自从问题(和大多数答案(发布在此缺陷报告的解决方案中以来,标准已更改。
使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_expr
和 end_expr
是调用 begin
/end
.² 的魔法
对开始/结束返回值的要求很简单:您必须重载预++
,确保初始化表达式是有效的、可以在布尔上下文中使用的二进制!=
、返回可以分配初始化range_declaration
的一元*
,并公开公共析构函数。
以与迭代器不兼容的方式执行此操作可能是一个坏主意,因为如果您这样做,C++的未来迭代可能会相对随意地破坏代码。
顺便说一句,标准的未来修订版很可能允许end_expr
返回与begin_expr
不同的类型。 这很有用,因为它允许"惰性端"评估(如检测零终止(,易于优化,与手写 C 循环一样高效,以及其他类似的优点。
¹ 请注意,for(:)
循环将任何临时变量存储在 auto&&
变量中,并将其作为左值传递给您。 您无法检测是否正在迭代临时(或其他右值(;for(:)
循环不会调用此类重载。 参见 n4527 中的 [stmt.ranged] 1.2-1.3。
begin
/end
方法,要么调用仅 ADL 查找自由函数 begin
/end
,要么调用 magic 以获得 C 样式数组支持。 请注意,除非range_expression
返回 namespace std
或依赖于 same 的对象,否则不会调用 std::begin
。
在 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 的类型不同。 结束迭代器类型可以是"哨兵",它仅支持!=
开始迭代器类型。
为什么这很有用的一个实际示例是,当使用char*
==
时,您的结束迭代器可以读取"检查您的char*
以查看它是否指向'0'
"。 这允许 C++ 范围表达式在循环访问以 null 结尾的 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
;在 c++11 中,我必须更冗长。
这是一个快速而肮脏的可迭代范围视图:
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) };
}
使用 C++17 模板类推导。
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。
我写我的答案是因为有些人可能对没有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: %cn", c);
该标准的相关部分是 6.5.4/1:
如果_RangeT是类类型,则非限定 ID 的开头和结尾为 在类_RangeT的范围内查找,仿佛通过类成员访问 查找 (3.4.5(,如果其中一个(或两个(找到至少一个声明, 开始 - expr 和结束 expr 是
__range.begin()
和__range.end()
, 分别;— 否则,开始 expr 和结束 expr 是
begin(__range)
和end(__range)
,分别查找开头和结尾的位置 依赖于参数的查找 (3.4.2(。出于此名称的目的 查找,命名空间 std 是一个关联的命名空间。
因此,您可以执行以下任一操作:
- 定义
begin
和end
成员函数 - 定义 ADL 将找到的
begin
和end
自由函数(简化版本:将它们放在与类相同的命名空间中( - 专攻
std::begin
和std::end
无论如何,std::begin
调用 begin()
成员函数,因此如果您只实现上述一项,那么无论您选择哪一个,结果都应该相同。对于基于范围的 for 循环,这是相同的结果,对于没有自己神奇名称解析规则的普通代码也是如此,所以只是using std::begin;
后跟一个不合格的 begin(a)
调用。
但是,如果实现成员函数和 ADL 函数,则基于范围的 for 循环应调用成员函数,而普通人将调用 ADL 函数。在这种情况下,最好确保他们做同样的事情!
如果你正在编写的东西实现了容器接口,那么它将已经具有begin()
和end()
成员函数,这应该足够了。如果它是一个不是容器的范围(如果它是不可变的,或者你不知道前面的大小,那将是一个好主意(,你可以自由选择。
在您列出的选项中,请注意,您不得重载std::begin()
.您可以为用户定义类型专门化标准模板,但除此之外,向命名空间 std 添加定义是未定义的行为。但无论如何,专用标准函数是一个糟糕的选择,因为缺少部分函数专用化意味着您只能为单个类执行此操作,而不能为类模板执行此操作。
我应该专门化 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;
}
}
这是另一个将开始/结束作为函数的示例。它们必须与类位于同一命名空间中,因为 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;
}
}
如果您想直接使用类的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;
}
受到BitTickler关于如何使其适用于非"容器"类型的评论的启发,这里有一个适用于double
的最小示例:
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
(。
在这里,我分享了创建自定义类型的最简单示例,它将与">基于范围的 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 :)
谢谢。
Chris Redford的答案也适用于Qt容器(当然(。这是一个改编(请注意,我分别从const_iterator方法constEnd()
返回一个constBegin()
(:
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(); }
};
我没有什么要解释的,因为答案已经这样做了。但我可能不得不引用标准 (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(范围、开始和结束是仅为说明而定义的变量;和
(3.1( 开始-终止-终止确定如下:
(1.3.1( 如果 for-range-initializer 是数组类型 R 的表达式,开始-expr 和结束 expr 分别是范围和范围+N,其中 N是数组绑定。如果 R 是未知边界的数组或类型不完整,程序格式不正确;
(1.3.2( 如果 for-range-initializer 是类类型 C 的表达式,和 [class.member.lookup] 在 C 的范围内,名称开始和end每个找到至少一个声明,begin-expr 和 end-expr 分别是range.begin(( 和 range.end(( 分别;
(1.3.3( 否则,开始-expr 和 end-expr 是 begin(range( 和结束(范围(,分别是开始和结束经历的地方依赖于参数的查找([basic.lookup.argdep](。
请注意,字符串、数组和所有 STL 容器都是可迭代的数据结构,因此它们已经可以使用基于范围的 for 循环进行迭代。为了使数据结构可迭代,它必须与现有的 STL 迭代器类似地工作:
1-必须有begin
和end
方法在该结构上运行,无论是作为成员还是作为独立函数,并将迭代器返回到结构的开头和结尾。
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));
//}
}
该程序的输出是:
第一第二第三第四
详细说明@Steve杰索普的回答的某些部分,起初我不明白。希望对您有所帮助。
无论如何,
std::begin
调用begin()
成员函数,所以如果你 只实现上述其中一项,那么结果应该是相同的 无论您选择哪一个。这是相同的结果 基于范围的 for 循环,对于纯粹的凡人代码也是如此 它没有自己的神奇名称解析规则,所以只是using std::begin;
紧随其后的是无条件的begin(a)
电话。但是,如果实现成员函数和ADL 函数, 然后基于范围的 for 循环应该调用成员函数,而 凡人会调用 ADL 函数。最好确保他们这样做 在这种情况下也是如此!
https://en.cppreference.com/w/cpp/language/range-for :
- 如果。。。
- 如果
range_expression
是类类型的表达式,则C
具有名为begin
的成员和名为end
的成员(无论 该成员的类型或可访问性(,则begin_expr
__range.begin(
(,end_expr
__range.end()
;- 否则,
begin_expr
begin(__range)
,end_expr
end(__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.
// }
}
- 如何将enable-if与模板参数和参数包一起使用
- 如何将PERF_AMPLE_READ与mmap一起使用
- 如何将两个不同矢量的同一位置的两个元素组合在一起
- 如何将C++中的库和头与MinGW一起使用
- 将--whole archive链接器选项与CMake和具有其他库依赖项的库一起使用
- 为什么我不能将 rand() 与数组的大小一起使用?
- 要与"if constexpr"一起使用的编译时消息(在预处理器之后)
- 不能将复制初始化与隐式转换的多个步骤一起使用
- 将fold表达式与std::一起用于两个元组
- spdlog标头仅与外部fmt一起使用.spdlog错误:'内部':不是'fmt'
- 将 std::allocate_shared 与多态资源分配器一起使用
- 如何在自定义类中启用'auto loops'?
- 为什么常量词在重载运算符中不与 ostream 对象一起使用<<?
- 将 OpenCV 与 CMAKE 中的项目一起构建为第三方库的正确方法
- 将 exprtk 与自定义类的对象一起使用
- 将 std::set 与基于键的比较器一起使用
- 将 C++ 类与 Rcpp 一起使用,从 C 或 R 修改它
- 如何将 Eigen::Ref 与 pybind11 一起使用?
- 如何将AERT_Allocate与 std:vector 一起使用
- 如何使我的自定义类型与"range-based for loops"一起使用?