이 글은 Abseil "Tip of the Week #1:
string_view"를 한글로 옮긴 번역본입니다. 원문 출처는 문서 하단에 있습니다.
string_view란 무엇이고, 왜 알아야 하는가
(상수) 문자열을 인자로 받는 함수를 만들 때, 흔히 세 가지 선택지가 있습니다. 두 가지는 이미 알고 있을 것이고, 나머지 하나는 모를 수도 있습니다.
// C 스타일 관례
void TakesCharStar(const char* s);
// 오래된 표준 C++ 관례
void TakesString(const std::string& s);
// string_view C++ 관례
void TakesStringView(absl::string_view s); // Abseil
void TakesStringView(std::string_view s); // C++17앞의 두 방식은 호출자가 이미 해당 형식으로 문자열을 가지고 있을 때 가장 잘 동작합니다. 하지만 변환이 필요할 때(const char* → std::string, 또는 std::string → const char*)는 어떻게 될까요?
std::string을 const char*로 변환해야 하는 호출자는 (효율적이지만 불편한) c_str() 함수를 써야 합니다.
void AlreadyHasString(const std::string& s) {
TakesCharStar(s.c_str()); // 명시적 변환
}const char*를 std::string으로 변환해야 하는 호출자는 추가로 할 일은 없지만(좋은 소식), (편리하지만 비효율적인) 임시 string이 생성되고 그 내용이 복사됩니다(나쁜 소식).
void AlreadyHasCharStar(const char* s) {
TakesString(s); // 컴파일러가 복사본을 만든다
}그럼 어떻게 해야 할까?
Google이 이런 문자열 매개변수를 받을 때 선호하는 방식은 string_view입니다. 이는 C++17에서 미리 채택한(pre-adopted) 타입으로, 지금은 std::string_view를 쓸 수 있더라도 absl::string_view를 사용하세요.
string_view 클래스의 인스턴스는 기존 문자 버퍼에 대한 “뷰(view)“라고 생각할 수 있습니다. 구체적으로 string_view는 포인터와 길이로만 이루어지며, string_view가 소유하지 않고 수정할 수도 없는 문자 데이터의 한 구간을 가리킵니다. 따라서 string_view를 복사하는 것은 얕은(shallow) 연산이며, 문자열 데이터는 복사되지 않습니다.
string_view는 const char*와 const std::string& 양쪽으로부터 암시적 변환 생성자를 가집니다. 복사를 하지 않으므로 숨겨진 복사에 따른 O(n) 메모리 비용이 없습니다. const std::string&가 전달되는 경우 생성자는 O(1) 시간에 실행됩니다. const char*가 전달되는 경우 생성자는 자동으로 strlen()을 호출합니다(또는 매개변수가 두 개인 string_view 생성자를 쓸 수도 있습니다).
void AlreadyHasString(const std::string& s) {
TakesStringView(s); // 명시적 변환 불필요; 편리하다!
}
void AlreadyHasCharStar(const char* s) {
TakesStringView(s); // 복사 없음; 효율적이다!
}string_view는 데이터를 소유하지 않으므로, string_view가 가리키는 문자열은 (const char*와 마찬가지로) 수명이 명확해야 하며 string_view 자체보다 오래 살아 있어야 합니다.
이는 string_view를 저장 용도로 쓰는 것이 종종 의심스럽다는 뜻입니다. 기반 데이터가 string_view보다 오래 살아남는다는 보장이 필요합니다. 예를 들어 아래 구조체는, 자신을 매개변수로 받은 함수 호출이 끝난 뒤에도 저장될 수 있다면 아마 말이 되지 않습니다.
struct TestScore {
absl::string_view username; // 아마 `std::string`이어야 한다
double score;
};API가 단일 호출 동안에만 문자열 데이터를 참조하면 되고 수정할 필요가 없다면, string_view를 받는 것으로 충분합니다.
이후에 데이터를 사용하거나 수정해야 한다면, std::string(my_string_view)로 C++ string 객체로 명시적으로 변환할 수 있습니다. 또 다른 방법은 Tip #117에서 다루는데, std::string을 값으로 전달하고 호출부에서 적절할 때 std::move를 쓰는 것입니다.
기존 코드베이스에 string_view를 넣는 것이 항상 정답은 아닙니다. 매개변수를 string_view로 전달하도록 바꿨는데 그 값이 다시 std::string이나 NUL로 끝나는 const char*를 요구하는 함수로 전달된다면 오히려 비효율적일 수 있습니다. string_view는 유틸리티 코드에서 시작해 위로 올라가며 도입하거나, 새 프로젝트를 시작할 때 완전히 일관되게 적용하는 것이 가장 좋습니다.
몇 가지 추가 참고사항
-
다른 문자열 타입과 달리,
string_view는int나double을 넘기듯 값(by value)으로 전달해야 합니다.string_view는 작은 값이기 때문입니다. -
string_view를const로 표시하는 것은string_view객체 자체를 수정할 수 있는지에만 영향을 줄 뿐, 그것으로 기반 문자들을 수정할 수 있는지와는 무관합니다 — 애초에 수정할 수 없습니다. 이는const char*에서 포인터 자체는 바꿀 수 있어도 그것으로 문자를 수정할 수는 없는 것과 정확히 같습니다. -
함수 매개변수의 경우, 함수 선언에서
string_view를const로 한정하지 마세요(Tip #109). 함수 정의에서는 주변 코드와의 일관성 등을 위해 재량껏const로 한정해도 됩니다(Tip #109). 그 외 지역 변수에const를 쓰는 것은 권장도 비권장도 아닙니다(스타일 가이드). -
string_view는 반드시 NUL로 끝나지는 않습니다. 따라서 다음처럼 쓰는 것은 안전하지 않습니다.printf("%s\n", sv.data()); // 이렇게 하지 마세요대신 다음을 선호하세요(Tip #124).
absl::PrintF("%s\n", sv); -
string_view는string이나const char*처럼 로그로 남길 수 있습니다.LOG(INFO) << "Took '" << sv << "'"; -
const std::string&나 NUL로 끝나는const char*를 받는 기존 루틴을, 대부분의 경우 안전하게string_view로 바꿀 수 있습니다. 이 작업에서 마주친 유일한 위험은 해당 함수의 주소를 취한 경우인데, 이때는 결과 함수 포인터 타입이 달라져 빌드가 깨집니다. -
string_view는 constexpr 생성자와 trivial 소멸자를 가집니다. 정적/전역 변수(스타일 가이드)나 상수(Tip #140)에 사용할 때 이 점을 염두에 두세요. -
string_view는 사실상 참조(reference)이며, 멤버 변수로는 최선의 선택이 아닐 수 있습니다(Tip #180 참고).
출처: Abseil Tip of the Week #1: string_view — 원저자 Michael Chastain, 2012-04-20 최초 게시, 2020-08-18 갱신. https://abseil.io/tips/1