Iterator
는 다양한 자료구조를 일관된 하나의 방법으로 접근할 수 있도록 만든 포인터와 유사한 인터페이스이다. 다양한 std의 함수들은 자료구조의 종류에 상관 없이 이터레이터를 통해서 요소에 접근할 수 있다. 포인터와 거의 유사한 연산을 수행한다.
// iter는 어떤 이터레이터이다.
// 이터레이터가 가리키는 요소의 값을 읽는다.
cout << *iter << endl;
// 이터레이터가 가리키는 요소의의 값을 수정한다.
*iter = 10;
// 다음 위치의 이터레이터가 된다.
iter++;
// 어떤 컨테이너의 시작 이터레이터와 끝의 다음 이터레이터를 반환하는 메서드
// 알고리즘은 내부적으로 이터레이터의 연산을 이용해 자신의 역할 수행한다.
sort(con.begin(), con.end());
이터레이터 자체는 타입이 아니다. 이터레이터는 concept
이라는 키워드를 이용해 정의하는데, 정의되는 내용은 이터레이터로써 갖추어야할 연산이나, 요구 사항 등이다. concept은 템플릿의 인수로 올바른 타입이 올 수 있도록 요구 사항을 지정한다. 만약 어떤 타입 T가 이러한 요구사항을 충족한다면, 타입 T를 이터레이터라고 부를 수 있고, 이터레이터가 필요한 자리에 사용할 사용할 수 있다. (개인적으로는 덕타이핑이랑 굉장히 유사하다고 느꼈다.)
각 컨테이너는 이터레이터 개념(concept)을 충족하는 이터레이터 실체 클래스를 각각 구현한다. 예를 들어 vector
컨테이너는 내부적으로 vector<T>::iterator
라는 별도의 이터레이터를 갖는 식이다. 직접 이터레이터 클래스를 만들 수 있다. 또, 포인터 타입은 이터레이터의 요구사항을 충족할 수 있는 연산을 모두 가졌기 때문에 이를 이터레이터로 사용할 수도 있다.
이터레이터는 요구하는 연산에 따라서 input_iterator
, random_access_iterator
등 다양한 이터레이터 개념이 있다. 그러나, 사용하는 입장에서 이러한 종류를 아는 것은 그다지 중요하다고 생각되지 않아 생략한다. ms 공식문서에서 자세한 정보를 얻을 수 있다.
이터레이터는 증감을 통해 각 요소들의 순서 관계를 갖는다. 따라서 어떤 이터레이터 i가 허용된 연산을 통해서 이터레이터 j에 도달할 수 있다면, 두 이터레이터는 같은 sequence를 참조한다고 한다. 이터레이터간의 연산은 같은 sequence를 참조할 때만 유효하다. 아무 연관관계가 없는 이터레이터에서의 연산은 정의되지 않는다.
iter1와 iter3는 같은 sequence를 참조한다. iter2과 iter3는 다른 sequence를 참조한다. iter4는 아무 연관관계를 갖지 않는다. 만약 이터레이터의 범위를 요구하는 함수에 iter1과 함께 iter2나 iter4를 넘겨준다면 문제가 생길 것이다.
1. 컨테이너에서 이터레이터 얻기
중요한 것은 일단 컨테이너에서 내부 요소에 대한 이터레이터를 제공한다는 것이다. 컨테이너는 이터레이터를 이용해 내부 원소들에 접근할 수 있도록 자신의 요소들의 시작과 끝을 가리키는 이터레이터를 제공한다. begin()
을 통해서 컨테이너의 시작점을, end()
를 통해서 끝의 다음 위치를 얻을 수 있다. end()
는 끝 요소가 아니라 끝의 다음을 가리킨다는 것에 주의해야 한다. 이를 past-the-end
라고 하는데, 어떤 요소도 가리키지 않고 단순히 해당 sequence가 끝났음을 의미한다. 항상 정의되지 않은 값을 가지고 있다. int arr[10]
에서 arr[10]
은 out of range라는 것과 비슷하다고 생각하면 된다.
// vector container의 이터레이터
std::vector<int> v{1,2,3};
auto b1 = v.begin(); // 1을 가리키는 이터레이터
auto e1 = v.end(); // 끝인 3의 다음을 가리키는 이터레이터
auto b2 = std::begin(v); // 메서드가 아닌 함수로도 존재한다.
auto e2 = std::end(v);
// e1++; // error!! past-the-end의 다음은 없다.
// *e2; // error!! past-the-end는 아무것도 가리키지 않는다.
end()
는 반복에서 끝을 찾을 때나, set과 같은 자료구조에서 특정 값을 찾을 때, 컨테이너 안에 해당 값이 존재하지 않음을 표현할 때 사용하기도 한다.
#include<iostream>
#include<vector>
#include<set>
using namespace std;
int main() {
vector<int> v{ 1,2,3,4,5 };
auto iter = v.begin();
while (iter != v.end()) {
cout << *iter++ << ' ';
}
cout << endl;
set<int> s{ 1,2,3,4,5 };
if (s.find(10) == s.end()) {
cout << "10은 s안에 존재하지 않음" << endl;
} else {
cout << "10이 s안에 존재함" << endl;
}
}
1 2 3 4 5
10은 set 안에 존재하지 않음
begin()
/end()
에서 파생된 두가지 기능이 더 있다. 첫 번째는 cbegin()
과 cend()
인데, 여기서 접두사 c는 const를 뜻하는 것으로 간접 참조해서 데이터를 읽을 수는 있지만, 수정할 수는 없도록 제한해서 사용하고자 할 때 사용한다.
두 번째는 rbegin()
과 rend()
이다. 접두사 r은 reverse를 뜻한다. 여기서 rbegin()
은 마지막 원소를 가리키고, rend()
는 첫번째 이전 원소를 가리킨다. r과 c를 동시해 사용한 crbegin()
과 crend()
도 있다.
vector<int> v{1,2,3,4,5};
for(auto i=v.begin(); i!=v.end(); i++) {
*i -= 1; // 0,1,2,3,4
}
// cbegin을 사용했으면 cend를 사용해야함에 주의할 것
for(auto i=v.cbegin(); i!=v.cend(); i++) {
*i += 1; // ERROR!!!!! const는 수정할 수 없음.
}
for(auto i=v.rbegin(); i!=v.rend(); i++) {
cout << *i << ' '; // 4 3 2 1 0
}
이터레이터를 제공하는 모든 타입은 범위 기반 반복문에서 사용할 수 있다.
vector<int> v{1,2,3,4,5};
for(auto& i : v) {
cout << i << ' '; // 1 2 3 4 5
}
2. 연산
이터레이터의 모든 연산은 포인터의 연산과 대부분 유사하다.
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
vector<int> v1{ 1,2,3,4 };
vector<int>::iterator iter;
// 1. 대입 연산
iter = v1.begin(); // v1.begin()값을 읽어 iter에 쓴다.
// 2. 역참조 읽기, 쓰기
// 이터레이터가 가리키는 요소의 값에 접근한다.
// 일부 이터레이터는 쓰기가 허용되지 않는다.
*iter; // vector의 첫 번째 요소인 1을 가리킨다.
*iter = 10; // vector의 첫 번째 요소인 1을 10으로 덮어 쓴다.
// 3. 증감&감소
// 일부 이터레이터는 감소를 지원하지 않는다.
++iter; // vector의 두 번쨰 요소를 가리킨다.
iter++; // vector의 세 번쨰 요소를 가리킨다.
--iter; // *iter == 2
iter--; // *iter == 10
// random access 및 정수와의 연산
iter = v1.begin();
*(iter + 2); // 3
iter[2]; // 3
iter = v1.end();
*(iter - 1); // 4
auto iter2 = v1.begin() + 2;
// 동등성 비교
iter == iter2; // false
iter != iter2; // true
// 대소 비교
iter >= iter2; // true
iter <= iter2; // false
iter < iter2; // true
iter > iter2; // false
// 이터리이터간 덧셈 뺄셈
iter - iter2; // 2
iter + iter; // error!!!! 덧셈은 불가능.
}
포인터 타입에 const를 적용해서 연산을 제한하는 것처럼, 이터레이터도 종류에 따라서 지원하는 연산이 다르다. 컨테이너에서 제공하는 모든 이터레이터는 요소의 값을 읽을 수 있으며 ++로 다음 이터레이터를 가리키게 할 수 있다. 그러나, 감소하거나, 가리키는 값을 수정하거나, random access는 지원하지 않을 수도 있다. 이는 컨테이너의 내부 자료구조에 따른다.
3. 알고리즘에 이터레이터 전달
std에서 제공하는 대부분의 알고리즘 함수들은 sequence의 시작점과 끝점을 입력을 받는다. 해당 구간을 입력받은 함수들은 해당 구간과 그 구간의 이터레이터간 관계에 따라서 알고리즘을 수행한다.
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
// sort는 오름차순으로 정렬한다.
// 일반적인 사용법
vector<int> v1{ 3,1,4,5,2 };
sort(v1.begin(), v1.end()); // 1 2 3 4 5
// vector의 부분적인 범위만 사용
vector<int> v2{ 5,4,3,1,2 };
sort(v2.begin(), v2.begin() + 2); // 4 5 3 1 2
// 정렬은 이터레이터간의 순서에 따라서 수행됨.
// 역순인 이터레이터를 사용해 역순으로 오름차순 정렬이 된다. (내림차순)
vector<int> v3{ 1,2,3,4,5 };
sort(v3.rbegin(), v3.rend()); // 5 4 3 2 1
// 포인터를 이터레이터로 사용.
int arr[] = { 8,1,5,4,2 };
sort(arr, arr + 5); // 1 2 4 5 8
}
4. 이터레이터 무효화
만약 정렬과 같은 방법으로 기존의 순서가 바꼈다면, 정렬 이전의 관계를 가지고 있는 최신화 되지 않은 이터레이터의 관계는 문제가 될 수 있다. 그래서, 이 때, 최신화 되지 않은 이터레이터는 어떠한 연관도 갖지 않도록 무효화(invalidation) 된다. 재할당이 일어날 때, 삽입 혹은 삭제로 여러 요소들의 관계가 변할 때 등과 같이 다양한 이유로 이터레이터가 무효화 될 수 있으며, 이를 신경쓰지 않는 가장 좋은 방법은 연산 후 매번 새로운 이터레이터를 사용하는 것이다.
5. 직접 구현하기
새로운 자료구조를 만들면서 이터레이터를 제공하고 싶을 때, 이터레이터를 직접 만들 수 있다. 다음은 단일 연결리스트와 이를 위한 이터레이터를 구현 예제이다.
- LinkedList.h
#pragma once
// 단일 연결 리스트의 노드
template<class T>
struct Node {
T data;
Node* next = nullptr;
Node(T data) : data(data) {};
};
template<class T>
class LinkedList {
private:
Node<T>* head = nullptr;
int size = 0;
public:
// LinkedList 내부에 정의된 이터레이터
class iterator {
private:
Node<T>* ptr;
public:
iterator(Node<T>* ptr) : ptr(ptr) {};
// 역참조 연산 정의
T& operator*() { return this->ptr->data; }
// 전위 증감 연산자 정의
iterator& operator++() {
if (ptr) ptr = ptr->next;
return *this;
}
// 비교 연산 정의
bool operator!= (const iterator & other) const {
return ptr != other.ptr;
}
bool operator== (const iterator& other) const {
return ptr == other.ptr;
}
};
public:
~LinkedList() {
while (head) {
Node<T>* temp = head;
head = head->next;
delete temp;
}
}
T& get(int index) {
if (index >= size) throw "out of range";
auto iter = this->begin();
for (int i = 0; i < index; i++) { ++iter; };
return *iter;
}
Node<T>* append_front(T val) {
auto node = new Node(val);
if (this->head) {
node->next = head;
}
this->head = node;
this->size++;
return node;
}
// 이터레이터를 반환하는 메서드
iterator begin() { return iterator(this->head); }
iterator end() { return iterator(nullptr); }
};
- main.cpp
#include<iostream>
#include<algorithm>
#include"LinkedList.h"
using namespace std;
int main() {
LinkedList<int> list;
list.append_front(1);
list.append_front(2);
list.append_front(3);
list.append_front(4);
list.append_front(5);
for (auto iter = list.begin(); iter != list.end(); ++iter) {
cout << *iter << ' '; // 5 4 3 2 1
}
cout << endl;
cout << list.get(2) << endl; // 2
// error : 비교 및 대입 관련된 연산 요구사항이 충족되지 않음.
// sort(list.begin(), list.end());
// 데이터를 채우는 함수
fill(list.begin(), list.end(), 1);
for (auto i : list) {
cout << i << ' '; // 1 1 1 1 1
}
}
5 4 3 2 1
3
1 1 1 1 1
LinkedList의 이터레이터는 역참조와 증감 연산자만 정의되어 있다. 따라서 해당 연산자만으로 요구사항을 충족하는 fill 함수나 범위 기반 for에서는 이터레이터로 사용할 수 있지만, 비교 연산이 필요한 sort에서는 요구사항을 충족하지 못했기 때문에 사용할 수 없다.