C++ string vs string_view | 복사 없는 문자열 처리 완벽 비교

C++ string vs string_view | 복사 없는 문자열 처리 완벽 비교

이 글의 핵심

C++ string vs string_view 차이점. string_view는 복사 없이 문자열을 참조하는 경량 타입으로, 함수 매개변수에 유리합니다. 저장은 string, 읽기는 string_view를 사용하세요.

들어가며

C++17의 string_view문자열을 복사하지 않고 참조만 하는 경량 타입입니다. 함수 매개변수로 사용하면 복사 비용을 제거할 수 있습니다.

비유로 말씀드리면, string책을 사서 책장에 꽂는 것(소유), string_view책 제목만 적힌 메모를 들고 원본 책을 가리키는 것(비소유)에 가깝습니다. 메모만 남기고 원본을 반납하면 내용을 읽을 수 없습니다(댕글링).

이 글을 읽으면

  • string과 string_view의 차이를 이해합니다
  • 성능 비교와 복사 비용을 파악합니다
  • 함수 매개변수 선택 기준을 익힙니다
  • 댕글링 포인터 주의사항을 확인합니다

목차

  1. string vs string_view 비교
  2. 실전 구현
  3. 고급 활용
  4. 성능 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

string vs string_view 비교

비교표

항목std::stringstd::string_view
소유권소유 (힙 할당)비소유 (참조만)
복사 비용높음 (문자 복사)낮음 (포인터 복사)
수정가능불가능 (읽기 전용)
크기32바이트 (구현 의존)16바이트 (포인터 + 길이)
null 종료보장보장 안 됨
수명자동 관리수동 관리 (주의)

내부 구조

// std::string (간략화)
class string {
    char* data_;     // 힙 메모리 포인터
    size_t size_;    // 길이
    size_t capacity_; // 용량
};

// std::string_view
class string_view {
    const char* data_;  // 원본 포인터 (비소유)
    size_t size_;       // 길이
};

실전 구현

1) 함수 매개변수

#include <iostream>
#include <string>
#include <string_view>

// ❌ 복사 발생
void printString(std::string s) {  // 값 전달 → 복사
    std::cout << s << std::endl;
}

// ✅ 복사 없음
void printStringView(std::string_view s) {  // 뷰 → 복사 없음
    std::cout << s << std::endl;
}

int main() {
    std::string str = "Hello, World!";
    
    printString(str);      // 복사 발생
    printStringView(str);  // 복사 없음
    
    return 0;
}

2) 부분 문자열

#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string str = "Hello, World!";
    
    // string: 복사 발생
    std::string sub1 = str.substr(0, 5);  // "Hello" 복사
    std::cout << sub1 << std::endl;
    
    // string_view: 복사 없음
    std::string_view sv = str;
    std::string_view sub2 = sv.substr(0, 5);  // 포인터 + 길이만
    std::cout << sub2 << std::endl;
    
    return 0;
}

3) 문자열 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <vector>

std::vector<std::string_view> split(std::string_view s, char delim) {
    std::vector<std::string_view> tokens;
    size_t start = 0;
    
    while (start < s.size()) {
        size_t end = s.find(delim, start);
        if (end == std::string_view::npos) {
            tokens.push_back(s.substr(start));
            break;
        }
        
        tokens.push_back(s.substr(start, end - start));
        start = end + 1;
    }
    
    return tokens;
}

int main() {
    std::string data = "apple,banana,cherry";
    auto tokens = split(data, ',');  // 복사 없음
    
    for (auto token : tokens) {
        std::cout << token << std::endl;
    }
    
    return 0;
}

출력:

apple
banana
cherry

4) 로깅

#include <iostream>
#include <string>
#include <string_view>
#include <vector>

class Logger {
private:
    std::vector<std::string> logs_;
    
public:
    void log(std::string_view msg) {  // 매개변수는 string_view
        logs_.emplace_back(msg);  // string으로 변환해 저장
    }
    
    const std::string& getLog(size_t idx) const {
        return logs_[idx];
    }
    
    size_t size() const {
        return logs_.size();
    }
};

int main() {
    Logger logger;
    
    logger.log("시작");
    logger.log("처리 중");
    logger.log("완료");
    
    for (size_t i = 0; i < logger.size(); ++i) {
        std::cout << logger.getLog(i) << std::endl;
    }
    
    return 0;
}

고급 활용

1) 접두사/접미사 체크 (C++20)

#include <iostream>
#include <string_view>

int main() {
    std::string_view sv = "Hello, World!";
    
    // C++20: starts_with/ends_with
    bool b1 = sv.starts_with("Hello");  // true
    bool b2 = sv.ends_with("World!");   // true
    bool b3 = sv.starts_with("Hi");     // false
    
    std::cout << std::boolalpha;
    std::cout << "starts_with(\"Hello\"): " << b1 << std::endl;
    std::cout << "ends_with(\"World!\"): " << b2 << std::endl;
    std::cout << "starts_with(\"Hi\"): " << b3 << std::endl;
    
    return 0;
}

2) 문자열 트림

#include <iostream>
#include <string_view>

std::string_view trim(std::string_view s) {
    size_t start = 0;
    while (start < s.size() && std::isspace(s[start])) {
        ++start;
    }
    
    size_t end = s.size();
    while (end > start && std::isspace(s[end - 1])) {
        --end;
    }
    
    return s.substr(start, end - start);
}

int main() {
    std::string str = "  Hello, World!  ";
    std::string_view trimmed = trim(str);
    
    std::cout << "[" << trimmed << "]" << std::endl;  // [Hello, World!]
    
    return 0;
}

3) 문자열 비교 최적화

#include <iostream>
#include <string>
#include <string_view>

bool isCommand(std::string_view input, std::string_view command) {
    return input == command;
}

int main() {
    std::string input = "quit";
    
    // string_view로 비교 (복사 없음)
    if (isCommand(input, "quit")) {
        std::cout << "종료" << std::endl;
    } else if (isCommand(input, "help")) {
        std::cout << "도움말" << std::endl;
    }
    
    return 0;
}

성능 비교

벤치마크 1: 함수 매개변수 전달

#include <chrono>
#include <iostream>
#include <string>
#include <string_view>

void benchString(std::string s) {
    // 읽기만
}

void benchStringRef(const std::string& s) {
    // 읽기만
}

void benchStringView(std::string_view s) {
    // 읽기만
}

int main() {
    std::string str = "Hello, World! This is a test string.";
    
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchString(str);
    }
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchStringRef(str);
    }
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
    
    auto start3 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        benchStringView(str);
    }
    auto end3 = std::chrono::high_resolution_clock::now();
    auto time3 = std::chrono::duration_cast<std::chrono::milliseconds>(end3 - start3).count();
    
    std::cout << "string (값): " << time1 << "ms" << std::endl;
    std::cout << "const string&: " << time2 << "ms" << std::endl;
    std::cout << "string_view: " << time3 << "ms" << std::endl;
    
    return 0;
}

결과 (GCC 13, -O3):

매개변수 타입시간상대 속도
std::string (값)850ms17x
const std::string&50ms1.0x
std::string_view50ms1.0x

분석: string_view는 const string& 와 동일한 성능


벤치마크 2: 부분 문자열

#include <chrono>
#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string str = "Hello, World!";
    
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        std::string sub = str.substr(0, 5);  // 복사
    }
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000000; ++i) {
        std::string_view sv = str;
        std::string_view sub = sv.substr(0, 5);  // 복사 없음
    }
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
    
    std::cout << "string::substr: " << time1 << "ms" << std::endl;
    std::cout << "string_view::substr: " << time2 << "ms" << std::endl;
    
    return 0;
}

결과 (GCC 13, -O3):

방법시간상대 속도
string::substr120ms24x
string_view::substr5ms1.0x

분석: string_view가 24배 빠름 (복사 없음)


실무 사례

사례 1: HTTP 요청 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>

class HttpRequest {
private:
    std::string method_;
    std::string path_;
    std::unordered_map<std::string, std::string> headers_;
    
public:
    void parse(std::string_view request) {
        size_t methodEnd = request.find(' ');
        method_ = request.substr(0, methodEnd);
        
        size_t pathStart = methodEnd + 1;
        size_t pathEnd = request.find(' ', pathStart);
        path_ = request.substr(pathStart, pathEnd - pathStart);
    }
    
    std::string_view getMethod() const {
        return method_;
    }
    
    std::string_view getPath() const {
        return path_;
    }
};

int main() {
    std::string request = "GET /api/users HTTP/1.1";
    
    HttpRequest req;
    req.parse(request);
    
    std::cout << "Method: " << req.getMethod() << std::endl;
    std::cout << "Path: " << req.getPath() << std::endl;
    
    return 0;
}

사례 2: CSV 파싱

#include <iostream>
#include <string>
#include <string_view>
#include <vector>

std::vector<std::string_view> parseCSVLine(std::string_view line) {
    std::vector<std::string_view> fields;
    size_t start = 0;
    
    while (start < line.size()) {
        size_t end = line.find(',', start);
        if (end == std::string_view::npos) {
            fields.push_back(line.substr(start));
            break;
        }
        
        fields.push_back(line.substr(start, end - start));
        start = end + 1;
    }
    
    return fields;
}

int main() {
    std::string line = "홍길동,30,서울";
    auto fields = parseCSVLine(line);
    
    std::cout << "이름: " << fields[0] << std::endl;
    std::cout << "나이: " << fields[1] << std::endl;
    std::cout << "주소: " << fields[2] << std::endl;
    
    return 0;
}

사례 3: 로그 필터링

#include <iostream>
#include <string>
#include <string_view>
#include <vector>

class LogFilter {
public:
    bool shouldLog(std::string_view level, std::string_view message) {
        if (level == "DEBUG") {
            return false;  // DEBUG 레벨 필터링
        }
        
        if (message.find("password") != std::string_view::npos) {
            return false;  // 민감 정보 필터링
        }
        
        return true;
    }
};

int main() {
    LogFilter filter;
    
    std::string log1 = "DEBUG: 디버그 메시지";
    std::string log2 = "INFO: 사용자 로그인";
    std::string log3 = "ERROR: password 오류";
    
    if (filter.shouldLog("DEBUG", log1)) {
        std::cout << log1 << std::endl;
    }
    
    if (filter.shouldLog("INFO", log2)) {
        std::cout << log2 << std::endl;
    }
    
    if (filter.shouldLog("ERROR", log3)) {
        std::cout << log3 << std::endl;
    }
    
    return 0;
}

사례 4: 명령어 처리

#include <iostream>
#include <string>
#include <string_view>

class CommandHandler {
public:
    void handleCommand(std::string_view cmd) {
        if (cmd == "quit" || cmd == "exit") {
            std::cout << "종료" << std::endl;
        } else if (cmd.starts_with("echo ")) {
            std::string_view msg = cmd.substr(5);
            std::cout << msg << std::endl;
        } else if (cmd == "help") {
            std::cout << "도움말" << std::endl;
        } else {
            std::cout << "알 수 없는 명령어" << std::endl;
        }
    }
};

int main() {
    CommandHandler handler;
    
    handler.handleCommand("echo Hello");
    handler.handleCommand("help");
    handler.handleCommand("quit");
    
    return 0;
}

트러블슈팅

문제 1: 댕글링 포인터

증상: 소멸된 문자열 참조

// ❌ 댕글링
std::string_view getSuffix() {
    std::string str = "Hello, World!";
    return std::string_view(str).substr(7);  // "World!"
}  // str 소멸

int main() {
    auto sv = getSuffix();
    std::cout << sv << std::endl;  // ❌ 소멸된 문자열 참조
    
    return 0;
}

// ✅ string 반환
std::string getSuffix() {
    std::string str = "Hello, World!";
    return str.substr(7);  // string 반환
}

int main() {
    auto s = getSuffix();
    std::cout << s << std::endl;  // ✅ OK
    
    return 0;
}

문제 2: 임시 string에서 string_view

증상: 임시 객체 소멸

// ❌ 임시 객체
std::string_view sv = std::string("Hello");  // 임시 객체
std::cout << sv << std::endl;  // ❌ 임시 객체 이미 소멸

// ✅ string 저장
std::string s = std::string("Hello");
std::string_view sv = s;
std::cout << sv << std::endl;  // ✅ OK

// 또는 string_view 리터럴
using namespace std::string_view_literals;
std::string_view sv2 = "Hello"sv;
std::cout << sv2 << std::endl;  // ✅ OK

문제 3: null 종료 보장 안 됨

증상: C API 연동 오류

#include <cstdio>
#include <string>
#include <string_view>

int main() {
    std::string str = "Hello";
    std::string_view sv = str;
    
    // ❌ null 종료 보장 안 됨
    const char* cstr = sv.data();
    printf("%s\n", cstr);  // 위험 (null 종료 보장 안 됨)
    
    // ✅ string으로 변환
    std::string s(sv);
    const char* cstr2 = s.c_str();  // null 종료 보장
    printf("%s\n", cstr2);
    
    return 0;
}

문제 4: string_view를 멤버 변수로 저장

증상: 댕글링 포인터

// ❌ string_view를 멤버로 저장
class Logger {
private:
    std::vector<std::string_view> logs_;  // ❌ 위험
    
public:
    void log(std::string_view msg) {
        logs_.push_back(msg);  // 원본 수명 주의
    }
};

// ✅ string으로 저장
class Logger {
private:
    std::vector<std::string> logs_;  // ✅ 안전
    
public:
    void log(std::string_view msg) {
        logs_.emplace_back(msg);  // string으로 변환
    }
};

마무리

string_view문자열 복사를 제거해 성능을 높이는 강력한 도구입니다.

핵심 요약

  1. string vs string_view

    • string: 소유, 수정 가능
    • string_view: 참조, 읽기 전용
  2. 선택 기준

    • 함수 매개변수: string_view
    • 저장: string
    • 수정: string&
    • C API: string::c_str()
  3. 성능

    • 매개변수 전달: string_view ≈ const string& >>> string (값)
    • 부분 문자열: string_view >>> string
    • 저장: string (string_view는 위험)
  4. 주의사항

    • 댕글링 포인터 주의
    • null 종료 보장 안 됨
    • 임시 객체 주의
    • 멤버 변수로 저장 금지

선택 가이드

상황권장이유
함수 매개변수 (읽기)string_view복사 없음
멤버 변수 (저장)string소유권
함수 매개변수 (수정)string&수정 가능
C API 연동string::c_str()null 종료
임시 객체 생성string소유권 필요

코드 예제 치트시트

// 함수 매개변수: string_view
void print(std::string_view s) {
    std::cout << s << std::endl;
}

// 저장: string
class Logger {
    std::vector<std::string> logs_;
    
public:
    void log(std::string_view msg) {
        logs_.emplace_back(msg);  // string으로 변환
    }
};

// 부분 문자열: string_view
std::string str = "Hello, World!";
std::string_view sv = str;
std::string_view sub = sv.substr(0, 5);  // 복사 없음

// C API: c_str()
std::string s = "Hello";
printf("%s\n", s.c_str());

다음 단계

  • string 기초: C++ string 기초
  • string_view: C++ string_view
  • 문자열 최적화: C++ 문자열 최적화

참고 자료

한 줄 정리: 함수 매개변수는 string_view로 복사를 제거하고, 저장은 string으로 소유권을 확보하며, 원본 수명을 항상 주의한다.