string_view와 span: 복사 없이 넘기기, SSO와의 관계, mdspan, 댕글링 방지

들어가며: 문자열·배열을 넘길 때마다 복사가 부담된다

로그 파싱, 설정 읽기, 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 배열 모두 호환

substr 복사 폭발, (ptr, size) 실수, 댕글링: 실무에서 겪는 문제

시나리오 1: 로그 파싱 시 substr 복사 폭발

로그 한 줄이 "2024-01-15 10:30:00 [INFO] User login" 형태일 때 substr로 잘라내면 매번 새 std::string이 만들어집니다. 타임스탬프처럼 SSO 한도(15자)를 넘는 부분 문자열은 줄마다 힙 할당을 일으킵니다.

// ❌ 문제: 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로 구간만 가리키면 할당이 없습니다. 다만 반환된 뷰는 line 원본이 살아 있는 동안만 유효합니다.

// ✅ 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(std::span<const std::byte>(buf, len));  // 포인터+길이도 span으로 묶어서 전달

시나리오 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으로 뷰의 일부를 만들면 길이 계산을 span이 대신합니다.

// ✅ 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 하나로 모든 문자열 타입을 받습니다(C++20부터는 sv.starts_with() 멤버 함수도 있습니다).

// ✅ 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["span (const T)"]
    F -->|수정| H["span (T)"]

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을 수정하면 무효
임시 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) {
    // isspace에 음수 char를 넘기면 UB이므로 unsigned char로 변환
    while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.front()))) sv.remove_prefix(1);
    while (!sv.empty() && std::isspace(static_cast<unsigned char>(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 some_c_api(const char* s);
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 종료일 때만!
}

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;
}

C++17 이하 프로젝트에서는 Guidelines Support Library의 gsl::span이 거의 같은 인터페이스를 제공합니다.

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부터 끝
// 템플릿 인자 버전: 결과가 정적 extent span<int, 3>
auto c = s.subspan<2, 3>();  // 인덱스 2부터 3개

완전한 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::span<const char> tok) {
        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]
// 와이어 포맷은 10바이트 헤더지만, 같은 필드의 C++ 구조체는 패딩 때문에 12바이트가 됨.
// 그래서 구조체로 reinterpret_cast하지 않고 오프셋별로 memcpy해서 읽음
// (reinterpret_cast는 정렬·엄격 별칭 규칙 위반으로 UB가 될 수 있음).
constexpr uint32_t MAGIC = 0xDEADBEEF;
constexpr size_t HEADER_SIZE = 10;   // 매직 + 버전 + 길이
constexpr size_t TRAILER_SIZE = 4;   // 체크섬
template <typename T>
T read_at(std::span<const std::byte> s, size_t offset) {
    T v;
    std::memcpy(&v, s.data() + offset, sizeof(T));
    return v;  // 호스트 바이트 순서 가정: 네트워크 순서라면 ntohl 등으로 변환
}
bool process_packet(std::span<const std::byte> buf) {
    if (buf.size() < HEADER_SIZE + TRAILER_SIZE) return false;
    auto header = buf.subspan(0, HEADER_SIZE);
    if (read_at<uint32_t>(header, 0) != MAGIC) return false;
    uint32_t payload_len = read_at<uint32_t>(header, 6);
    if (buf.size() - HEADER_SIZE - TRAILER_SIZE < payload_len) return false;  // 덧셈 오버플로 없이 비교
    auto payload = buf.subspan(HEADER_SIZE, payload_len);
    auto checksum_span = buf.subspan(HEADER_SIZE + payload_len, TRAILER_SIZE);
    std::cout << "Payload size: " << payload.size() << " bytes\n";
    (void)checksum_span;  // 체크섬 검증 등
    return true;
}
int main() {
    std::vector<std::byte> packet(HEADER_SIZE + 32 + TRAILER_SIZE);
    uint32_t magic = MAGIC, len = 32;
    uint16_t version = 1;
    std::memcpy(packet.data(), &magic, 4);
    std::memcpy(packet.data() + 4, &version, 2);
    std::memcpy(packet.data() + 6, &len, 4);
    process_packet(packet);  // Payload size: 32 bytes
    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';  // 컴파일 에러 — 읽기 전용
}

지역 string 댕글링, 임시 객체 뷰, subspan 범위 초과 같은 에러

에러 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 전달

// ❌ 잘못된 사용
std::span<int> s(nullptr, 10);  // 위험
// ✅ 해결: 크기 0이면 data()가 nullptr일 수 있음 — 사용 전 검사
void process(std::span<const int> s) {
    if (s.empty()) return;
    // s.data() 사용
}

에러 5: subspan 범위 초과

subspan(offset, count)은 offset + count가 size() 이하라는 전제 조건이 있고, 어기면 미정의 동작입니다(검사하지 않음).

// ❌ 잘못된 사용
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 범위 초과

string_view::substr(pos, count)은 pos > size()이면 std::out_of_range 예외를 던집니다(count가 남은 길이보다 크면 남은 만큼만 잘라 줌). 예외를 피하고 싶다면 미리 범위를 검사합니다.

// ❌ 잘못된 사용
std::string_view sv = "hi";
auto sub = sv.substr(10, 5);  // pos=10 > size()=2 — std::out_of_range
// ✅ 해결: 범위 검사
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

string_view와 span을 언제, 어떻게 쓰나

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

string_view vs const string&, span vs (ptr, size)

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

파싱 파이프라인, 버퍼 풀 + span, 프로토콜 파서

패턴 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 + subspan)

#include <span>
#include <cstdint>
#include <cstring>
struct Header {
    uint32_t magic;
    uint32_t length;
};
bool parse_packet(std::span<const std::byte> packet) {
    if (packet.size() < sizeof(Header)) return false;
    Header h;
    std::memcpy(&h, packet.data(), sizeof(Header));  // reinterpret_cast 대신 복사 (정렬·별칭 안전)
    if (packet.size() - sizeof(Header) < h.length) return false;
    auto payload = packet.subspan(sizeof(Header), h.length);
    process_payload(payload);
    return true;
}

패턴 3: [[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);

패턴 4: 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());
}

패턴 5: 다차원 데이터는 mdspan (C++23)

이미지나 행렬처럼 2차원 인덱스가 필요하면 1차원 버퍼 위에 std::mdspan을 씌웁니다. 소유하지 않는 뷰라는 점과 수명 규칙은 span과 같습니다. 표준 라이브러리 지원은 비교적 최근에 들어왔으므로 사용하는 컴파일러 버전에서 <mdspan>이 있는지 먼저 확인합니다.

#include <mdspan>
#include <vector>
std::vector<float> pixels(480 * 640);
std::mdspan img(pixels.data(), 480, 640);  // 행 480, 열 640, 기본은 행 우선(layout_right)
img[10, 20] = 1.0f;                         // C++23 다차원 첨자 연산자
for (std::size_t y = 0; y < img.extent(0); ++y)
    for (std::size_t x = 0; x < img.extent(1); ++x)
        img[y, x] *= 0.5f;

같이 보면 좋은 글


이전 글: C++ 실전 가이드 #38-1: const·noexcept·[[nodiscard]]로 인터페이스 의도 명확히 하기 다음 글: C++ 실전 가이드 #38-3: 인터페이스 설계와 PIMPL·ABI