std::string_view 란
std::string_view
는 C++17부터 추가된, 문자열을 다루는데 아주 강력하고 효과적인 컨테이너입니다. 내부 구현은 다르겠지만, std::string_view
의 동작과 기능은 std::span
과 거의 비슷합니다. std::span
이 std::vector
, C-style array, std::array
등의 다양한 형태의 배열을 효율적으로 다루기 위해서 존재했다면, std::string_view
는 std::string
, const char *
, char []
와 같은 다양한 형태의 문자열을 효율적으로 다루기 위해서 존재합니다.
한편 std::span
에 대해서 궁금하시다면, [모던 C++]std::span, 왜 쓰는 걸까?라는 글을 보고 오시면 도움이 될 것 같습니다.
기본적으로 std::string_view
는 원본 문자열을 복사 생성 없이 그대로 참조하겠다는 성격이 강합니다. 그런데 이미 우리는 참조형이 있는데 왜 std::string_view
가 필요할까요? 그 이유는 C++에는 std::string
외에 다양한 문자열 타입들이 있기 때문입니다.
std::string, const char *, char [] 의 차이점
일단 C++에는 앞서 말한 다양한 문자열의 형태가 존재하고, 모두가 그냥 std::string
을 사용하면 좋겠지만, 성능, 사용성, 그리고 작업 환경 등에 따라서 std::string
외에 다른 문자열 처리를 해야할 때가 있습니다(std::string
은 추상화된 클래스이기 떄문에, 메모리 관리나 멤버 메소드 제공 등의 측면에서 편리하지만, 반대로 말하면 일반 const char *
등을 사용할 때보다 약간의 성능이나 메모리 사용 측면에서 단점이 발생합니다.) 또한 불행하게도 std::string
, const char *
, char []
모두 내부 구현 동작 방식이 조금씩 다릅니다.
예를 들어서 std::string str1 = "script by";
라고 선언하면, std::string
형의 str1
은 script by
라는 문자열을 가리키게 됩니다. 이때 script by
라는 문자열은 그 크기에 따라서 힙 메모리 영역에 올라가냐 혹은 그냥 스택 메모리 영역에 올라가냐가 달라지는데, 문자열의 길이가 작을 경우 그냥 컴파일러가 알아서 스택 위에 올리고, 문자열의 길이가 크다면 컴파일러가 힙 메모리 영역 위에 올립니다.
한편 const char * str2 = "nx006";
라고 선언하면, nx006
라는 문자열은 Read only 메모리 영역(읽기 전용 메모리 영역)에 올라가게 됩니다. 그리고 str2
는 Read only 메모리 영역에 있는 문자열을 가리키게 됩니다.
마지막으로 char[] str3 = "CODE EDGE;"
라고 선언하면, CODE EDGE
라는 문자열은 stack 메모리 영역 위에 올라가게 됩니다. 그리고 str3
가 이 문자열을 가리키게 됩니다.
기존 문자열 처리의 문제점
다음 코드를 볼게요. 이 코드는 아나그램에 대한 해시값을 생성하는 코드입니다.
// anagram hash generator
std::string anagram_hash(const std::string& str)
{
std::map<char, int> alphabet_tables;
std::ranges::for_each(str, [&alphabet_tables](const auto& c) {alphabet_tables[c]++; });
std::string hash = "";
for (const auto& [alphabet, number] : alphabet_tables) {
hash += alphabet;
hash += std::to_string(number);
}
return std::move(hash);
}
위 코드에서 인자값의 타입으로 const std::string& str
을 받고 있습니다. 그리고 만약 main 함수에서 다음과 같이 문자열들을 넘겨준다고 가정해봅시다.
int main ()
{
std::string str1 = "AABBC";
const char * str2 = "AABBC";
char[] str3 = "AABBC";
const auto hash1 = anagram_hash(str1);
const auto hash2 = anagram_hash(str2);
const auto hash3 = anagram_hash(str3);
std::cout << hash1 << '\n'
<< hash2 << '\n'
<< hash3 << '\n';
return 0;
}
결과:
A2B2C1
A2B2C1
A2B2C1
위 코드는 세 가지 입력값 str1
, str2
, str3
에 대해서 동작을 하긴 합니다. 그러나 자세한 동작 과정을 살펴보시면, 의도하지 않은 동작이 이루어지고 있다는 것을 알 수 있습니다.
std::string str1
에 대해선 문제 없이 작동합니다. 파라미터 타입이 참조형이기에 copy constructor 없이 인수를 함수에 넘겨줄 수 있습니다.
그런데 const char * str2
와 char[] str3
에서 문제가 생깁니다. 이를 넘겨줄 경우 내부적으로는 std::string
형의 임시 객체가 만들어진 후 여기에 str2
와 str3
를 복사하는 Copy construction가 일어나게 됩니다. 그리고 복사된 메모리를 str
이 가리키게 됩니다. 의도치 않은 복사 생성자가 호출되었고, 이로 인해 성능 저하가 발생하고 있습니다.
std::string_view의 사용
이를 해결하기 위해서 우리는 std::string_view
를 통해 파라미터 타입을 조정할 수 있습니다.
std::string anagram_hash(const std::string_view str)
{
std::map<char, int> alphabet_tables;
std::ranges::for_each(str, [&alphabet_tables](const auto& c) {alphabet_tables[c]++; });
std::string hash = "";
for (const auto& [alphabet, number] : alphabet_tables) {
hash += alphabet;
hash += std::to_string(number);
}
return std::move(hash);
}
파라미터 타입을 std::string&
에서 std::string_view
로 바꾸어준다면, str2
, str3
를 사용하는 상황에서도 의도치 않은 임시 객체의 복사 생성 없이 우리는 원본 문자열을 바로 함수 내부에서 참조할 수 있습니다. std::string_view
는 문법 이식이 간단하고 매우 강력한 기능이여서 적극적으로 활용할 수 있습니다.
std::string_view 사용 시 주의점
std::string_view
는 참조형입니다. 즉 다시 말해서 원본이 변경되면, std::string_view
도 변경됩니다. 만약 원본 메모리가 라이프 사이클이 끝나서 파괴된다면, std::string_view
역시 가비지 값을 가리키게 됩니다. 따라서 std::string_view
역시 다른 참조형과 마찬가지로 원본과 라이프 사이클이 어긋나지 않게 주의해야 합니다.