잇창명 개발 블로그

C++ 반복자는 왜 그렇게 헷갈리는 걸까?

이 글은 단축링크 https://eatch.dev/s/iter로도 들어올 수 있습니다.

이 글은 키보드로만 조작할 수 있는 인터랙티브 요소를 포함하고 있으며, 모바일 환경에 최적화되지 않았습니다. PC로 열람을 권장합니다.

에츠허르 다익스트라는 Why numbering should start at zero라는 서한을 남긴 적이 있습니다. 전체 내용은 별도의 보충글에 정리해 두었고, 이 중 글과 관련이 있는 부분을 발췌하면 이렇습니다.

자연수의 부분수열 2, 3, ..., 12를 끔찍한 줄임표 없이 나타낼 때, 다음과 같은 4종류의 방식을 고를 수 있다.

  • a) \(2 \le i < 13\)
  • b) \(1 < i \le 12\)
  • c) \(2 \le i \le 12\)
  • d) \(1 < i < 13\)

(중략)

자연수에는 최솟값이 존재한다. b)와 d)와 같이 하한을 제외할 경우 가장 작은 자연수부터 시작하는 부분수열의 하한이 비자연수가 되어야 한다. 이는 더러우니 하한은 a)와 c)처럼 ≤로 표기하는 것을 선호한다. 한편 가장 작은 자연수부터 시작하는 부분수열을 생각하자. 상한을 포함할 경우 부분수열이 빈 수열이 되었을 때 상한이 비자연수가 되어야 한다. 이는 더러우니 상한은 a)와 d)처럼 <로 표기하는 것을 선호한다. 이로써 방식 a)를 가장 우선하는 것으로 결론지을 수 있다.

(후략)

요즘 들어서는 \(a \le i < b\) 대신 \(a \le i < a + l\)으로 타협(?)하는 분위기가 보이고 있는데 개인적으로 이 글의 논지에 꽤 공감하는 편입니다.

제목에 C++ 반복자를 써놓고 갑자기 이 글을 인용한 이유는, C++ 반복자가 헷갈리는 이유를 이 글의 내용으로 어느 정도 설명할 수 있기 때문입니다.

완벽하지는 않네요.

이 네 가지 의문점이 제가 STL을 처음 배우면서 가장 헷갈렸던 부분인데, 위 글의 내용과는 별개로 네이버 블로그에 나름대로 이유를 찾아서 올린 적이 있습니다. 이제 이 블로그를 주력으로 사용하고 있으니 해당 글의 내용을 다시 정리해서 올려보려고 합니다.

다른 멘탈 모델로 보기

다들 모르는 개념을 배우면서 멘탈 모델을 하나씩은 만드실 거라고 생각합니다. 예를 들어서 제가 반복자를 처음 배울 때의 멘탈 모델 중 하나가 마인크래프트 핫바였습니다.

마인크래프트에서 핫바의 모습. 9개의 사각형이 가로로 붙어 있고 이 중 일부에 아이템이 들어 있다. 선택되어 있는 아이템이 더 크고 밝은 사각형으로 강조되어 있다.

생각해보면 반복자와 핫바로 할 수 있는 것들이 꽤 비슷합니다.

사실 이렇게 의식적으로 생각하면서 멘탈 모델을 정했다기보다는 그동안 마인크래프트를 오래 해와서 친숙하니 반복자를 보자마자 '어 이거 마인크래프트 그거랑 비슷하다'부터 나온 것에 가깝습니다. 여러분도 비슷한 경험이 있는지 모르겠네요.

유감스럽지만, 이 멘탈 모델을 받아들이면 글의 위에서 언급한 "헷갈리는 부분"에 부딪쳐버리게 됩니다.

프로그래밍 개념을 이해하는 데 이왕이면 더 명확하고 간단한 멘탈 모델이 낫다고 설명할 필요는 아마 없을 것 같습니다(자꾸 헷갈리면 답답하잖아요!). 그러면 마인크래프트 핫바보다 반복자를 더 깔끔하게 설명할 수 있는 멘탈 모델이 있을까요? 생각보다 훨씬 가까이 있습니다. 텍스트 커서를 소개합니다.

가산 및 감산

텍스트 커서가 반복자랑 무슨 상관일까요? 일단 텍스트 커서로 뭘 할 수 있는지부터 살펴봅시다. 편의상 여기서부터는 "커서"라고만 하면 마우스 커서가 아니라 텍스트 커서를 일컫는 것으로 하겠습니다.

바로 위의 이 배열은 인터랙티브입니다! 클릭하고 좌우 방향키를 눌러보세요. 색상이 살짝 진해졌다면 포커스가 제대로 잡힌 상태입니다.

텍스트 편집을 하려면 어디를 편집하는 것인지 알 수 있어야겠죠. 좌우 방향키로 커서를 움직일 수 있습니다. 이는 양방향 반복자의 증감 연산과 동일합니다.

임의 접근

한 칸씩만 움직일 수 있으면 지루하겠죠. 마우스로 편집하고 싶은 위치를 누르면 커서가 바로 그 위치로 이동합니다. 이는 임의 접근 반복자의 덧·뺄셈 연산(특히 .begin() + x)과 동일합니다.

덮어쓰기

지금은 사라지고 있는 추세지만, 일부 텍스트 편집기나 워드 프로세서에서는 Insert 키를 눌러서 삽입 모드(Insert)와 수정 모드(Overtype)를 오갈 수 있습니다.

수정 모드에서 "stdlib"의 맨 앞에 커서를 두고 "string"으로 편집하고 있다.

삽입 모드는 우리가 항상 쓰고 있는 익숙한 모드입니다(문자와 문자 사이에 새로운 문자를 삽입함). 수정 모드로 들어가면 커서가 세로선이 아니라 블록 모양으로 반짝이고, 키보드를 치면 그 칸에 있던 문자가 지워지고 입력한 문자로 바뀝니다.

Insert를 누르고 숫자키 0-9를 눌러서(키패드 여부는 상관 없습니다) 수정 모드를 체험해볼 수 있습니다. 이는 출력 반복자의 쓰기 연산과 동일합니다(*it = x;). 이 상태에서 Insert를 한 번 더 누르면 실제 텍스트 에디터와 동일하게 숫자키를 누르면 커서가 한 칸 오른쪽으로 옮겨갑니다(*it++ = x;와 같이 작동합니다).

Insert를 한 번 더 누르면 삽입 모드로 돌아갑니다. C++에서는 배열 중간에 삽입하는 연산도 반복자로 할 수는 있지만, 이 글에서는 글의 주제에 집중하기 위해 입력을 막았습니다. 사실 배열 길이도 바뀌게 짜기 귀찮네요.

역방향 반복자

그런데 위의 배열에서 Insert를 누르고 자세히 살펴보면 세로선 커서가 있던 곳의 오른쪽이 반짝입니다. 커서를 가장 왼쪽에 두면 첫 번째 원소를 수정할 수 있고, 가장 오른쪽(즉, "past-the-end" 위치)에 두면 애초에 수정할 것이 없습니다. 반복자의 동작과 잘 들어맞네요.

수정 모드에서 커서의 오른쪽이 아니라 왼쪽이 반짝거리도록 할 수 있다면 역방향 반복자도 어느 정도 설명할 수 있을 것 같습니다. 마침 오른쪽에서 왼쪽으로 쓰는 문자 체계가 있네요. 아랍어나 히브리어 등이 여기에 속하는데, 이 언어로도 컴퓨터를 쓸 수 있어야 되기 때문에 텍스트 에디터도 여기에 대응하고 있습니다. 가령...

커서의 오른쪽 위에 방향 표시가 튀어나와 있다.

이런 차이를 알고 나면 역방향 반복자를 만들었을 때 한 칸 앞의 원소를 가리키는 것이 좀 더 납득될지도 모르겠네요. 아래의 배열에서 직접 테스트해볼 수 있습니다(R 키로 방향 전환).

범위 지정

텍스트의 일부분을 선택하는 건 다들 해보셨을 겁니다. Visual Studio Code 같은 최신 에디터는 다중 선택을 지원하기도 하지만 가장 기본적으로는 시작점부터 끝점까지 마우스로 끌어서 그 영역을 선택할 수 있습니다(이외에 Shift를 이용하는 방법이 있지만 생략합니다). 이는 반복자 2개로 범위를 지정하는 것과 동일합니다. 특히 Ctrl+A로 전체 선택은 .begin()부터 .end()까지와 동일합니다.

범위 지정도 이유 없이 하지는 않겠죠. 일단 복사를 하든지, 지우든지, 글자 크기 조정을 하든지 무언가 그 범위에서 하고 싶은 것이 있을 겁니다. <algorithm>에서 제공하는 여러 함수들이 이렇게 반복자 2개로 범위를 받고 그 안에서 연산을 합니다. 이 범위가 방식 a)와 같으니 자연스럽게 \(\left[ \mathrm{begin}, \mathrm{end} \right)\) 범위가 됩니다.

상한과 하한

std::lower_boundstd::upper_bound의 헷갈리는 정의 역시 커서 멘탈 모델로 생각해볼 수 있습니다.

즉 이 두 함수는 value보다 작은 것, value와 같은 것, value보다 큰 것을 분할해주며, 이 두 함수만을 이용해 구현한 std::equal_range는 정확히 value와 같은 범위를 돌려준다는 것을 이해할 수 있습니다. 아래 배열에서 직접 테스트해볼 수 있습니다(Alt+숫자 키로 std::equal_range만큼 선택).

복습해보기

지금까지 언급했던 모든 내용을 정리해 봅시다. 반복자와 커서로 비슷하게 할 수 있는 것들이 마인크래프트 핫바보다 훨씬 다양합니다.

이 유사점들을 전부 확인해볼 수 있도록 아래 배열에 모든 기능들을 켜 두었습니다. Alt+숫자 키를 입력하면 선택하기 전에 정렬을 먼저 수행합니다.

실제로 제가 커서 멘탈 모델을 받아들이고 난 뒤에 광명을 찾았습니다. 이외에도 다른 효능을 들자면...

다음번에 C++로 프로그래밍할 때는 커서를 떠올려보는 게 어떨까 싶네요.

C++ 반복자는 왜 그렇게 헷갈리는 걸까?