iterator, 왜 사용할까?
iterator의 개념
먼저 C++로 코딩을 하신다고 하면, 반복자 iterator에 대해서 꼭 아시는게 좋습니다. iterator는 C++98 이전부터 있어왔던 포인터의 일종입니다. 매우 강력한 기능이고, 의외로 C++을 사용할 때 C와 다르게 포인터를 사용할 일이 정말 특이한 경우가 아니라면 거의 없는데, iterator는 정말 많이 사용합니다.
iterator에서 가장 많이 사용하는 메소드는 begin()
과 end()
인데, 각각 컨테이너의 시작점, 그리고 끝점 + 1 칸을 리턴합니다.
(그림 출처: cppreference vector begin, end)
end()
가 가장 끝점이 아닌, 가장 끝점에서 한 칸 더 나아간 위치를 리턴한다는 것을 주의하십시오. 그리고 iterator가 포인터인 만큼, 리턴하는 값이 주소이지 값(value) 타입이 아니란 것도 주의해야 합니다.
또한 iterator는 operator++
연산도 지원합니다. ++ 연산 시 iterator가 한 칸 더 이동합니다.
iterator를 다음과 같이 사용할 수 있습니다.
int main()
{
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::list<int> lis = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::array<int, 10> arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::string str = "Hello World";
std::cout << "Vector: ";
for (auto iter = vec.begin(); iter != vec.end(); iter++) {
std::cout << *iter << " ";
}
std::cout << std::endl;
std::cout << "List: ";
for (auto iter = lis.begin(); iter != lis.end(); iter++) {
std::cout << *iter << " ";
}
std::cout << std::endl;
std::cout << "Array: ";
for (auto iter = arr.begin(); iter != arr.end(); iter++) {
std::cout << *iter << " ";
}
std::cout << std::endl;
std::cout << "String: ";
for (auto iter = str.begin(); iter != str.end(); iter++) {
std::cout << *iter << " ";
}
std::cout << std::endl;
return 0;
}
결과:
Vector: 1 2 3 4 5 6 7 8 9 10
List: 1 2 3 4 5 6 7 8 9 10
Array: 1 2 3 4 5 6 7 8 9 10
String: H e l l o W o r l d
참고로 일반 C-스타일의 배열은 iterator를 지원하지 않습니다;; 우리가 C-스타일 어레이를 버리고 std::array
를 사용해야만 하는 이유죠.
그리고 위 예제에서 볼 수 있듯이, iterator는 list
에도 지원합니다. 또한 사실 다음 예제와 같이, map/set
, unordered_map/set
, span
등 거의 대부분의 STL 컨테이너가 모두 지원합니다.
int main()
{
std::unordered_map<int, int> hashmap;
hashmap[1] = 1;
hashmap[2] = 2;
hashmap[3] = 3;
std::unordered_set<int> hashset;
hashset.emplace(1);
hashset.emplace(2);
hashset.emplace(3);
std::map<int, int> map;
map[1] = 1;
map[2] = 2;
map[3] = 3;
std::set<int> set;
set.emplace(1);
set.emplace(2);
set.emplace(3);
std::cout << "hashmap: " << std::endl;
for (auto iter = hashmap.begin(); iter != hashmap.end(); iter++) {
std::cout << iter->first << " " << iter->second << std::endl;
}
std::cout << "hashset: " << std::endl;
for (auto iter = hashset.begin(); iter != hashset.end(); iter++) {
std::cout << *iter << std::endl;
}
std::cout << "map: " << std::endl;
for (auto iter = map.begin(); iter != map.end(); iter++) {
std::cout << iter->first << " " << iter->second << std::endl;
}
std::cout << "set: " << std::endl;
for (auto iter = set.begin(); iter != set.end(); iter++) {
std::cout << *iter << std::endl;
}
return 0;
}
결과:
hashmap:
3 3
2 2
1 1
hashset:
3
2
1
map:
1 1
2 2
3 3
set:
1
2
3
begin, cbegin, rbegin
begin()
, end()
의 예시는 위에서 살펴보았습니다.
한편 읽기 전용 iterator(const_iterator
)라 해서, iterator가 가리키는 값을 수정할 수 없는 iterator도 존재합니다. cbegin()
, cend()
가 그것입니다.
std::vector<int> vec = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto iter = vec.cbegin(); iter != vec.cend(); iter++) {
std::cout << *iter << " ";
}
이렇게 되면 값을 수정할 수 없으므로 더 안전한 프로그래밍을 할 수 있겠죠. 그리고 역방향 iterator(reverse iterator)라 해서 역방향으로 iterator가 돌 수도 있습니다.
(그림 출처: cppreference vector rbegin)
std::vector<int> vec = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto iter = vec.rbegin(); iter != vec.rend(); iter++) {
std::cout << *iter << " ";
}
결과:
10 9 8 7 6 5 4 3 2 1
수정할 수 없는 역방향 iterator의 경우 crbegin()
, crend()
라 합니다.
range base for loop
사실 만약 range base for loop을 사용했다면, 이미 iterator를 적극적으로 사용하고 계신 겁니다.
범위 기반 for loop의 내부 구조를 까보면, iterator와 완전히 동일하게 작동하거든요.
int main()
{
std::vector<int> vec = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
std::cout << "vec: ";
for (const auto &i : vec) {
std::cout << i << " ";
}
std::cout << std::endl << "map: " << std::endl;
std::unordered_map<int, int> map;
map.emplace(1, 2);
map.emplace(2, 3);
map.emplace(3, 4);
for (const auto& [key, value] : map) {
std::cout << key << " " << value << std::endl;
}
return 0;
}
결과:
vec: 1 2 3 4 5 6 7 8 9 10
map:
3 4
2 3
1 2
참고로
for (auto i : vec) {
std::cout << i << std::endl;
}
for (auto i = vec.begin(); i != vec.end(); ++i) {
std::cout << *i << std::endl;
}
위 코드는 어셈블리 구조상 완전히 동일합니다. 또한 cppreference에서 range base for loop의 구현을 보시면
(출처: cppreference range base for)
C++17 전까지만 해도 위에서 본 iterator 사용 패턴과 완벽하게 동일한 것을 알 수 있고, 그 이후에도 조금씩의 형태 변화는 있었지만 거의 동일하단 것을 알 수 있습니다.
한편 C++에서 반복문을 돌 때 가장 최적화가 잘 된 것은 range base for loop입니다. 따라서 range base for loop을 사용할 수 있는 상황이라면 최대한 range base for loop을 사용하시면 됩니다.
그런데 이 특히 vector
에서 iterator, range base for loop을 절대 사용해선 안 되는 경우가 있습니다. 바로 push
, emplace
등의 연산을 하는 경우인데 이 부분에 대한 이야기는 iterator에 push, emplace를 사용하면 안 되는 이유라는 글을 참고하세요.
iterator 정확한 선언
우리가 지금껏 iterator에 대해서 열심히 사용했는데, 전부 iterator를 auto
로만 선언해와서 정확한 선언 방식을 모를 때가 많습니다. 사실 그 선언이 너무 복잡해서 그냥 auto
로 선언하는 게 제일 베스트인데, 그래도 선언 타입은 다음과 같습니다.std::container_name::iterator iter;
container_name
은 iterator가 가리키고자 하는 컨테이너 종류입니다.
예시를 몇 가지 보여주자면
std::vector<int>::iterator v_iter = vec.begin();
std::list<int>::iterator l_iter = lst.begin();
std::deque<int>::iterator d_iter = deq.begin();
사실 워낙 길고 복잡하여서 그냥 auto 쓰는게 제일 안전하고 가독성도 좋아 보입니다.
iterator의 다른 유용한 사용 예시
iterator는 여러 cpp의 기본 제공 STL 라이브러리와 호응이 잘 맞습니다.
find
find
함수는 조건에 맞는 iterator를 반환합니다.
int main()
{
std::vector<int> vec = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto it = std::find(vec.begin(), vec.end(), 5);
if (it != vec.end())
std::cout << "Found " << *it << " at position " << std::distance(vec.begin(), it) << std::endl;
else
std::cout << "Not found" << std::endl;
return 0;
}
결과:
Found 5 at position 5
참고로 보통 iterator를 반환할 때, 만약 조건에 해당하는 대상이 없다면 보통 end()
를 반환합니다. 따라서 대상이 있는지 없는지를 체크할 때는 end()
를 찾으면 됩니다.
distance
distance
함수는 iterator 사이의 거리를 반환합니다.
int main()
{
std::vector<int> vec = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
const auto dist = std::distance(vec.begin(), vec.end());
std::cout << "Distance: " << dist << std::endl;
const auto iter5 = std::find(vec.begin(), vec.end(), 5);
const auto dist_To_5 = std::distance(vec.begin(), iter5);
std::cout << "Distance to 5: " << dist_To_5 << std::endl;
return 0;
}
결과:
Distance: 10
Distance to 5: 5
for_each
for_each
는 일종의 반복문인데, 역시나 cpp에서 가장 최적화가 잘 되어 있는 반복문 중 하나이므로 사용할 수 있으면 사용하시면 됩니다. 정확히 말하자면, for_each
는 일정 범위 내에 주어진 함수를 수행합니다.
int main()
{
std::vector<int> vec = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::for_each(vec.begin(), vec.end(), [](int i) { std::cout << i << " "; });
return 0;
}
결과:
0 1 2 3 4 5 6 7 8 9
참고로 이런 STL 라이브러리들은 C++20부터 도입된 range의 개념으로 더 강력해졌는데, 추후 글에서 이에 대해 소개할 수 있으면 소개하겠습니다.
또 다른 유용한 기능들이 많지만, 본 글은 STL과 다른 표준 라이브러리를 소개하는게 아닌 iterator에 대해서 소개하는 글이기 때문에 여기까지만 소개하겠습니다. 필요한 경우 직접 찾아보시면 될 것 같습니다.
마무리
사실 iterator는 C++에 STL 라이브러리가 도입될 때, 그러니깐 대략 C++98 부근부터 존재했었으므로 모던 C++ 카테고리에 들어가는게 맞는지는 모르겠습니다. 그런데 이 카테고리의 다른 글들이 C++의 여러 기능들을 소개하고 있으므로, 그냥 해당 카테고리 안에 편입시켰습니다.