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의 수명이 호출자에게 있으므로 안전}std::string_view는 16바이트 포인터+크기 쌍으로, 어떤 문자열 소스에서도 복사 없이 뷰를 만든다.- 읽기 전용 문자열 파라미터는
const std::string&대신std::string_view를 사용한다. - 원본 문자열의 수명이 뷰보다 반드시 길어야 한다.
std::string이 재할당되거나 임시 객체가 소멸하면 뷰는 댕글링 상태가 된다. - C API처럼 null-termination이 필요한 곳에는
std::string{view}로 변환해 넘긴다.