"범위 기반 for 루프"에서 작동하도록 사용자 정의 유형을 만드는 방법은 무엇입니까?


252

요즘 많은 사람들처럼 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()?

간단히 말해, 그 지침은 무엇입니까?


begin/endstatic 또는 free 멤버 또는 친구 를 정의하여 가능합니다 begin/end. 어떤 네임 스페이스에서 무료 기능을 사용하는지주의하십시오 : stackoverflow.com/questions/28242073/…
alfC

컨테이너가 아닌 float 값 범위의 예를 사용하여 답변을 게시 할 수 있습니까 for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }? `´operator! = ()``가 정의하기 어렵다는 사실을 어떻게 해결하는지 궁금합니다. *__begin이 경우 역 참조 ( )는 어떻습니까? 누군가 어떻게 그렇게 했는지 보여 주면 큰 공헌이 될 것입니다!
BitTickler

답변:


183

질문 (및 대부분의 답변) 이이 결함 보고서의 해결책에 게시 이후 표준이 변경되었습니다 .

for(:)타입에 루프 를 적용하는 방법 X은 이제 두 가지 방법 중 하나입니다.

  • 멤버를 X::begin()만들고 X::end()반복자처럼 작동하는 것을 반환

  • 무료 함수를 작성 begin(X&)하고 end(X&)당신의 유형과 같은 네임 스페이스에, 반복자 같은 역할을 돌려 무언가를 X

그리고 유사점도 비슷합니다 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

begin / end 반환 값에 대한 요구 사항은 간단합니다. pre-를 오버로드 ++하고, 초기화식이 유효한지, !=부울 컨텍스트에서 사용할 수있는 이진 , *할당 초기화 할 수있는 항목을 반환 하는 이진 range_declaration, 공개를 노출해야합니다. 파괴 장치.

반복자와 호환되지 않는 방식으로 그렇게하는 것은 아마도 나쁜 생각입니다 .C ++의 향후 반복은 코드를 깨는 것에 대해 상대적으로 무심 할 수 있기 때문입니다.

제쳐두고, 표준의 향후 개정이와 end_expr다른 유형을 반환 하도록 허용 할 가능성이 상당히 높다 begin_expr. 이 기능은 손으로 쓴 C 루프만큼 효율적으로 최적화하기 쉬운 "게으른 끝"평가 (널 종료 감지와 같은) 및 기타 유사한 이점을 허용한다는 점에서 유용합니다.


¹ for(:)루프는 임시 auto&&변수를 변수 에 저장 하고이를 lvalue로 전달합니다. 임시 (또는 다른 rvalue)를 반복하는지 감지 할 수 없습니다. 이러한 과부하는 for(:)루프에 의해 호출되지 않습니다 . n4527의 [stmt.ranged] 1.2-1.3을 참조하십시오.

² 부르는 어느 begin/ 된 end방법 또는 ADL 전용 조회없는 기능 begin/ end, 또는 C 스타일 배열 지원 마법. 참고 std::begin않는 호출되지 않습니다 range_expression다시 표시의 유형의 개체 namespace std또는 동일에 의존.


범위-표현식이 업데이트되었습니다

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

의 유형 __begin__end분리되었습니다.

이를 통해 종료 반복자가 시작과 동일한 유형이 될 수 없습니다. 엔드 이터레이터 유형은 !=시작 이터레이터 유형 만 지원하는 "센티넬"일 수 있습니다 .

이 유용한 이유의 실제 예는 끝 반복자가 읽기 "당신을 확인할 수 있다는 것입니다 char*그것을 가리키는 경우로 볼 '0'때" ==A를 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;
  }
};

완전한 C ++ 17 지원이없는 컴파일러에서의 라이브 예제 ; for루프 수동 확장.


범위 기반에 대해 다른 조회 메커니즘을 사용하는 경우 해당 범위 기반을 정렬 하여 일반 코드에서 사용할 수 있는 것과 다른 쌍 beginend기능 을 얻을 수 있습니다. 아마도 그들은 다르게 행동하도록 매우 전문화 될 수 있습니다 (즉, 최적화를 극대화하기 위해 end 인수를 무시함으로써 더 빠를 수 있습니다). 그러나 네임 스페이스로는이 작업을 수행하는 방법을 확신하기에 충분하지 않습니다.
Aaron McDaid

@AaronMcDaid 실용적이지 않았습니다. begin / end를 호출하는 일부 방법은 시작 / 종료에 대한 범위 기반으로 끝나고 다른 방법은 그렇지 않기 때문에 놀라운 결과를 쉽게 얻을 수 있습니다. (클라이언트 측의) 무해한 변경 사항은 동작 변경을 가져옵니다.
Yakk-Adam Nevraumont

1
당신은 필요하지 않습니다 begin(X&&). 임시는 auto&&범위 기반에 의해 공중에서 일시 중단되며 begin항상 lvalue ( __range)로 호출됩니다 .
TC

2
이 답변은 실제로 복사하고 구현할 수있는 템플릿 예제에서 도움이됩니다.
Tomáš Zato-복원 모니카

오히려 반복자 유형 (*, ++,! =)의 속성에 스트레스를주었습니다. 이터레이터 유형 사양을 더 대담하게 만들려면이 답장을 바꿔야합니다.
Red.

62

일부 사람들은 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);

2
이 예제에는 begin () 및 end () 메소드가 있으며 사용자 정의 컨테이너 유형에 맞게 쉽게 조정할 수있는 기본 (이해하기 쉬운) 예제 반복자 클래스도 있습니다. std :: array <>와 가능한 대체 구현을 비교하는 것은 다른 질문이며, 제 생각에는 범위 기반 for 루프와 관련이 없습니다.
csjpeter

이것은 매우 간결하고 실용적인 답변입니다! 내가 찾던 것이 바로 그거야! 감사!
Zac Taylor

1
더 제거하기 위해 적절할 것 const 에 대한 반환 규정 const DataType& operator*(), 사용자가 사용하도록 선택할 수 있습니다 const auto&또는 auto&? 어쨌든 고마워, 큰 대답;)

53

표준의 관련 부분은 6.5.4 / 1입니다.

_RangeT가 클래스 유형 인 경우, 클래스 멤버 액세스 검색 (3.4.5)에 의해 규정되지 않은 ID 시작 및 종료가 클래스 _RangeT 범위에서 조회되고 (또는 둘 다) 하나 이상의 선언을 찾으면 시작합니다. -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::begin하고std::end

std::beginbegin()어쨌든 멤버 함수를 호출 하므로 위 중 하나만 구현하면 선택한 결과에 관계없이 결과가 동일해야합니다. 이는 원거리 기반 for 루프에 대해 동일한 결과이며, 고유 한 마법 이름 확인 규칙이없는 필사자 코드에 대한 동일한 결과이며 이에 using std::begin;대한 정규화되지 않은 호출 이 이어집니다 begin(a).

멤버 함수 ADL 함수 를 구현하는 경우 범위 기반 for 루프는 멤버 함수를 호출해야하지만 필사자 만 ADL 함수를 호출합니다. 이 경우에도 동일한 작업을 수행해야합니다.

작성하는 것이 컨테이너 인터페이스를 구현하는 경우 이미 컨테이너 인터페이스를 보유 begin()하고 있으며 end()멤버 기능이 이미 충분합니다. 컨테이너가 아닌 범위라면 (불변하거나 앞면의 크기를 모르는 경우 좋은 아이디어입니다) 자유롭게 선택할 수 있습니다.

배치 한 옵션 중 과부하가 걸리지 않아야합니다std::begin() . 사용자 정의 유형에 대한 표준 템플릿을 특수화 할 수 있지만 네임 스페이스 std에 정의를 추가하는 것은 정의되지 않은 동작입니다. 그러나 부분 함수 전문화가 부족하여 클래스 템플릿이 아닌 단일 클래스에 대해서만 할 수 있기 때문에 표준 함수를 전문화하는 것은 좋지 않은 선택입니다.


반복자가 많이 충족해야하는 특정 요구 사항이 있습니까? 즉, ForwardIterator 또는 그 라인을 따라 무언가가 있어야합니다.
Pubby

2
@Pubby : 6.5.4를 보면 InputIterator로 충분하다고 생각합니다. 그러나 실제로 반환 된 유형 범위 기반의 경우 반복 자라고 생각하지 않습니다 . 이 명령문은 표준에 해당하는 것으로 표준에 정의되어 있으므로 표준에서 코드에 사용 된 표현식 (operators !=, prefix ++및 unary) 만 구현하면 충분합니다 *. 그것은 아마 현명 구현 begin()end()멤버 함수 또는 비회원 ADL 기능 반복자 이외의 반환 어떤 것을,하지만 난 그게 법적 같아요. 전문 std::begin반환하는 비 - 반복자는 UB, 나는 생각한다.
Steve Jessop

std :: begin에 과부하가 걸리지 않아야합니까? 표준 라이브러리가 몇 가지 경우에 그렇게하기 때문에 묻습니다.
ThreeBit

@ThreeBit : 예, 확실합니다. 표준 라이브러리 구현 규칙은 프로그램 규칙과 다릅니다.
Steve Jessop

3
open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1442 에 대해 업데이트해야합니다 .
TC

34

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

1
@ereOn 클래스가 정의 된 동일한 네임 스페이스에서. 두 번째 예보기
BЈовић

2
뿐만 아니라 축하합니다 : 그것은 (설명하는 용어 인수 두 번째 예를 들어, 종속 조회 (ADL)이나 코닉 조회 mentionning 가치가있을 수도 있습니다 이유를 무료로 함수가에서 운영하는 클래스와 같은 네임 스페이스에 있어야합니다)를.
Matthieu M.

1
@ereOn : 실제로는 그렇지 않습니다. ADL은 인수가 속한 네임 스페이스를 자동으로 포함하도록 검색 범위를 확장하는 것입니다. 불행히도 이름 조회 부분을 건너 뛰는 과부하 해결에 대한 좋은 ACCU 기사 가 있습니다. 이름 조회는 후보 함수 수집과 관련이 있으며, 현재 범위 + 인수 범위를 살펴 보는 것으로 시작합니다. 일치하는 이름이 없으면 전역 범위에 도달 할 때까지 현재 범위의 상위 범위로 이동 한 후 다시 검색하십시오.
Matthieu M.

1
@ BЈовић 죄송하지만 end () 함수에서 어떤 이유로 위험한 포인터를 반환합니까? 나는 그것이 효과가 있다는 것을 알고 있지만 이것의 논리를 이해하고 싶다. 배열의 끝은 v [9]입니다. 왜 v [10]을 반환 하시겠습니까?
gedamial

1
@gedamial 동의합니다. 나는 그것이 있어야한다고 생각합니다 return v + 10. &v[10]배열 바로 뒤의 메모리 위치를 역 참조합니다.
Millie Smith

16

클래스 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;
}

2
그 언급이의 가치는 const_iterator또한에 액세스 할 수 있습니다 auto통해 (C ++ 11) 호환 방법 cbegin, cend
underscore_d

2

여기서는 " range-based for loop "에서 작동하는 커스텀 타입을 생성하는 가장 간단한 예를 공유하고 있습니다 :

#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 :)
감사합니다.


end 메소드에서 유효하지 않은 메모리를 역 참조하는 것을 피하기 위해 하나의 추가 요소를 할당하지 않는 이유는 무엇입니까?
AndersK

거의 모든 최종 반복기 포인트로 인해 @Anders 그들의 함유 구조체의 단. end()에만이 메모리 위치 본 '-의 주소를'걸리므 기능 자체는 분명히 잘못된 메모리 위치를 역 참조하지 않습니다. 추가 요소를 추가하면 더 많은 메모리가 필요 your_iterator::end()하며 해당 방식을 역 참조하는 방식을 사용하면 다른 반복자와 동일한 방식으로 빌드되므로 작동하지 않습니다.
Qqwy

@ Qqwy 그의 최종 방법 de-refences- return &data[sizeofarray]IMHO 그냥 주소 데이터 + sizeofarray를 반환해야하지만 내가 아는
AndersK

@Anders 당신이 맞습니다. 날 선명하게 유지해 주셔서 감사합니다 :-). 예, data + sizeofarray이것을 작성하는 올바른 방법입니다.
Qqwy 2019

1

Chris Redford의 답변은 Qt 컨테이너에서도 작동합니다 (물론). 다음은 적응입니다 ( const_iterator 메소드에서 constBegin()각각 a를 반환 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(); }
};

0

@Steve Jessop의 답변 중 일부를 자세히 설명하고 싶습니다. 처음에는 이해하지 못했습니다. 도움이 되길 바랍니다.

std::beginbegin()어쨌든 멤버 함수를 호출 하므로 위 중 하나만 구현하면 선택한 결과에 관계없이 결과가 동일해야합니다. 이는 원거리 기반 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_expris begin(__range)and end_expris는 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.
//      }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.