[C++23 새기능 소개] std::spanstream

C++23에서는 표준 라이브러리에 std::spanstream 계열의 클래스 템플릿이 추가되어, 메모리 상의 연속 구역(spans)을 스트림처럼 다룰 수 있는 편리한 방법을 제공하게 되었습니다. 기존에 std::stringstream를 통해 문자열 기반 버퍼를 다루었다면, 이제는 std::span을 이용하여 메모리 버퍼를 입출력 스트림으로 다룰 수 있으며, 이를 통해 메모리에 이미 존재하는 데이터에 대해 스트림 연산을 간편히 적용할 수 있습니다.

 

이번 글에서는 std::spanstream, std::ispanstream, std::ospanstream의 개념과 사용법, 그리고 이전 방식과 비교하여 어떤 점이 개선되었는지 알아보겠습니다.

std::spanstream란 무엇인가요?

C++23에서 추가된 <spanstream> 헤더에는 다음과 같은 클래스 템플릿이 정의됩니다:

  • std::basic_spanstream<CharT, Traits>: 메모리 상의 연속 구역(span)을 기반으로 입출력 가능한 스트림 클래스로, CharT 타입의 문자 시퀀스를 내부 버퍼로 사용합니다.
  • std::basic_ispanstream<CharT, Traits>: 입력 전용(span 기반의) 스트림
  • std::basic_ospanstream<CharT, Traits>: 출력 전용(span 기반의) 스트림

이를 통해 기존의 std::stringstream가 문자열을 기반 버퍼로 사용했던 것처럼, std::spanstream는 std::span을 기반으로 하여 이미 존재하는 메모리 버퍼에 대해 스트림 입출력을 수행할 수 있습니다. 이는 예를 들어 이미 할당된 메모리 상에서 형식화된 입출력, 파싱 등을 수행하거나, 특정 구역에 대한 출력 제어에 유용합니다.

이전 버전에서는 어떻게 했나요?

C++20까지는 std::string 또는 std::string_view를 이용해 문자열 기반 스트림(std::stringstream)을 사용할 수 있었지만, 이미 존재하는 메모리 버퍼(예: raw 배열, 외부 라이브러리에서 제공하는 버퍼)에 대해 직접 스트림 연산을 수행하기 위해서는 다음과 같은 우회 방법을 사용해야 했습니다.

예제: 기존 방식(C++20까지)

#include <sstream>
#include <iostream>
#include <vector>

// 이미 존재하는 메모리 버퍼를 스트림으로 다루고 싶다면?
// 보통은 이 버퍼를 std::string으로 복사한 뒤 std::stringstream에 연결하는 식으로 처리.

int main() {
    std::vector<char> buffer = {'4', '2'};
    // buffer 내용을 파싱하기 위해 std::string으로 복사
    std::string s(buffer.begin(), buffer.end());

    std::istringstream iss(s);
    int value;
    iss >> value; // 42 파싱 가능
    std::cout << value << '\n';

    return 0;
}
  • 문제점: 이미 존재하는 버퍼를 문자열로 복사하는 추가 과정 필요, 메모리 오버헤드, 성능 저하 가능.

C++23의 std::spanstream 사용 예제

#include <spanstream>
#include <iostream>
#include <vector>

int main() {
    // 이미 존재하는 메모리 버퍼 (문자 배열)
    std::vector<char> buffer = {'4','2',' ','9','9'};

    // buffer를 span으로 감싸서 ispanstream에 연결
    std::ispanstream iss(std::span(buffer)); 
    int value1, value2;
    iss >> value1 >> value2;

    std::cout << value1 << ", " << value2 << '\n'; // 출력: 42, 99
    return 0;
}
  • std::ispanstream를 이용해 메모리 버퍼를 직접 스트림 소스로 사용.
  • 별도의 문자열 복사 없이 std::span을 통해 바로 파싱 가능.

출력 예제

#include <spanstream>
#include <iostream>

int main() {
    char buf[50];
    std::ospanstream oss(std::span(buf, 50));

    oss << 123 << " Hello" << ' ' << 4.56;
    // buf 안에 "123 Hello 4.56" 형식으로 데이터 기록

    std::cout << buf << '\n'; // 출력: 123 Hello 4.56

    return 0;
}
  • std::ospanstream를 통해 buf에 직접 출력 내용 기록.
  • std::ostream과 동일한 방식으로 << 연산자 사용 가능.

어떻게 좋아졌나요?

  • 메모리 복사 감소: 기존에는 문자열 기반 스트림을 위해 추가 복사 필요했지만, 이제는 이미 존재하는 버퍼 위에서 직접 스트림 연산 수행 가능.
  • 유연성 증가: std::span을 통해 다양한 메모리 리소스(정적 배열, 동적 할당 버퍼, 외부 라이브러리 버퍼)를 쉽게 스트림으로 사용.
  • 가독성 및 유지보수성 향상: 별도의 변환 과정 없이 코드에서 바로 I/O 스트림 문법 사용 가능.

주의 사항

  • 버퍼 관리 주의: std::spanstream는 전달받은 span 범위 내에서만 쓰기/읽기 가능. 범위 초과 접근 시 안전성 문제 발생 가능.
  • C++23 지원: 이 기능은 C++23이 필요하며, 컴파일러와 표준 라이브러리 지원 여부 확인 필요.

요약

C++23의 std::spanstream 계열 클래스(std::basic_spanstream, std::basic_ispanstream, std::basic_ospanstream)는 기존 std::stringstream와 유사한 스트림 기능을 메모리 기반 std::span에 적용할 수 있게 합니다. 이를 통해 불필요한 문자열 복사 없이 기존 버퍼 상에서 형식화된 입출력, 파싱을 수행할 수 있어 성능과 가독성 모두 개선됩니다.

 

참고 자료:

반응형