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에 적용할 수 있게 합니다. 이를 통해 불필요한 문자열 복사 없이 기존 버퍼 상에서 형식화된 입출력, 파싱을 수행할 수 있어 성능과 가독성 모두 개선됩니다.
참고 자료:
'개발 이야기 > C++' 카테고리의 다른 글
C++20과 C++23을 활용한 “파이썬스러운” API 구현 #1: range와 enumerate (1) | 2024.12.10 |
---|---|
[C++23 새기능 소개] std::views::stride (0) | 2024.12.09 |
[C++23 새기능 소개] std::views::split_when (0) | 2024.12.09 |
[C++23 새기능 소개] std::ranges::starts_with, std::ranges::ends_with, std::ranges::contains (0) | 2024.12.09 |
[C++23 새기능 소개] std::to_underlying (0) | 2024.12.09 |