C++ std::string_view·std::span 완벽 가이드 | 제로카피 뷰·댕글링 방지

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. 실무에서 겪는 문제 시나리오
  2. std::string_view 완전 가이드
  3. std::span 완전 가이드
  4. 자주 발생하는 에러와 해결법
  5. 베스트 프랙티스
  6. 성능 비교
  7. 프로덕션 패턴
  8. 정리

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) 순서로 잘못 넘기거나, vectorarray를 동일하게 처리하기 어렵습니다.

// ❌ 순서 바꾸면 버그
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_viewstd::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_viewline을 가리킵니다. 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_viewspan
대상문자열 (char)임의 연속 메모리
수정읽기 전용span는 수정 가능
null 종료가정하지 않음
표준C++17C++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_viewnull 종료를 보장하지 않습니다. 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 사용 시

  1. 함수 인자: 읽기 전용 문자열 인자는 std::string_view로 받기
  2. 반환: 원본이 호출자보다 오래 유지될 때만 string_view 반환
  3. 저장: string_view를 멤버·컨테이너에 저장할 때 원본 수명을 반드시 확인
  4. 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 사용 시

  1. 함수 인자: 연속 메모리를 받을 때 (ptr, size) 대신 std::span 사용
  2. 읽기/쓰기 구분: span<const T> vs span<T>
  3. subspan: subspan으로 일부만 전달할 때 범위 검사
  4. 원본 수정: span 사용 중에는 vector::push_back 등 재할당 유발 연산 금지
  5. 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 할당뷰만 생성
findO(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

벤치마크 요약 (참고용)

시나리오stringstring_view
100만 줄 split100만+ 할당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) 대체, 타입 안전

핵심 원칙:

  1. 읽기 전용 문자열 → string_view
  2. 연속 메모리 뷰 → span
  3. 원본 수명이 뷰보다 길어야 함
  4. 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 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리