I'm FanJae.

[C++] Iterator 본문

C++/Basic

[C++] Iterator

FanJae 2024. 8. 28. 13:31

1.반복자(iterator)

#include <iostream>
#include <list>

int main()
{
    int x[5] = {1,2,3,4,5};
    
    int* p1 = x;
    ++p1;
    std::cout << *p1 << " ";
    
    std::list s = {1,2,3,4,5};
    
    auto p2 = s.begin();
    ++p2;
    std::cout << *p2 << " "; // 2
}

- 배열은 연속된 메모리이다.

- 1번째 요소의 주소를 담은 포인터 변수만 있으면, ++, * 연산으로 모든 요소에 접근할 수 있다.

- 반면 일반적인 list는 연속된 메모리가 아니므로 ++연산등을 통한 요소 접근은 불가능하다.

- 그러나, std::list는 연속된 메모리가 아니지만, 반복자(iterator)만 있으면 ++, * 연산으로 모든 요소에 접근이 가능하다.

- p2 자체가 포인터는 아니다.

- 내부적으로 포인터 멤버 데이터가 있고, operator++, operator* 연산자를 재정의해 포인터처럼 만든 것이다.

 

std::vector v = {1,2,3,4,5};
    
auto p2 = v.begin();
++p2;
std::cout << *p2 << " "; // 2

- vector가 연속된 메모리임에도 불구하고 이와 같이 문제없이 출력하는것을 확인할 수 있다.

 

1-1. 반복자(iterator) 디자인 패턴

- 복합 객체의 내부 구조에 상관없이 동일한 방법으로 모든 요소에 접근하게 한다.

- 즉, 모든 컨테이너의 요소 접근을 동일한 방법으로 수행한다.

 

① STL 에서의 반복자 구현 방식

- 포인터와 동일한 방법으로 사용할 수 있도록 만들어져있다.

- 즉, 이는 진짜 포인터는 아니지만 포인터와 동일한 방식으로 ++, * 연산자를 사용한다.

 

1-2. 반복자(iterator)의 정확한 타입

#include <list>
#include <vector>
#include <ranges>

int main()
{
    // 1. 반복자의 정확한 타입
    std::list c = {1,2,3,4,5};
    
    std::list::iterator p = c.begin(); // C++17
    std::list<int>::iterator p = c.begin(); // C++17 이하
}

- 반복자의 정확한 타입은 컨테이너타입::iterator이다.

- std::list<int>::iteartor p1 = c.begin() // C++ 17 이전까지는 이와같다.

- std::list::iterator p1 = c.begin() // c++ 17 부터는 이 방버볻 지원한다.

 

① 문제점

- 코드가 복잡해보인다. 즉, 가독성이 상당히 나쁘다

- 컨테이너 변경시 반복자 타입도 변경되어야 한다. (아래 예시 참고)

std::vector c = {1,2,3,4,5};

std::vector<int>::iterator p = c.begin();

- list를 사용하다가 vector로 바꾸면 반복자 타입도 수정해야한다.

- 따라서 되도록이면 auto를 사용한다.

 

② 반복자를 꺼내는 3가지 방법

c.begin() C++98 시절 부터 사용되던 전통적인 방법
단점으로, ray array에는 사용을 할 수 없다.
std::begin(c) C++11 에서 추가된 방법이다.
c가 raw array라도 사용이 가능하다.
std::range::begin(c) <ranges> 헤더 추가 필요
C++20 에서 추가된 방법으로 위 방식 보다 좀 더 안전한 방식이라고 한다.

 

1-3. 반복자 관련 유의사항

#include <iostream>
#include <vector>

int main()
{
    std::vector v1 = {1, 2, 3, 4, 5};
    
    auto p1 = v1.begin();
    auto p2 = v1.end();
    
    *p1 = 10;
    *p2 = 20; // runtime error
}

- begin()은 시작 주소를 가리키는데 반해, end()는 마지막 요소가 아닌 마지막 다음 요소를 가리키는 반복자이다.

- 따라서 * 연산을 하면 안된다. runtime error를 발생시킨다.

- c.begin() 등의 방식으로 꺼낸 반복자가 구간에 끝에 도달했는지 조사하는 용도로 사용된다.

- 대략 위와 같은 형태라고 볼 수 있다.

while( p1 != p2 )
{
    std::cout << *p1 << " ";
    ++p1;
}

 

1-4. reverse iterator

#include <iostream>
#include <vector>

int main()
{
    std::vector v1 = {1,2,3,4,5};
    
    auto p1 = v1.begin();
    auto p2 = v1.end();
    
    while(p1 != p2)
    {
         std::cout << *p1 << " ";
         ++p1;
    }
}

- 이는 일반적인 정방향 순회의 예제이다.

- iterator는 정방향 뿐만 아니라 역방향도 제공한다.

 

#include <iostream>
#include <vector>

int main()
{
    std::vector v1 = {1,2,3,4,5};
    
    auto p1 = v1.rbegin();
    auto p2 = v1.rend();
    
    while( p1 != p2 )
    {
         std::cout << *p1 << std::endl;
         ++p1;
    }
}

- 역반복자(reverse iterator)란, 컨테이너의 요소를 거꾸로 순회할때 사용한다.

- 역반복자를 사용하면 기존에 작성된 정방향으로 동작하는 알고리즘을 코드 수정없이 역방향으로 동작하게 할 수 있다.


2. ranges

 

2-1. range - for 

#include <iostream>
#include <vector>

int main()
{
    std::vector v = {1,2,3,4,5};
    
    for ( auto e : v )
          std::cout << e << std::endl;
}

- 컨테이너의 모든 요소를 열거할 때 range - for를 사용하면 편리하게 열거 가능하다.

- 단, 유의할 점은 이와 같이 반복문을 이용해 모든 값을 수정한다면 참조를 사용해 수정해야한다.

#include <iostream>
#include <vector>

int main()
{
    std::vector v = {1,2,3,4,5};
    
    for ( auto& e : v )
          std::cout << e << std::endl;
}

 - 이와 같이 사용할 수 있다.

 

※ 일부 요소만 열거 하고 싶은 경우

① C++20 에서 추가된 view 개념 사용

② legacy for 문과 [] 연산자

③ legacy for 문과 반복자

 

2-1. std::views

 

2-1-1. view를 이용한 정방향 열거

#include <iostream>
#include <vector>
#include <ranges> // C++20

int main()
{
    std::vector v = {1,2,3,4,5};
    
    for ( auto& e : std::views::take(v, 3) )
    {
         std::cout << e << std::endl;
    }    
}

 

2-1-2. view를 이용한 역방향 열거

#include <iostream>
#include <vector>
#include <ranges> // C++20

int main()
{
    std::vector v = {1,2,3,4,5};
    
    for ( auto& e : std::views::take(v, 5) ) // 순차적으로 5개
    {
         std::cout << e << std::endl;
    }    
    
    for ( auto& e : std::views::reverse(v, 5) ) // 역순으로 5개
    {
         std::cout << e << std::endl;
    }        
}

 

2-1-3. view를 이용한 중간 범위 삭제 후 열거

#include <iostream>
#include <vector>
#include <ranges> // C++20

int main()
{
    std::vector v = {1,2,3,4,5};
    
    for ( auto& e : std::views::drop(v, 5) ) // 5개를 삭제 후 열거 즉, 6,7,8,9,10
    {
         std::cout << e << std::endl;
    }    
    
}

- 이를 이용하면, 앞에 있는 데이터 5개를 제외시키고, 열거한다.

 

2-1-4. 특정 조건이 만족하는 것에 대한 열거

#include <iostream>
#include <vector>
#include <ranges> // C++20

bool foo(int n) { return n % 2 == 0; }
int main()
{
    std::vector v = {1,2,3,4,5};
    
    for ( auto& e : std::views::filter(v, foo) ) // 모든 요소 중 참을 리턴하는 것
    {
         std::cout << e << std::endl;
    }    
 
    for ( auto& e : std::views::filter(v, [](int n) { return n % 2 == 0; } ) ) // 동일
    {
         std::cout << e << std::endl;
    }    
    
}

- 특정 조건이 만족하는 즉, 참을 리턴하는 것만 뽑아낼 수 있다.

- 위와 같이 활용할 수 있다. 위 아래는 모두 동일한 결과를 리턴한다.

 

2-2. legacy for 문과 [] 연산자

#include <iostream>
#include <vector>
#include <ranges> // C++20

int main()
{
    std::vector v = {1,2,3,4,5};
    
    for (int i = 0; i < 3; i++)
    {
        v[i] = 0; // list 지원 안됨.
    }
}

- 이렇게 사용할 경우 list는 지원이 안된다는 문제가 존재한다.

 

2-3. legacy for 문과 반복자

#include <iostream>
#include <vector>
#include <ranges> // C++20

int main()
{
    std::vector v = {1,2,3,4,5};
    
    for (int i = 0; i < 3; i++)
    {
    //   v[i] = 0; // list 지원 안됨.
         *p = 0;  // 모든 컨테이너 가능.
    }
}

- 이와 같이 반복자를 사용하면 모든 컨테이너에 대해 지원이 된다.

 

 

'C++ > Basic' 카테고리의 다른 글

[C++] Exception  (0) 2024.08.29
[C++] Algorithm  (3) 2024.08.28
[C++] STL Container II  (0) 2024.08.27
[C++] STL Container I  (0) 2024.08.27
[C++] Operator Overloading IV  (2) 2024.08.26
Comments