簡體   English   中英

如何使我的自定義類型與“基於范圍的 for 循環”一起使用?

[英]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變化類似。 這將適用於實現缺陷報告更改的編譯器和不實現缺陷報告更改的編譯器。

返回的對象不一定是迭代器。 與 C++ 標准的大多數部分不同, for(:)循環被指定為擴展為等價於

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_exprend_expr是調用begin / end的魔法。²

對開始/結束返回值的要求很簡單:您必須重載 pre- ++ ,確保初始化表達式有效,二進制!=可以在布爾上下文中使用,一元*返回可以分配的內容-初始化range_declaration並公開一個公共析構函數。

以與迭代器不兼容的方式這樣做可能是一個壞主意,因為如果你這樣做,C++ 的未來迭代可能會相對比較隨意地破壞你的代碼。

順便說一句,標准的未來修訂版很有可能允許end_expr返回與begin_expr不同的類型。 這很有用,因為它允許“延遲”評估(如檢測空終止),易於優化,與手寫 C 循環一樣高效,以及其他類似優點。


¹ 請注意, for(:)循環將任何臨時存儲在auto&&變量中,並將其作為左值傳遞給您。 您無法檢測是否正在迭代臨時(或其他右值); 這樣的重載不會被for(:)循環調用。 參見 n4527 中的 [stmt.ranged] 1.2-1.3。

² 調用begin / end方法,或自由函數begin / end的 ADL-only 查找,C 風格數組支持的魔法。 注意std::begin不會被調用,除非range_expression返回namespace std中類型的對象或依賴於相同的對象。


中,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的類型已經解耦。

這允許結束迭代器與開始的類型不同。 您的結束迭代器類型可以是一個“哨兵”,它只支持!=與開始迭代器類型。

為什么這很有用的一個實際示例是,當==帶有char*時,您的最終迭代器可以讀取“檢查您的char*以查看它是否指向'0' ”。 這允許 C++ range-for 表達式在迭代以 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 中,我必須更加冗長。

這是一個快速而骯臟的可迭代范圍視圖:

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  std::size_t size() const { 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 {
    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 {
    auto r=*this;
    while(n-->0 && !r.empty())
      r=r.without_front();
    return r;
  }
  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。

我寫下我的答案是因為有些人可能對沒有 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);

標准的相關部分是6.5.4/1:

如果 _RangeT 是類類型,則在類 _RangeT 的范圍內查找未限定的 ID 開始和結束,就像通過類成員訪問查找 (3.4.5) 一樣,如果其中一個(或兩者)找到至少一個聲明,開始- expr 和 end-expr 分別是__range.begin()__range.end()

— 否則, 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 是未定義的行為。 但是無論如何,專門化標准函數是一個糟糕的選擇,因為缺少部分函數專門化意味着您只能為單個類而不是類模板這樣做。

我應該只專注於 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;
    }
}

如果你想直接用它的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;
}

受 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 在使用優化編譯時都會產生非常合理的代碼(即-Os或以上-O1用於 GCC 或-O2用於 Clang)。

在這里,我將分享創建自定義類型的最簡單示例,該示例將與“基於范圍的 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 方法分別返回一個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 函數。 最好確保他們在這種情況下做同樣的事情!


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.
//      }
}

我想我沒有什么要解釋的,因為答案已經做到了。 但我可能不得不引用標准(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 是一個表達式,則認為它好像被括號包圍(這樣逗號運算符就不能重新解釋為分隔兩個 init-declarators);

(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.lookup] 在 C 的范圍內為名稱 begin 和 end 每個找到至少一個聲明,begin-expr 和end-expr 分別是 range.begin() 和 range.end();

(1.3.3) 否則,begin-expr 和 end-expr 分別是 begin(range) 和 end(range),其中 begin 和 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));
      //}
}

這個程序的輸出是:

第一第二第三第四

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM