본문으로 건너뛰기 C++ Regex Iterator | '정규식 반복자' 가이드

C++ Regex Iterator | '정규식 반복자' 가이드

C++ Regex Iterator | '정규식 반복자' 가이드

이 글의 핵심

C++ Regex Iterator - "정규식 반복자" 가이드. C++ Regex Iterator의 regex_iterator란?, 기본 사용, 실전 예시를 실전 코드와 함께 설명합니다.

regex_iterator란?

std::regex_searchstd::regex_match는 문자열에서 한 번의 매치만 찾아줍니다. 하지만 로그 파일에서 모든 숫자를 뽑거나, 텍스트에서 모든 단어를 추출하는 것처럼 문자열 안에 존재하는 모든 매치를 순서대로 순회해야 하는 경우가 훨씬 많습니다. std::sregex_iterator(C++11)는 이런 요구를 충족하기 위한 반복자로, 내부적으로 이전 매치가 끝난 지점부터 다시 regex_search를 호출하는 방식으로 동작하며, 표준 반복자 인터페이스(begin/end, operator++, operator*)를 그대로 제공하기 때문에 range-based for나 STL 알고리즘과도 자연스럽게 어울립니다. 아래 예제는 문자열에서 숫자로 이루어진 모든 토큰을 순서대로 출력합니다.

#include <regex>

std::string text = "C++ 11, C++ 14, C++ 17";
std::regex pattern{R"(\d+)"};

auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
auto end = std::sregex_iterator();

for (auto it = begin; it != end; ++it) {
    std::cout << it->str() << std::endl;
}
// 11
// 14
// 17

기본 사용

sregex_iterator를 사용하는 기본 패턴은 항상 동일합니다. 순회를 시작할 반복자(begin)는 대상 문자열의 시작·끝 이터레이터와 정규식 객체를 넘겨 생성하고, 순회를 끝낼 반복자(end)는 인자 없이 기본 생성합니다. 이 “기본 생성된 반복자가 곧 끝을 의미한다”는 관용구는 C++ 표준 라이브러리 여러 곳(istream_iterator 등)에서 반복되는 패턴이므로 한 번 익혀두면 다른 곳에서도 그대로 적용할 수 있습니다. 역참조한 값은 std::smatch 타입으로, 전체 매치와 캡처 그룹 정보를 모두 담고 있습니다.

#include <regex>
#include <string>

std::string text = "abc 123 def 456";
std::regex pattern{R"(\d+)"};

// 반복자 생성
auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
auto end = std::sregex_iterator();

// 순회
for (auto it = begin; it != end; ++it) {
    std::smatch match = *it;
    std::cout << match.str() << std::endl;
}

실전 예시

아래 네 가지 예시는 로그 분석, 텍스트 마이닝, 사용자 입력 검증 같은 실무에서 sregex_iterator가 실제로 어떻게 쓰이는지 보여줍니다. 패턴 자체의 문법보다는 “반복자로 순회하며 결과를 어떻게 모으는가”에 초점을 맞춰 읽으면 됩니다.

예시 1: 단어 추출

\b\w+\b는 단어 경계(\b)로 감싸인 영숫자 시퀀스를 찾는 패턴으로, 텍스트에서 구두점을 제외한 순수 단어만 뽑아낼 때 흔히 쓰입니다. extractWords는 반복자로 순회하며 매치된 문자열을 벡터에 차곡차곡 쌓아 반환하는데, 이렇게 “순회하며 컨테이너에 누적”하는 패턴은 정규식 반복자를 다루는 함수 대부분에서 반복적으로 등장합니다.

#include <regex>
#include <vector>

std::vector<std::string> extractWords(const std::string& text) {
    std::regex pattern{R"(\b\w+\b)"};
    
    auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
    auto end = std::sregex_iterator();
    
    std::vector<std::string> words;
    for (auto it = begin; it != end; ++it) {
        words.push_back(it->str());
    }
    
    return words;
}

int main() {
    auto words = extractWords("Hello, World! C++ 2026");
    
    for (const auto& word : words) {
        std::cout << word << std::endl;
    }
    // Hello
    // World
    // C
    // 2026
}

예시 2: 캡처 그룹

패턴에 괄호 ()로 표시한 부분은 캡처 그룹이 되어, 매치된 전체 문자열뿐 아니라 그 안의 부분 문자열도 개별적으로 접근할 수 있게 됩니다. match[0]은 항상 전체 매치를, match[1]부터는 괄호가 등장한 순서대로 각 그룹을 가리킵니다. 이 예제는 이메일 주소에서 사용자명과 도메인을 각각의 그룹으로 분리해 추출하는데, 로그나 설정 파일처럼 정해진 형식의 텍스트에서 특정 필드만 뽑아낼 때 이 패턴이 매우 유용합니다.

#include <regex>

int main() {
    std::string text = "user1@example.com, user2@test.org";
    std::regex pattern{R"((\w+)@(\w+\.\w+))"};
    
    auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
    auto end = std::sregex_iterator();
    
    for (auto it = begin; it != end; ++it) {
        std::smatch match = *it;
        std::cout << "이메일: " << match[0] << std::endl;
        std::cout << "사용자: " << match[1] << std::endl;
        std::cout << "도메인: " << match[2] << std::endl;
        std::cout << std::endl;
    }
}

예시 3: URL 파싱

여러 개의 캡처 그룹을 조합하면 구조화된 데이터를 하나의 정규식으로 파싱할 수 있습니다. 이 예제는 프로토콜(http/https), 호스트, 경로를 각각 별도의 그룹으로 캡처해 URL 구조체에 담아 반환합니다. 텍스트 안에 여러 개의 URL이 섞여 있어도 sregex_iterator가 각각을 순서대로 찾아주므로, 한 번의 순회로 문서 전체에서 URL 목록을 뽑아낼 수 있습니다. 다만 실무에서 URL 파싱이 중요하다면 이런 정규식보다 전용 URL 파싱 라이브러리를 쓰는 편이 엣지 케이스(퍼센트 인코딩, IPv6 호스트 등) 처리에 더 안전합니다.

#include <regex>

struct URL {
    std::string protocol;
    std::string host;
    std::string path;
};

std::vector<URL> extractURLs(const std::string& text) {
    std::regex pattern{R"((https?)://([^/]+)(/[^\s]*))"};
    
    auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
    auto end = std::sregex_iterator();
    
    std::vector<URL> urls;
    for (auto it = begin; it != end; ++it) {
        std::smatch match = *it;
        urls.push_back({
            match[1].str(),  // protocol
            match[2].str(),  // host
            match[3].str()   // path
        });
    }
    
    return urls;
}

예시 4: 토큰 분할

sregex_token_iterator는 매치 자체가 아니라 매치 사이사이에 남은 부분을 순회하고 싶을 때 사용합니다. 생성자의 마지막 인자로 -1을 넘기면 “매치되지 않은 나머지 구간”을 토큰으로 반환하도록 지시하는 것인데, 여기서는 공백 패턴 \s+을 구분자로 삼아 사실상 split 함수처럼 동작합니다. std::string에는 표준 split 함수가 없기 때문에, 이 패턴은 문자열을 구분자 기준으로 나누는 실용적인 대안으로 자주 쓰입니다.

#include <regex>

std::vector<std::string> tokenize(const std::string& text) {
    std::regex pattern{R"(\s+)"};  // 공백
    
    std::sregex_token_iterator begin(text.begin(), text.end(), pattern, -1);
    std::sregex_token_iterator end;
    
    return {begin, end};
}

int main() {
    auto tokens = tokenize("Hello   World  C++");
    
    for (const auto& token : tokens) {
        std::cout << "[" << token << "]" << std::endl;
    }
    // [Hello]
    // [World]
    // [C++]
}

regex_token_iterator

sregex_token_iterator의 세 번째 생성자 인자로 넘기는 값은 “어떤 그룹을 토큰으로 뽑을지”를 지정합니다. -1은 특별한 의미로, 매치되는 부분이 아니라 매치와 매치 사이에 남는 텍스트(즉 구분자를 제외한 나머지)를 토큰으로 취급하라는 뜻입니다. 아래 예제처럼 쉼표를 구분자로 지정하면 CSV 한 줄을 필드 단위로 분리하는 것과 같은 효과를 얻을 수 있습니다. 0 이상의 값을 넘기면 반대로 해당 캡처 그룹에 매치된 텍스트만 토큰으로 반환합니다.

std::string text = "a,b,c,d";
std::regex pattern{","};

// -1: 구분자 제외
std::sregex_token_iterator begin(text.begin(), text.end(), pattern, -1);
std::sregex_token_iterator end;

for (auto it = begin; it != end; ++it) {
    std::cout << *it << std::endl;
}
// a
// b
// c
// d

자주 발생하는 문제

문제 1: 반복자 수명

sregex_iterator는 대상 문자열을 복사해서 갖고 있지 않고, 원본 문자열을 가리키는 이터레이터만 내부에 저장합니다. 따라서 반복자가 살아 있는 동안 원본 문자열도 반드시 살아 있어야 합니다. 아래 getIterator 함수처럼 지역 변수 text를 함수 안에서 만들고 그 문자열을 가리키는 반복자를 반환하면, 함수가 끝나는 순간 text가 소멸되면서 반환된 반복자는 이미 해제된 메모리를 가리키는 댕글링 상태가 됩니다. 이후 이 반복자를 역참조하면 정의되지 않은 동작(UB)이 발생합니다.

// ❌ 댕글링
auto getIterator() {
    std::string text = "hello 123";
    std::regex pattern{R"(\d+)"};
    return std::sregex_iterator(text.begin(), text.end(), pattern);
    // text 소멸
}

// ✅ 문자열 수명 보장
std::string text = "hello 123";
auto it = std::sregex_iterator(text.begin(), text.end(), pattern);

문제 2: 성능

std::regex 객체를 생성하는 것은 단순한 값 초기화가 아니라 패턴 문자열을 파싱하고 내부 상태 기계(state machine)로 컴파일하는 상대적으로 무거운 연산입니다. 반복문 안에서 매번 같은 패턴으로 std::regex를 새로 만들면 이 컴파일 비용이 루프 횟수만큼 반복되어 심각한 성능 저하로 이어집니다. 패턴이 바뀌지 않는다면 반드시 루프 바깥(또는 static)에서 한 번만 컴파일하고, 그 객체를 계속 재사용해야 합니다.

// regex는 느림
std::regex pattern{R"(\d+)"};

// ❌ 매번 컴파일
for (const auto& text : texts) {
    std::regex pattern{R"(\d+)"};  // 반복 컴파일
    std::regex_search(text, pattern);
}

// ✅ 한 번만 컴파일
std::regex pattern{R"(\d+)"};
for (const auto& text : texts) {
    std::regex_search(text, pattern);
}

문제 3: 빈 매치

\d*처럼 “0개 이상”을 의미하는 수량자를 쓰면 아무 문자도 소비하지 않는 빈 매치가 발생할 수 있습니다. sregex_iterator는 빈 매치도 하나의 유효한 매치로 순회 결과에 포함시키기 때문에, 숫자가 없는 위치마다 빈 문자열 매치가 끼어들어 결과 목록이 예상보다 훨씬 많아집니다. 아래처럼 빈 문자열을 걸러내는 방어 코드를 추가하거나, 애초에 \d+(1개 이상)처럼 빈 매치가 나올 수 없는 패턴을 쓰는 것이 더 안전합니다.

std::string text = "hello";
std::regex pattern{R"(\d*)"};  // 0개 이상

// 빈 매치 가능
auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
auto end = std::sregex_iterator();

for (auto it = begin; it != end; ++it) {
    if (!it->str().empty()) {
        std::cout << it->str() << std::endl;
    }
}

문제 4: 그룹 인덱스

캡처 그룹 인덱스는 패턴에서 여는 괄호 (가 등장하는 순서대로 1부터 매겨지며, 인덱스 0은 항상 전체 매치를 가리키는 예약된 값입니다. 그룹이 여러 개 중첩되어 있으면 어떤 괄호가 몇 번 그룹인지 헷갈리기 쉬운데, 이럴 때는 C++20부터 지원되는 이름 있는 캡처 그룹((?<name>...), 구현체 지원 여부 확인 필요)을 쓰거나, 주석으로 각 그룹의 의미를 명시해 두는 것이 유지보수에 도움이 됩니다. 존재하지 않는 인덱스에 접근하면 예외가 아니라 빈 sub_match가 반환되므로 matched 멤버로 유효성을 확인하는 습관도 중요합니다.

std::string text = "user@example.com";
std::regex pattern{R"((\w+)@(\w+)\.(\w+))"};

std::smatch matches;
if (std::regex_match(text, matches, pattern)) {
    // matches[0]: 전체 매치
    // matches[1]: 첫 번째 그룹
    // matches[2]: 두 번째 그룹
    // ...
}

활용 패턴

<regex> 헤더가 제공하는 함수와 반복자는 목적에 따라 명확히 구분해서 써야 합니다. 전체 문자열이 패턴과 정확히 일치하는지 검사할 때는 regex_match, 문자열 어딘가에 패턴이 존재하는지(그리고 그 위치의 캡처 그룹)를 알고 싶을 때는 regex_search, 매치된 부분을 다른 문자열로 바꾸고 싶을 때는 regex_replace를 사용합니다. 그리고 지금까지 다룬 것처럼 문자열 안의 모든 매치를 순서대로 훑어야 한다면 sregex_iterator가 적합합니다. 아래 코드는 이 네 가지 용도를 한눈에 비교할 수 있도록 정리한 것입니다.

// 1. 검증
bool isValid = std::regex_match(text, pattern);

// 2. 검색
std::smatch matches;
std::regex_search(text, matches, pattern);

// 3. 치환
auto result = std::regex_replace(text, pattern, replacement);

// 4. 모든 매치
auto it = std::sregex_iterator(begin, end, pattern);

정규식 반복 매칭의 동작

std::regex_search한 번에 한 구간만 찾습니다. 문자열 안의 모든 비중첩 매치를 순회하려면 std::regex_iterator(문자 시퀀스용) / std::sregex_iterator(std::string 반복자용)를 씁니다. 반복자는 내부적으로 이전 매치 끝 다음 위치부터 다시 regex_search를 호출하는 패턴으로 구현됩니다.

  • 전역 매치가 아님: 기본 반복자는 부분 일치를 나열합니다. 전체 문자열이 패턴과 정확히 일치하는지 보려면 regex_match를 쓰세요.
  • 중첩·겹침: 표준 반복자는 일반적으로 다음 검색 시작점을 매치 끝으로 옮기므로, 겹치는 패턴(예: aaa)은 요구사항에 따라 별도 설계가 필요합니다.

sregex_iterator 타입 계열

  • sregex_iterator: std::string::const_iterator 범위 + std::regex로 초기화.
  • cregex_iterator: const char* 범위용.
  • wsregex_iterator: std::wstring용.

종료 반복자는 std::sregex_iterator() 기본 생성으로 두는 관용구가 널리 사용됩니다. smatchready()로 유효한 매치를 담고 있는지, size()로 캡처 그룹 개수(그룹 0 포함)를 확인할 수 있으며, str(n)으로 n번째 그룹의 문자열을 바로 얻을 수 있어 matches[n].str()보다 간결하게 쓸 수 있습니다.

auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
auto end   = std::sregex_iterator();
for (auto it = begin; it != end; ++it) {
    const std::smatch& m = *it;
    // m.ready(), m.size(), m.str(n)
}

실전: 로그 파싱 예시

아래는 타임스탬프·레벨·메시지를 한 줄에서 선택는 예시입니다(패턴은 로그 포맷에 맞게 조정).

#include <regex>
#include <string>
#include <iostream>
#include <vector>

struct LogLine {
    std::string timestamp;
    std::string level;
    std::string message;
};

// 예: "2026-03-30 12:00:00 ERROR something failed"
bool parseLogLine(const std::string& line, LogLine& out) {
    static const std::regex re(
        R"((\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (\w+) (.*))");
    std::smatch m;
    if (!std::regex_match(line, m, re)) {
        return false;
    }
    out.timestamp = m[1].str();
    out.level = m[2].str();
    out.message = m[3].str();
    return true;
}

// 여러 줄에서 특정 레벨만 모으기
std::vector<std::string> extractErrors(const std::string& text) {
    std::regex levelLine(R"(\b(ERROR|CRITICAL)\b.*)");
    auto begin = std::sregex_iterator(text.begin(), text.end(), levelLine);
    auto end = std::sregex_iterator();
    std::vector<std::string> errors;
    for (auto it = begin; it != end; ++it) {
        errors.push_back(it->str());
    }
    return errors;
}

: 한 파일을 통째로 string에 올린 뒤 줄 단위로 나누면 regex_iterator는 “줄 안의 여러 토큰”에, 줄 단위 루프는 “레코드 경계”에 쓰기 좋습니다.

성능 주의사항

  • 컴파일 비용: std::regex 생성자는 패턴을 컴파일합니다. 루프 밖에서 한 번만 만들고 재사용하세요.
  • 엔진: GCC/LLVM의 std::regex매우 큰 입력이나 복잡한 패턴에서 기대보다 느릴 수 있습니다. 핫 경로면 프로파일링 후 Boost.Regex, RE2 스타일 라이브러리, 또는 수동 파서를 검토합니다.
  • 할당: smatch/sregex_iterator 사용은 내부적으로 부분 문자열을 만들 수 있습니다. 대량 로그에서는 string_view 기반 커스텀 스캐너나 고정 버퍼 파서가 더 나을 수 있습니다.
  • std::regex_constants::optimize: 구현에 따라 최적화를 힌트할 수 있으나, 항상 빨라진다는 보장은 없음—측정이 우선입니다.

FAQ

Q1: Regex는?

A: 정규 표현식 (C++11).

Q2: 반복자?

A: regex_iterator 모든 매치.

Q3: 캡처 그룹?

A: () 사용. matches[N].

Q4: 성능?

A: 느림. 컴파일 재사용.

Q5: 토큰 분할?

A: regex_token_iterator.

Q6: 학습 리소스는?

A:

  • “Mastering Regular Expressions”
  • cppreference.com
  • “C++ Primer”

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글


이 글에서 다루는 키워드 (관련 검색어)

C++, regex, iterator, match, C++11 등으로 검색하시면 이 글이 도움이 됩니다.