무엇인가
컨테이너의 내부 구조를 감춘 채 원소를 차례로 훑을 수단을 준다.
앞의 두 편(Command·Strategy)은 접으면 이득만 있었다. 이 편은 다르다. 접을 수는 있는데 잃는 것이 분명히 있다. 그래서 이 갈래에서 가장 중요한 문서다.
구조
바깥에서 도는 방식과 안에서 도는 방식이 있다.
graph TD subgraph 외부 반복 U1[사용하는 쪽] -->|++, != 를 직접| C1[컨테이너] end subgraph 내부 반복 U2[사용하는 쪽] -->|클로져를 넘긴다| C2[컨테이너] C2 -->|원소마다 호출| U2 end
루프를 누가 도는가가 갈린다. 위는 사용하는 쪽, 아래는 컨테이너 쪽이다.
외부 반복 (클래스판)
반복자를 만들어 돌려주고, 도는 일은 사용하는 쪽이 한다.
#include <iostream>
#include <string>
#include <vector>
struct Person {
std::string name;
int age;
};
class Persons {
std::vector<Person> persons_;
public:
void add(Person p) { persons_.push_back(std::move(p)); }
using iterator = std::vector<Person>::const_iterator;
iterator begin() const { return persons_.begin(); }
iterator end() const { return persons_.end(); }
};
int main() {
Persons ps;
ps.add({"수원", 36});
ps.add({"지훈", 17});
ps.add({"민아", 42});
for (const auto& p : ps) std::cout << p.name << ' ';
std::cout << '\n';
// 도는 쪽이 흐름을 쥐고 있다 — 중간에 멈출 수 있다
for (const auto& p : ps) {
if (p.age < 20) {
std::cout << "미성년 발견: " << p.name << " — 중단\n";
break;
}
}
}begin()/end()만 있으면 범위 기반 for가 그냥 돈다. C++에서는 이게 관용이다.
내부 반복 (람다판)
컨테이너가 돌고, 원소마다 클로져를 부른다.
#include <functional>
#include <iostream>
#include <string>
#include <vector>
struct Person {
std::string name;
int age;
};
class Persons {
std::vector<Person> persons_;
public:
void add(Person p) { persons_.push_back(std::move(p)); }
void each(const std::function<void(const Person&)>& fn) const {
for (const auto& p : persons_) fn(p);
}
};
int main() {
Persons ps;
ps.add({"수원", 36});
ps.add({"지훈", 17});
ps.add({"민아", 42});
ps.each([](const Person& p) { std::cout << p.name << ' '; });
std::cout << '\n';
// 중간에 멈추고 싶다면? each는 끝까지 돈다.
// 플래그로 흉내 내는 수밖에 없다 — 순회 자체는 계속된다.
bool done = false;
ps.each([&done](const Person& p) {
if (done) return;
if (p.age < 20) {
std::cout << "미성년 발견: " << p.name << " — 이후는 건너뛴다\n";
done = true;
}
});
}사용하는 쪽에서 루프가 사라진 것은 사실이다. 그런데 대가가 있다.
무엇을 잃는가
| 외부 반복 | 내부 반복 | |
|---|---|---|
| 중간에 멈추기 | break | 불가. 플래그로 흉내만 |
| 두 컨테이너를 나란히 | 반복자 둘을 같이 전진 | 불가 |
| 표준 알고리즘 사용 | std::find_if 등 그대로 | 직접 다시 만들어야 |
| 게으른 순회 | 필요한 만큼만 | 전부 돈다 |
| 루프 코드 | 사용하는 쪽에 있다 | 사라진다 |
마지막 한 줄만 내부 반복이 이긴다. 위 네 줄은 전부 잃는 쪽이다.
특히 break가 안 되는 것이 결정적이다 — 위 예제에서 “찾으면 그만”이 안 되고
끝까지 돌면서 플래그를 검사한다. 원소가 백만 개면 백만 번 다 돈다.
each가 bool을 반환하게 만들어 “false면 중단”으로 고칠 수는 있다.
void eachUntil(const std::function<bool(const Person&)>& fn) const {
for (const auto& p : persons_) {
if (!fn(p)) return;
}
}되긴 되는데, 이제 클로져의 반환값이 “계속할지 말지”라는 규약이 생겼다.
읽는 사람이 그 규약을 알아야 한다. break는 아무도 설명할 필요가 없었다.
그래서 C++에서는
반복자를 내주는 쪽이 기본이다. begin()/end()만 있으면
범위 기반 for도, 표준 알고리즘도, 뷰도 전부 공짜로 딸려 온다.
// begin/end만 있으면 이 전부가 그냥 된다
std::find_if(ps.begin(), ps.end(), [](const Person& p) { return p.age < 20; });
std::count_if(ps.begin(), ps.end(), [](const Person& p) { return p.age >= 40; });
for (const auto& p : ps) { /* ... */ }여기서도 람다는 쓰인다. 다만 반복 방식이 아니라 조건으로 쓰인다 — Strategy가 접힌 자리이지 Iterator가 접힌 자리가 아니다. 이 구분이 이 문서의 핵심이다.
언제 접고 언제 접지 않나
접지 않는다 — 기본값이다. begin()/end()를 내준다.
접는다
- 순회 중에 락을 쥐고 있어야 할 때. 반복자를 내주면 잠금 범위를 통제할 수 없지만
each는 함수 안에서 락을 잡고 놓을 수 있다. - 순회가 단순하지 않을 때(트리 순회, 여러 저장소를 이어 훑기). 반복자를 제대로 만드는 비용이 크면 콜백이 싸다.
- 내부 표현을 절대 노출할 수 없을 때.
블랙보드 패턴의 제어 루프가 두 번째 경우에 가깝다. 지식원들을 훑으며 조건을 묻는 순회라 반복자로 표현할 이유가 없다.