콘텐츠로 이동

C++ string_view와 문자열 최적화

C++에서 문자열 처리의 가장 흔한 성능 낭비는 불필요한 복사입니다. 함수에 std::string을 값으로 전달하면 항상 복사가 일어납니다. std::string_view는 문자열 데이터를 소유하지 않고 참조만 하는 경량 뷰(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. 스택 복사 비용도 낮고, 힙 할당이 전혀 없습니다.


#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" — 부분 문자열도 복사 없음
}

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 (포인터 연산만)

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); // 안전
}
};

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"); // 안전

// 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"

// 읽기 전용 처리 → string_view
void Process(std::string_view text);
// 수정이 필요 → std::string& 또는 std::string
void Modify(std::string& text);
// 소유권 이전 → std::string (값)
void Store(std::string text); // 이동으로 전달
// 반환값: string_view를 반환하면 수명 관리 주의 필요
// 로컬 변수나 임시 객체의 뷰를 반환하면 UB
std::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}로 변환해 넘긴다.