C++ string_view와 문자열 최적화
C++에서 문자열 처리의 가장 흔한 성능 낭비는 불필요한 복사입니다. 함수에 std::string을 값으로 전달하면 항상 복사가 일어납니다. std::string_view는 문자열 데이터를 소유하지 않고 참조만 하는 경량 뷰(view)로, 복사 없이 문자열을 다룰 수 있게 해줍니다.
1. string_view의 구조
섹션 제목: “1. string_view의 구조”std::string_view는 내부적으로 두 개의 멤버만 가집니다.
// 개념적 구현 (실제 구현은 플랫폼마다 다름)class string_view { const char* _data; // 문자열 시작 포인터 size_t _size; // 길이};sizeof(std::string_view) == 16 bytes (64비트 기준), sizeof(std::string) == 24~32 bytes. 스택 복사 비용도 낮고, 힙 할당이 전혀 없습니다.
2. 기본 사용
섹션 제목: “2. 기본 사용”#include <string_view>#include <string>#include <iostream>
// 나쁜 예: const char* 리터럴을 받을 때 std::string 파라미터 → 복사 발생void PrintBad(std::string name) { std::cout << name; }
// 좋은 예: string_view는 복사 없이 std::string, const char*, 리터럴 모두 수용void PrintGood(std::string_view name) { std::cout << name; }
int main(){ std::string s = "Alice"; const char* c = "Bob";
PrintGood(s); // 복사 없음 PrintGood(c); // 복사 없음 PrintGood("Charlie"); // 복사 없음, 리터럴 직접 PrintGood({s.data() + 1, 3}); // "lic" — 부분 문자열도 복사 없음}3. 부분 문자열 처리
섹션 제목: “3. 부분 문자열 처리”std::string::substr()은 항상 새 std::string을 힙에 할당합니다. string_view::substr()은 복사 없이 뷰의 범위만 좁힙니다.
#include <string_view>#include <cassert>
void ParseToken(std::string_view input){ // "key=value" 형식 파싱 size_t eq = input.find('='); if (eq == std::string_view::npos) return;
std::string_view key = input.substr(0, eq); // 복사 없음 std::string_view value = input.substr(eq + 1); // 복사 없음
// std::string이 필요한 경우에만 변환 std::string keyStr{key};}
// 성능 비교 (루프 100만 회)// string::substr → ~45ms (힙 할당 포함)// string_view::substr → ~3ms (포인터 연산만)4. 댕글링(Dangling) 위험
섹션 제목: “4. 댕글링(Dangling) 위험”string_view는 원본 문자열의 수명에 전적으로 의존합니다. 원본이 소멸하면 뷰는 무효화됩니다.
// 위험: 임시 string의 뷰를 저장std::string_view GetView(){ std::string temp = "hello"; // 함수 종료 시 소멸 return temp; // 댕글링 뷰 반환 — UB!}
// 위험: string이 재할당되면 뷰 무효화std::string s = "hello";std::string_view sv = s;s += " world"; // 재할당 발생 → sv는 댕글링std::cout << sv; // UB
// 안전: 뷰를 저장하려면 원본의 수명이 뷰보다 길어야 함class Config { std::string _raw; // 원본 소유 std::string_view _section; // 원본 일부를 가리킴
public: void Load(std::string raw) { _raw = std::move(raw); _section = std::string_view(_raw).substr(0, 10); // 안전 }};5. null-termination 주의
섹션 제목: “5. null-termination 주의”string_view는 null 종단 문자를 보장하지 않습니다. C API에 넘길 때 주의가 필요합니다.
std::string_view sv = "hello world";std::string_view sub = sv.substr(0, 5); // "hello", null 없음
// 위험: C API는 null-termination을 기대FILE* f = fopen(sub.data(), "r"); // sub.data() == "hello world\0" 전체를 가리킴 // 우연히 동작하지만 보장 안 됨
// 안전: C API에는 std::string으로 변환std::string name{sub};FILE* f2 = fopen(name.c_str(), "r"); // 안전6. C++23 std::string_view 개선 사항
섹션 제목: “6. C++23 std::string_view 개선 사항”// C++23: string_view에서 직접 std::string 생성 (명시적 변환 생성자)std::string_view sv = "test";std::string s(sv); // C++17도 가능
// C++23: contains() 메서드 추가if (sv.contains("es")) { /* ... */ }
// C++23: std::string::substr(string_view) 오버로드std::string s2 = "hello world";auto view = std::string_view(s2).substr(6); // "world"7. 함수 시그니처 가이드
섹션 제목: “7. 함수 시그니처 가이드”// 읽기 전용 처리 → string_viewvoid Process(std::string_view text);
// 수정이 필요 → std::string& 또는 std::stringvoid Modify(std::string& text);
// 소유권 이전 → std::string (값)void Store(std::string text); // 이동으로 전달
// 반환값: string_view를 반환하면 수명 관리 주의 필요// 로컬 변수나 임시 객체의 뷰를 반환하면 UBstd::string_view SafePrefix(const std::string& s, size_t n){ return std::string_view(s).substr(0, n); // s의 수명이 호출자에게 있으므로 안전}8. string_view 유용한 멤버 함수 정리
섹션 제목: “8. string_view 유용한 멤버 함수 정리”#include <string_view>
std::string_view sv = " Hello, World! ";
// 검색sv.find("World"); // 9 (찾은 위치)sv.rfind('l'); // 11 (뒤에서 찾기)sv.find_first_of("aeiou"); // 모음 중 첫 번째 위치sv.find_first_not_of(" "); // 공백이 아닌 첫 번째 위치
// 접두/접미 제거 (C++20)sv.remove_prefix(2); // 앞 2글자 제거: "Hello, World! "sv.remove_suffix(2); // 뒤 2글자 제거: "Hello, World!"
// 비교sv.starts_with("Hello"); // C++20: truesv.ends_with("!"); // C++20: truesv.contains("World"); // C++23: true
// 부분 추출sv.substr(7, 5); // "World" (복사 없이 뷰 범위만 좁힘)
// 빈 문자열 체크sv.empty(); // falsesv.size(); // 13sv.length(); // 13 (same as size)sv.data(); // const char* 포인터 (null 종단 미보장)9. 실전 파싱 — CSV 파싱 예제
섹션 제목: “9. 실전 파싱 — CSV 파싱 예제”#include <string_view>#include <vector>#include <charconv>
// string_view로 복사 없는 CSV 파싱std::vector<std::string_view> split_csv(std::string_view line){ std::vector<std::string_view> tokens; size_t start = 0;
while (start < line.size()) { size_t comma = line.find(',', start); if (comma == std::string_view::npos) comma = line.size();
tokens.push_back(line.substr(start, comma - start)); start = comma + 1; } return tokens; // 원본 line의 뷰만 반환 — 힙 할당 없음}
// 숫자 파싱: string_view → int (복사 없음)int parse_int(std::string_view sv) { int result = 0; std::from_chars(sv.data(), sv.data() + sv.size(), result); return result;}
// 사용std::string csv_line = "Alice,30,Engineer";auto fields = split_csv(csv_line);// fields[0] = "Alice", fields[1] = "30", fields[2] = "Engineer"// 모두 csv_line을 가리키는 뷰 — 복사 없음
// 주의: csv_line이 소멸하면 fields의 모든 원소가 댕글링std::string_view는 16바이트 포인터+크기 쌍으로, 어떤 문자열 소스에서도 복사 없이 뷰를 만든다.- 읽기 전용 문자열 파라미터는
const std::string&대신std::string_view를 사용한다. - 원본 문자열의 수명이 뷰보다 반드시 길어야 한다.
std::string이 재할당되거나 임시 객체가 소멸하면 뷰는 댕글링 상태가 된다. - C API처럼 null-termination이 필요한 곳에는
std::string{view}로 변환해 넘긴다. - C++20
starts_with/ends_with, C++23contains로 가독성 좋은 문자열 검사가 가능하다.