콘텐츠로 이동

STL string & regex

std::string은 C++에서 가장 자주 사용되는 컨테이너 중 하나입니다. 내부 SSO(Small String Optimization)와 힙 할당 동작을 이해하면 불필요한 복사를 줄일 수 있습니다. std::regex는 C++11에서 표준화된 정규표현식 라이브러리로, 파싱·검증·치환 작업에 활용됩니다.


대부분의 STL 구현(libstdc++, MSVC STL)은 짧은 문자열을 힙 할당 없이 객체 내부 버퍼에 저장합니다.

#include <string>
#include <iostream>
std::string small = "hello"; // SSO: 힙 할당 없음 (≤15자, 구현마다 다름)
std::string large = std::string(100, 'x'); // 힙 할당 발생
// capacity와 size 확인
std::string s;
s.reserve(100); // 미리 힙 할당해 재할당 방지
for (int i = 0; i < 100; i++)
s += 'a'; // reserve 없으면 여러 번 재할당 발생
std::string s = "Hello, World!";
// 검색
s.find("World"); // 7 (없으면 std::string::npos)
s.rfind('l'); // 10 (뒤에서 검색)
s.find_first_of("aeiou"); // 1 ('e')
s.find_first_not_of("Helo, "); // 7 ('W')
// 부분 문자열
s.substr(7, 5); // "World"
// 수정
s.replace(7, 5, "C++"); // "Hello, C++!"
s.erase(5, 2); // "HelloWorld!" (", " 제거)
s.insert(5, ", "); // "Hello, World!"
// 변환
std::string num = std::to_string(42);
int n = std::stoi("123");
double d = std::stod("3.14");
// 대소문자 (표준 없음, 수동 처리)
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
std::vector<std::string> Split(const std::string& str, char delim)
{
std::vector<std::string> tokens;
std::string token;
std::istringstream stream(str);
while (std::getline(stream, token, delim))
tokens.push_back(std::move(token));
return tokens;
}
auto parts = Split("a,b,c,d", ',');
// {"a", "b", "c", "d"}

#include <regex>
#include <string>
#include <iostream>
std::string email = "[email protected]";
std::regex emailPattern(R"([\w.]+@[\w.]+\.[a-z]{2,})");
// 전체 문자열이 패턴과 일치하는지
if (std::regex_match(email, emailPattern))
std::cout << "유효한 이메일\n";
// 문자열 일부에서 패턴 탐색
std::string log = "[2024-01-15] Error: file not found";
std::regex datePattern(R"(\d{4}-\d{2}-\d{2})");
std::smatch match;
if (std::regex_search(log, match, datePattern))
std::cout << "날짜: " << match[0] << "\n"; // "2024-01-15"
std::string url = "https://www.example.com:8080/path";
std::regex urlPattern(R"((https?)://([^/:]+)(?::(\d+))?(/.*)?)");
std::smatch m;
if (std::regex_match(url, m, urlPattern))
{
std::cout << "프로토콜: " << m[1] << "\n"; // "https"
std::cout << "호스트: " << m[2] << "\n"; // "www.example.com"
std::cout << "포트: " << m[3] << "\n"; // "8080"
std::cout << "경로: " << m[4] << "\n"; // "/path"
}
std::string text = "The year 2023 and 2024 are recent";
std::regex yearPattern(R"(\d{4})");
// 모든 연도를 "YYYY"로 치환
std::string result = std::regex_replace(text, yearPattern, "YYYY");
// "The year YYYY and YYYY are recent"
// 캡처 그룹을 활용한 치환
std::string csv = "Smith,John,42";
std::regex csvPattern(R"((\w+),(\w+),(\d+))");
std::string formatted = std::regex_replace(csv, csvPattern, "$2 $1 (age: $3)");
// "John Smith (age: 42)"
std::string data = "apple 3, banana 5, cherry 2";
std::regex itemPattern(R"((\w+)\s+(\d+))");
auto begin = std::sregex_iterator(data.begin(), data.end(), itemPattern);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it)
{
const std::smatch& m = *it;
std::cout << m[1] << ": " << m[2] << "\n";
}
// apple: 3
// banana: 5
// cherry: 2

std::regex는 런타임에 패턴을 컴파일합니다. 핫 경로에서 매번 std::regex 객체를 생성하면 심각한 성능 저하가 발생합니다.

// 나쁜 예: 매 호출마다 패턴 컴파일
bool IsValidEmail(const std::string& email)
{
std::regex pattern(R"([\w.]+@[\w.]+\.[a-z]{2,})"); // 매번 컴파일!
return std::regex_match(email, pattern);
}
// 좋은 예: 정적 또는 멤버 변수로 한 번만 컴파일
bool IsValidEmail(const std::string& email)
{
static const std::regex pattern(R"([\w.]+@[\w.]+\.[a-z]{2,})");
return std::regex_match(email, pattern);
}

성능이 중요한 프로덕션 코드라면 std::regex 대신 수동 파싱이나 서드파티 라이브러리(PCRE2, RE2)를 검토합니다.


#include <ranges>
#include <string>
std::string s = " hello world ";
// C++20: 공백 제거
auto trimmed = s
| std::views::drop_while(::isspace)
| std::views::reverse
| std::views::drop_while(::isspace)
| std::views::reverse;
// 문자열 뷰로 분할 (C++20 split_view)
std::string csv = "a,b,c,d";
for (auto token : csv | std::views::split(','))
{
std::string_view sv(token.begin(), token.end());
std::cout << sv << '\n';
}

  • std::string은 SSO로 짧은 문자열을 힙 없이 저장하므로, 짧은 문자열 연산은 생각보다 빠르다.
  • 반복적으로 +=를 사용할 때는 reserve()로 재할당을 방지한다.
  • std::regex 객체는 생성 비용이 높으므로 static 또는 멤버 변수로 한 번만 컴파일한다.
  • 캡처 그룹은 std::smatch의 인덱스 또는 $1, $2로 접근하며, 치환 시 $&는 전체 매치를 의미한다.