C++ std::string_view·std::span 완벽 가이드 | 제로카피 뷰·댕글링 방지
이 글의 핵심
C++ std::string_view·std::span 완벽 가이드에 대한 실전 가이드입니다. 제로카피 뷰·댕글링 방지 등을 예제와 함께 상세히 설명합니다.
들어가며: 문자열·배열을 넘길 때마다 복사가 부담된다
”함수에 넘길 때마다 std::string 복사가 발생해요”
로그 파싱, 설정 읽기, API 응답 처리처럼 문자열을 읽기만 할 때 const std::string&로 받으면 리터럴이나 const char*에서 임시 std::string이 생성됩니다. std::string_view는 복사 없이 “보기만” 하므로 할당을 제거할 수 있습니다. 마찬가지로 배열·버퍼를 (포인터, 크기) 쌍으로 넘기면 인터페이스가 불안정하고, std::span으로 연속 메모리 뷰를 명확하게 표현할 수 있습니다.
문제의 코드:
// ❌ 문제 1: const std::string& — 리터럴 전달 시 임시 string 생성
void process(const std::string& s) { /* ... */ }
process("hello"); // 임시 std::string 생성
// ❌ 문제 2: (포인터, 크기) 쌍 — 인터페이스 불안정
void parse(char* ptr, size_t len);
parse(buf.data(), buf.size()); // 순서 바꾸면 버그
string_view·span으로 해결:
// ✅ string_view: 복사 없이 문자열 "보기"
void process(std::string_view sv) { /* ... */ }
process("hello"); // 임시 없음
// ✅ span: 연속 메모리 뷰를 타입으로 표현
void parse(std::span<char> buf);
parse(buffer); // vector, array, C 배열 모두 호환
이 글을 읽으면:
- string_view로 문자열 뷰 연산·수명 관리·댕글링 방지를 할 수 있습니다.
- span으로 배열·버퍼 뷰, subspan, 수정 가능/읽기 전용 구분을 할 수 있습니다.
- 자주 하는 실수와 프로덕션 패턴을 알 수 있습니다.
개념을 잡는 비유
optional은 값이 비어 있을 수도 있는 상자, string_view·span은 원본 문자열·배열의 별명 카드처럼 소유하지 않고 범위만 가리킵니다. RAII·unique_ptr는 자동문처럼 스코프를 나가면 자원을 닫습니다.
목차
1. 실무에서 겪는 문제 시나리오
시나리오 1: 로그 파싱 시 substr 복사 폭발
문제: 로그 한 줄이 "2024-01-15 10:30:00 [INFO] User login" 형태일 때, substr로 잘라내면 매번 std::string이 생성됩니다. 100만 줄 파싱 시 100만 번 이상의 할당이 발생합니다.
// ❌ 문제: substr이 매번 복사
std::string get_timestamp(const std::string& line) {
return line.substr(0, 19); // 새 std::string 할당
}
std::string get_level(const std::string& line) {
size_t start = line.find('[') + 1;
size_t end = line.find(']');
return line.substr(start, end - start); // 또 할당
}
해결: string_view로 구간만 가리키면 할당 없습니다.
// ✅ string_view: 복사 없이 뷰만
std::string_view get_timestamp(std::string_view line) {
return line.substr(0, 19);
}
std::string_view get_level(std::string_view line) {
size_t start = line.find('[') + 1;
size_t end = line.find(']');
return line.substr(start, end - start);
}
시나리오 2: 함수 인자로 (ptr, size) 전달 시 실수
문제: C 스타일 API처럼 void process(const char* buf, size_t len)으로 받으면, 호출자가 (len, buf) 순서로 잘못 넘기거나, vector와 array를 동일하게 처리하기 어렵습니다.
// ❌ 순서 바꾸면 버그
process(buf.size(), buf.data()); // 잘못된 순서!
// ❌ vector vs array 오버로드 필요
void process(const std::vector<std::byte>& v);
void process(const std::array<std::byte, 1024>& a);
해결: std::span으로 연속 메모리 뷰를 하나의 타입으로 받습니다.
// ✅ span: vector, array, C 배열 모두 호환
void process(std::span<const std::byte> buf);
process(std::vector<std::byte>{...});
process(std::array<std::byte, 1024>{});
process(buf, len); // span(buf, len)
시나리오 3: string_view 반환 후 댕글링
문제: 함수 내부에서 지역 std::string을 만들고, 그 string_view를 반환하면, 함수가 끝나면서 string이 파괴되고 댕글링 참조가 됩니다.
// ❌ 문제: 지역 string 파괴 후 댕글링
std::string_view get_first_token() {
std::string line = read_line();
return std::string_view(line).substr(0, line.find(','));
}
auto tok = get_first_token(); // line은 이미 파괴됨
std::cout << tok; // 미정의 동작!
해결: 원본이 호출자보다 오래 유지되거나, string으로 복사해 반환합니다.
// ✅ 원본을 인자로 받아 뷰 반환
std::string_view get_first_token(std::string_view line) {
size_t pos = line.find(',');
return (pos == std::string_view::npos) ? line : line.substr(0, pos);
}
// ✅ 또는 string으로 복사 반환
std::string get_first_token_copy(const std::string& line) {
size_t pos = line.find(',');
return (pos == std::string::npos) ? line : line.substr(0, pos);
}
시나리오 4: 버퍼의 일부만 처리할 때
문제: 수신 버퍼에서 헤더 16바이트를 건너뛰고 페이로드만 처리해야 할 때, 포인터 연산과 크기 계산이 번거롭고 실수하기 쉽습니다.
// ❌ 수동 포인터·크기 관리
void handle_packet(char* buf, size_t len) {
if (len < 16) return;
char* payload = buf + 16;
size_t payload_len = len - 16;
process(payload, payload_len);
}
해결: std::span::subspan으로 뷰의 일부를 안전하게 만듭니다.
// ✅ subspan으로 페이로드 뷰
void handle_packet(std::span<char> buf) {
if (buf.size() < 16) return;
std::span<char> payload = buf.subspan(16);
process(payload);
}
시나리오 5: 문자열 비교·검색에서 불필요한 변환
문제: std::string을 인자로 받는 함수에 const char*나 리터럴을 넘기면 임시 변환이 발생합니다. 여러 오버로드를 만들면 코드가 비대해집니다.
// ❌ 오버로드 폭발
bool starts_with(const std::string& s, const std::string& prefix);
bool starts_with(const std::string& s, const char* prefix);
bool starts_with(const char* s, const std::string& prefix);
// ...
해결: string_view 하나로 모든 문자열 타입을 받습니다.
// ✅ string_view 하나로 통합
bool starts_with(std::string_view s, std::string_view prefix) {
return s.size() >= prefix.size() &&
s.compare(0, prefix.size(), prefix) == 0;
}
starts_with("hello", "hel"); // 리터럴
starts_with(std::string("hi"), "h"); // string
starts_with(sv, "pre"); // string_view
타입 선택 흐름도
flowchart TD
A[문자열/배열을 넘길 때] --> B{용도}
B -->|읽기만·복사 없음| C["std string_view"]
B -->|연속 메모리 뷰| D["std span"]
C --> E[원본 수명 확인]
D --> F[수정/읽기 전용 구분]
F -->|읽기| G["spanconst T"]
F -->|수정| H["spanT"]
2. std::string_view 완전 가이드
기본 사용법
std::string_view는 문자열을 소유하지 않고 “보기만” 하는 경량 타입입니다. std::string, const char*, 리터럴을 복사 없이 받을 수 있습니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o sv_basic sv_basic.cpp && ./sv_basic
#include <iostream>
#include <string>
#include <string_view>
void print(std::string_view sv) {
std::cout << sv << " (size=" << sv.size() << ")\n";
}
int main() {
std::string s = "Hello World";
const char* cstr = "C string";
print(s); // std::string — 복사 없음
print(cstr); // C 문자열
print("Literal"); // 리터럴
print(s.substr(0, 5)); // "Hello" — string의 substr은 복사
print(std::string_view(s).substr(0, 5)); // string_view substr — 복사 없음
return 0;
}
실행 결과:
Hello World (size=11)
C string (size=8)
Literal (size=7)
Hello (size=5)
Hello (size=5)
뷰 연산 (substr, find, remove_prefix, remove_suffix)
string_view는 std::string과 유사한 인터페이스를 제공하지만, 복사 없이 뷰만 조작합니다.
#include <string_view>
#include <iostream>
int main() {
std::string_view sv = "Hello World";
// substr: 복사 없이 구간 뷰
std::string_view sub = sv.substr(0, 5); // "Hello"
std::string_view rest = sv.substr(6); // "World"
// find, rfind, find_first_of
size_t pos = sv.find(' ');
std::cout << "Space at: " << pos << "\n"; // 5
// starts_with, ends_with (C++20)
#if __cplusplus >= 202002L
bool a = sv.starts_with("Hello"); // true
bool b = sv.ends_with("World"); // true
#endif
// remove_prefix, remove_suffix: 뷰 범위 조정 (원본 변경 없음, 뷰만 이동)
std::string_view v = "prefix_data_suffix";
v.remove_prefix(6); // "data_suffix"
v.remove_suffix(7); // "data"
return 0;
}
댕글링 참조 방지
string_view가 가리키는 원본 메모리가 string_view보다 먼저 파괴되면 댕글링입니다. 다음 규칙을 지킵니다.
| 상황 | 안전 | 위험 |
|---|---|---|
| 원본을 인자로 받아 뷰 반환 | ✅ | — |
| 지역 string의 뷰 반환 | — | ❌ |
| 멤버 string의 뷰를 멤버로 보관 | ✅ (수명 동일) | — |
| 임시 string의 뷰를 변수에 저장 | — | ❌ |
| 루프 내 지역 변수의 뷰를 컨테이너에 저장 | — | ❌ |
댕글링 방지 완전 예제 — 안전/위험 패턴을 한 번에 비교합니다:
// g++ -std=c++17 -o dangling_example dangling_example.cpp
// 주의: bad_example() 호출 시 UB — 데모용으로만
#include <string>
#include <string_view>
#include <vector>
#include <iostream>
// ❌ 위험 1: 지역 string의 뷰 반환
std::string_view bad_return() {
std::string s = "local string";
return s; // s 파괴 후 반환값 = 댕글링
}
// ❌ 위험 2: 임시에서 뷰 추출
void bad_temporary() {
std::string_view v = std::string("temp"); // 임시 파괴 → v 댕글링
std::cout << v; // UB
}
// ❌ 위험 3: 루프 내 지역 변수 뷰 저장
std::vector<std::string_view> bad_loop() {
std::vector<std::string_view> result;
for (int i = 0; i < 3; ++i) {
std::string s = "line " + std::to_string(i);
result.push_back(s); // s는 루프 끝에 파괴 → 댕글링
}
return result; // 모든 뷰가 무효
}
// ✅ 안전 1: 원본을 인자로 받아 뷰 반환
std::string_view safe_return(std::string_view input) {
return input.substr(0, 5); // input은 호출자가 소유
}
// ✅ 안전 2: 멤버 string의 뷰 — 수명 동일
struct Config {
std::string data_;
std::string_view get_prefix() const {
return std::string_view(data_).substr(0, 10);
}
};
// ✅ 안전 3: 원본과 같은 스코프
void safe_scope() {
std::string s = "hello";
std::string_view v = s;
std::cout << v << "\n"; // s가 유효한 동안만 사용
}
int main() {
std::string line = "Hello World";
auto prefix = safe_return(line); // OK: line이 main에 있음
std::cout << prefix << "\n";
Config cfg;
cfg.data_ = "config_value";
std::cout << cfg.get_prefix() << "\n"; // OK
return 0;
}
완전한 string_view 예제: 로그 파서 (제로카피)
실전 로그 파서 — 에지 케이스(빈 라인, 형식 오류) 처리 포함:
// g++ -std=c++17 -o log_parser log_parser.cpp && ./log_parser
#include <string_view>
#include <optional>
#include <iostream>
#include <cctype>
struct LogEntry {
std::string_view timestamp; // "2024-01-15 10:30:00"
std::string_view level; // "INFO", "ERROR" 등
std::string_view message; // 로그 메시지
};
// 앞뒤 공백 제거 (뷰만 조정, 복사 없음)
std::string_view trim(std::string_view sv) {
while (!sv.empty() && std::isspace(sv.front())) sv.remove_prefix(1);
while (!sv.empty() && std::isspace(sv.back())) sv.remove_suffix(1);
return sv;
}
std::optional<LogEntry> parse_log_line(std::string_view line) {
line = trim(line);
if (line.empty()) return std::nullopt;
// 형식: "2024-01-15 10:30:00 [INFO] User login"
if (line.size() < 20) return std::nullopt; // 최소 "YYYY-MM-DD HH:MM:SS ["
std::string_view ts = line.substr(0, 19); // 고정 19자
size_t level_start = line.find('[', 19);
if (level_start == std::string_view::npos) return std::nullopt;
size_t level_end = line.find(']', level_start);
if (level_end == std::string_view::npos) return std::nullopt;
std::string_view level = line.substr(level_start + 1, level_end - level_start - 1);
size_t msg_start = line.find(' ', level_end);
std::string_view msg = (msg_start == std::string_view::npos)
? std::string_view{}
: trim(line.substr(msg_start + 1));
return LogEntry{ts, level, msg};
}
int main() {
const char* lines[] = {
"2024-01-15 10:30:00 [INFO] User login",
"2024-01-15 10:31:00 [ERROR] Connection failed",
"", // 빈 라인 — nullopt
" malformed line ", // 형식 오류 가능
};
for (auto line : lines) {
if (auto entry = parse_log_line(line)) {
std::cout << "TS: " << entry->timestamp
<< " | LV: " << entry->level
<< " | MSG: " << entry->message << "\n";
} else {
std::cout << "(skip or invalid)\n";
}
}
return 0;
}
실행 결과:
TS: 2024-01-15 10:30:00 | LV: INFO | MSG: User login
TS: 2024-01-15 10:31:00 | LV: ERROR | MSG: Connection failed
(skip or invalid)
(skip or invalid)
string_view로 split (복사 없음)
#include <string_view>
#include <vector>
#include <iostream>
std::vector<std::string_view> split(std::string_view s, char delim) {
std::vector<std::string_view> result;
size_t start = 0;
while (start < s.size()) {
size_t pos = s.find(delim, start);
if (pos == std::string_view::npos) {
result.push_back(s.substr(start));
break;
}
result.push_back(s.substr(start, pos - start));
start = pos + 1;
}
return result;
}
int main() {
std::string line = "a,b,c,d";
auto tokens = split(line, ',');
for (auto t : tokens) {
std::cout << "[" << t << "] ";
}
std::cout << "\n";
return 0;
}
주의: tokens에 담긴 string_view는 line을 가리킵니다. line이 파괴되기 전에만 사용해야 합니다.
string_view → string 변환 (필요할 때만)
#include <string>
#include <string_view>
void use_string_view(std::string_view sv) {
// C API나 null 종료가 필요할 때만 변환
std::string s(sv);
some_c_api(s.c_str());
// 또는 직접 (null 종료 보장 없음 주의)
// some_c_api(sv.data()); // sv가 null 종료일 때만!
}
3. std::span 완전 가이드
기본 사용법
std::span<T>는 연속 메모리의 뷰입니다. std::vector, std::array, C 스타일 배열을 복사 없이 하나의 타입으로 받을 수 있습니다. C++20에서 도입되었습니다.
// g++ -std=c++20 -o span_basic span_basic.cpp && ./span_basic
#include <span>
#include <vector>
#include <array>
#include <iostream>
void print(std::span<const int> s) {
for (int x : s) {
std::cout << x << " ";
}
std::cout << "\n";
}
int main() {
std::vector<int> v = {1, 2, 3};
std::array<int, 3> a = {4, 5, 6};
int arr[] = {7, 8, 9};
print(v); // vector
print(a); // array
print(arr); // C 배열
print(std::span(v.data(), 2)); // 일부만
return 0;
}
span의 읽기 전용 vs 수정 가능
| 타입 | 용도 |
|---|---|
std::span<const T> | 읽기 전용 뷰 |
std::span<T> | 수정 가능 뷰 |
#include <span>
#include <vector>
void read_only(std::span<const int> s) {
// s[0] = 1; // 컴파일 에러
int x = s[0]; // OK
}
void modify(std::span<int> s) {
s[0] = 42; // OK
}
int main() {
std::vector<int> v = {1, 2, 3};
read_only(v);
modify(v);
return 0;
}
subspan — 뷰의 일부
subspan(offset, count)로 원본의 일부를 가리키는 새 span을 만듭니다. 복사 없이 뷰만 생성합니다.
#include <span>
#include <vector>
#include <iostream>
void process_header(std::span<const std::byte> header) {
// 헤더 16바이트 처리
}
void process_payload(std::span<const std::byte> payload) {
// 페이로드 처리
}
void handle_packet(std::span<const std::byte> packet) {
if (packet.size() < 16) return;
std::span<const std::byte> header = packet.subspan(0, 16);
std::span<const std::byte> payload = packet.subspan(16);
process_header(header);
process_payload(payload);
}
int main() {
std::vector<std::byte> buf = { /* ... */ };
handle_packet(buf);
return 0;
}
subspan 오버로드
#include <span>
std::span<int> s = /* ... */;
// subspan(offset, count)
auto a = s.subspan(2, 3); // 인덱스 2부터 3개
// subspan(offset) — offset부터 끝까지
auto b = s.subspan(2); // 인덱스 2부터 끝
// dynamic_extent — 크기 지정 없이
auto c = s.subspan<2>(3); // C++20: 인덱스 2부터 3개 (고정 extent)
완전한 span 예제: 버퍼 파서 (in-place)
// g++ -std=c++20 -o span_parse span_parse.cpp && ./span_parse
#include <span>
#include <vector>
#include <cstring>
#include <iostream>
void parse_in_place(std::span<char> buffer, char delim,
void (*on_token)(std::span<const char>)) {
char* start = buffer.data();
char* end = buffer.data() + buffer.size();
char* p = start;
while (p != end) {
if (*p == delim || *p == '\0') {
*p = '\0';
if (p > start) {
on_token({start, static_cast<size_t>(p - start)});
}
start = p + 1;
}
++p;
}
if (p > start) {
on_token({start, static_cast<size_t>(p - start)});
}
}
int main() {
std::vector<char> buf = {'a', ',', 'b', ',', 'c', '\0'};
parse_in_place(buf, ',', {
std::cout << "Token: " << tok.data() << "\n";
});
return 0;
}
완전한 subspan 예제: 네트워크 패킷 버퍼 처리
헤더·페이로드·트레일러를 subspan으로 분리하는 실전 예제:
// g++ -std=c++20 -o buffer_process buffer_process.cpp && ./buffer_process
#include <span>
#include <vector>
#include <cstdint>
#include <cstring>
#include <iostream>
// 패킷 구조: [매직 4B][버전 2B][길이 4B][페이로드 N바이트][체크섬 4B]
constexpr uint32_t MAGIC = 0xDEADBEEF;
constexpr size_t HEADER_SIZE = 10; // 매직 + 버전 + 길이
constexpr size_t TRAILER_SIZE = 4; // 체크섬
struct PacketHeader {
uint32_t magic;
uint16_t version;
uint32_t payload_len;
};
bool process_packet(std::span<const std::byte> buf) {
if (buf.size() < HEADER_SIZE + TRAILER_SIZE) return false;
// subspan으로 각 구간 분리 — 포인터 연산 없음
auto header_span = buf.subspan(0, HEADER_SIZE);
const auto* h = reinterpret_cast<const PacketHeader*>(header_span.data());
if (h->magic != MAGIC) return false;
if (buf.size() < HEADER_SIZE + h->payload_len + TRAILER_SIZE) return false;
auto payload = buf.subspan(HEADER_SIZE, h->payload_len);
auto checksum_span = buf.subspan(HEADER_SIZE + h->payload_len, TRAILER_SIZE);
// 페이로드 처리 (예: 파싱, 검증)
std::cout << "Payload size: " << payload.size() << " bytes\n";
// 체크섬 검증 등...
(void)checksum_span;
return true;
}
int main() {
std::vector<std::byte> packet(64);
auto* h = reinterpret_cast<PacketHeader*>(packet.data());
h->magic = MAGIC;
h->version = 1;
h->payload_len = 32;
process_packet(packet);
return 0;
}
핵심: subspan으로 (ptr, len) 수동 계산 없이 구간을 타입 안전하게 분리합니다.
span과 string_view
| string_view | span | |
|---|---|---|
| 대상 | 문자열 (char) | 임의 연속 메모리 |
| 수정 | 읽기 전용 | span |
| null 종료 | 가정하지 않음 | — |
| 표준 | C++17 | C++20 |
문자열을 수정 가능한 버퍼로 다룰 때는 std::span<char>를 사용합니다.
#include <span>
#include <string_view>
void process_buffer(std::span<char> buf) {
buf[0] = 'X'; // 수정 가능
}
void process_string(std::string_view sv) {
// sv[0] = 'X'; // 컴파일 에러 — 읽기 전용
}
4. 자주 발생하는 에러와 해결법
에러 1: string_view 반환 시 지역 string 댕글링
증상: 크래시, 쓰레기 값, AddressSanitizer 에러
// ❌ 잘못된 사용
std::string_view get_token() {
std::string line = read_line();
return line.substr(0, line.find(',')); // line 파괴 후 댕글링
}
// ✅ 해결: 원본을 인자로 받거나 string 반환
std::string_view get_token(std::string_view line) {
size_t pos = line.find(',');
return (pos == std::string_view::npos) ? line : line.substr(0, pos);
}
에러 2: 임시 string에서 string_view 추출
증상: 댕글링
// ❌ 잘못된 사용
std::string_view sv = std::string("hello"); // 임시 파괴 후 sv 댕글링
std::cout << sv; // UB
// ✅ 해결: 원본을 변수에 보관
std::string s = "hello";
std::string_view sv = s;
std::cout << sv; // OK
에러 3: string_view를 컨테이너에 오래 보관
증상: 원본이 먼저 파괴되면 댕글링
// ❌ 위험: line이 파괴된 뒤 tokens 사용
std::vector<std::string_view> tokens;
{
std::string line = read_line();
tokens = split(line, ',');
}
use(tokens); // line은 이미 파괴됨 — 댕글링
// ✅ 해결: 원본과 같은 수명, 또는 string으로 저장
std::string line = read_line();
auto tokens = split(line, ',');
use(tokens); // line이 유효한 동안
에러 4: span에 nullptr 전달
증상: UB (nullptr 역참조)
// ❌ 잘못된 사용
std::span<int> s(nullptr, 10); // 위험
// ✅ 해결: 크기 0이면 data()가 nullptr일 수 있음 — 사용 전 검사
void process(std::span<const int> s) {
if (s.empty()) return;
// s.data() 사용
}
에러 5: subspan 범위 초과
증상: UB (범위 밖 접근)
// ❌ 잘못된 사용
std::span<int> s = /* size 10 */;
auto sub = s.subspan(5, 10); // 5+10=15 > 10 — UB
// ✅ 해결: 범위 검사
if (s.size() >= 15) {
auto sub = s.subspan(5, 10);
}
에러 6: string_view를 C API에 직접 전달
증상: string_view는 null 종료를 보장하지 않습니다. data()를 C API에 넘기면 버퍼 오버런 위험이 있습니다.
// ❌ 잘못된 사용
void c_api(const char* str);
std::string_view sv = "hello";
c_api(sv.data()); // sv가 "hello\0" 리터럴에서 온 게 아니면 위험
// ✅ 해결: null 종료가 필요하면 string으로 변환
std::string s(sv);
c_api(s.c_str());
에러 7: span으로 원본 수정 시 뷰 무효화
증상: vector::push_back 등으로 재할당이 일어나면, 기존 span이 가리키던 메모리가 무효화됩니다.
// ❌ 위험
std::vector<int> v = {1, 2, 3};
std::span<int> s = v;
v.push_back(4); // 재할당 가능 — s 무효화
s[0] = 0; // UB
// ✅ 해결: span 사용 중에는 원본 수정 금지, 또는 span을 재생성
std::span<int> s2 = v; // 재생성 후 사용
에러 8: string_view substr 범위 초과
증상: substr(pos, count)에서 pos > size()이면 예외 또는 UB. C++20 이전에는 pos > size() 시 std::out_of_range 예외.
// ❌ 잘못된 사용
std::string_view sv = "hi";
auto sub = sv.substr(10, 5); // pos=10 > size()=2 — 예외 또는 UB
// ✅ 해결: 범위 검사
std::string_view safe_substr(std::string_view sv, size_t pos, size_t n) {
if (pos >= sv.size()) return std::string_view{};
n = std::min(n, sv.size() - pos);
return sv.substr(pos, n);
}
에러 9: string_view와 string 혼용 시 수명 오해
증상: string을 반환하는 함수 결과에 string_view를 바인딩하면, 임시 string이 곧 파괴되어 댕글링.
// ❌ 잘못된 사용
std::string get_line();
std::string_view sv = get_line(); // 임시 string 파괴 → sv 댕글링
process(sv); // UB
// ✅ 해결: string을 변수에 보관
std::string line = get_line();
std::string_view sv = line;
process(sv); // OK
5. 베스트 프랙티스
string_view 사용 시
- 함수 인자: 읽기 전용 문자열 인자는
std::string_view로 받기 - 반환: 원본이 호출자보다 오래 유지될 때만
string_view반환 - 저장:
string_view를 멤버·컨테이너에 저장할 때 원본 수명을 반드시 확인 - C API: null 종료가 필요하면
std::string(sv)로 변환 후c_str()사용
// ✅ 함수 인자
void process(std::string_view input);
// ✅ 원본이 인자로 전달됨 — 안전한 반환
std::string_view get_prefix(std::string_view s) {
return s.substr(0, 5);
}
span 사용 시
- 함수 인자: 연속 메모리를 받을 때
(ptr, size)대신std::span사용 - 읽기/쓰기 구분:
span<const T>vsspan<T> - subspan:
subspan으로 일부만 전달할 때 범위 검사 - 원본 수정: span 사용 중에는
vector::push_back등 재할당 유발 연산 금지 - empty 검사:
s.empty()확인 후s.data()사용 — 크기 0일 때data()가 nullptr일 수 있음
// ✅ span 인자
void parse(std::span<const std::byte> data);
// ✅ 수정 가능 버퍼
void fill(std::span<char> buf);
string_view vs span 선택
| 상황 | 권장 |
|---|---|
| 문자열 읽기 | string_view |
| char 버퍼 수정 | span |
| 바이트 배열 | span |
| 일반 배열 뷰 | span |
6. 성능 비교
string_view vs const string&
| 연산 | const string& | string_view |
|---|---|---|
| 리터럴 전달 | 임시 string 생성 | 뷰만 생성 |
| substr | 새 string 할당 | 뷰만 생성 |
| find | O(n) | O(n) |
| 메모리 | — | 포인터 + size (16바이트) |
// ❌ const string& — 리터럴마다 임시 생성
void process(const std::string& s);
process("hello"); // 임시 std::string 생성
// ✅ string_view — 할당 없음
void process(std::string_view sv);
process("hello"); // 뷰만 생성
span vs (ptr, size)
| (ptr, size) | span | |
|---|---|---|
| 타입 안전 | 순서 바꿀 위험 | 컴파일 타임 검사 |
| 인터페이스 | 두 인자 | 하나 |
| subspan | 수동 계산 | 내장 API |
벤치마크 요약 (참고용)
| 시나리오 | string | string_view |
|---|---|---|
| 100만 줄 split | 100만+ 할당 | 0 할당 |
| 함수 인자 (리터럴) | 임시 생성 | 뷰만 |
| substr 파싱 | 매번 복사 | 뷰만 |
7. 프로덕션 패턴
패턴 1: 파싱 파이프라인 (string_view)
#include <string_view>
#include <optional>
#include <vector>
std::optional<std::string_view> extract_key(std::string_view json, std::string_view key) {
std::string pattern = "\"" + std::string(key) + "\":\"";
size_t pos = json.find(pattern);
if (pos == std::string_view::npos) return std::nullopt;
pos += pattern.size();
size_t end = json.find('"', pos);
if (end == std::string_view::npos) return std::nullopt;
return json.substr(pos, end - pos);
}
std::vector<std::string_view> split_lines(std::string_view text) {
std::vector<std::string_view> lines;
size_t start = 0;
while (start < text.size()) {
size_t end = text.find('\n', start);
if (end == std::string_view::npos) {
lines.push_back(text.substr(start));
break;
}
lines.push_back(text.substr(start, end - start));
start = end + 1;
}
return lines;
}
패턴 2: 버퍼 풀 + span
#include <span>
#include <vector>
class BufferPool {
std::vector<char> storage_;
public:
std::span<char> acquire(size_t size) {
storage_.resize(size);
return std::span(storage_);
}
void release(std::span<char> s) {
// storage_와 s가 같은 메모리인지 확인 후 처리
}
};
패턴 3: 프로토콜 파서 (span + subspan)
#include <span>
#include <cstdint>
struct Header {
uint32_t magic;
uint32_t length;
};
bool parse_packet(std::span<const std::byte> packet) {
if (packet.size() < sizeof(Header)) return false;
auto header_span = packet.subspan(0, sizeof(Header));
const Header* h = reinterpret_cast<const Header*>(header_span.data());
if (packet.size() < sizeof(Header) + h->length) return false;
auto payload = packet.subspan(sizeof(Header), h->length);
process_payload(payload);
return true;
}
패턴 4: [[nodiscard]]와 함께
[[nodiscard]] std::optional<std::string_view> get_config(std::string_view key);
[[nodiscard]] std::span<const std::byte> get_section(std::span<const std::byte> data, size_t offset);
패턴 5: string_view + span 조합 (문자열 버퍼 수정)
문자열 버퍼를 읽고 수정할 때 span<char>로 받아 파싱 후 string_view로 전달:
#include <span>
#include <string_view>
std::string_view parse_first_line(std::span<char> buf) {
for (size_t i = 0; i < buf.size(); ++i) {
if (buf[i] == '\n') {
buf[i] = '\0';
return std::string_view(buf.data(), i);
}
}
return std::string_view(buf.data(), buf.size());
}
프로덕션 체크리스트
- string_view: 반환·저장 시 원본 수명 확인
- span: subspan 범위 검사
- C API: string_view → string 변환 후 c_str()
- 원본 수정: span/string_view 사용 중 재할당·수정 금지
- [[nodiscard]]: 반환값 무시 방지
- string_view substr: pos, count 범위 검사
- span empty: data() 사용 전 empty() 확인
8. 정리
| 타입 | 용도 | 핵심 API | 장점 |
|---|---|---|---|
| string_view | 문자열 뷰 | substr, find, remove_prefix, remove_suffix | 제로카피, const string& 대체 |
| span | 연속 메모리 뷰 | subspan, data, size | (ptr, size) 대체, 타입 안전 |
핵심 원칙:
- 읽기 전용 문자열 →
string_view - 연속 메모리 뷰 →
span - 원본 수명이 뷰보다 길어야 함
- null 종료 필요 시
string으로 변환
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 문자열 기초 완벽 가이드 | std::string·C 문자열·string_view와 실전 패턴
- C++ 문자열 파싱 완벽 가이드 | stringstream·getline·제로카피·성능 벤치마크
- C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]
이 글에서 다루는 키워드 (관련 검색어)
C++ string_view span, std::string_view, std::span, 제로카피, 댕글링 방지, subspan, string_view 수명 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. string_view를 반환해도 되나요?
A. 원본이 호출자에서 더 오래 유지되는 경우에만 반환합니다. 함수 내 지역 string을 가리키는 string_view를 반환하면 댕글링입니다.
Q. span과 vector의 차이는?
A. vector는 메모리를 소유하고, span은 뷰만 제공합니다. span은 복사·할당 없이 기존 버퍼를 가리킵니다.
Q. C++17에서 span을 쓰려면?
A. C++20 std::span이 없으면, gsl::span(Guidelines Support Library) 또는 (ptr, size) 쌍을 사용합니다.
Q. string_view와 span을 같이 쓸 수 있나요?
A. 네. 문자열 버퍼를 span<char>로 수정하고, 파싱 결과를 string_view로 전달하는 식으로 조합할 수 있습니다.
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
한 줄 요약: string_view로 문자열, span으로 배열을 복사 없이 “보기만” 할 수 있습니다. 원본 수명을 반드시 확인하세요.
이전 글: C++ 실전 가이드 #38-1: optional·variant
다음 글: C++ 실전 가이드 #38-3: 인터페이스 설계와 PIMPL·ABI
관련 글
- C++ std::optional·std::variant 완벽 가이드 | nullptr 대신 타입 안전하게
- [C++ 클린 코드 기초: const, noexcept, ]로 인터페이스 의도 명확히 하기](/blog/cpp-series-38-1-clean-code-const-noexcept-nodiscard/)
- C++ 현대적 다형성 설계: 상속 대신 합성·variant
- C++ 인터페이스 설계와 PIMPL: 컴파일 의존성을 끊고 바이너리 호환성(ABI) 유지하기 [#38-3]
- C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리