주간 팁 #93: absl::Span
사용하기
원래 게시일: 2015년 4월 23일 (TotW #93)
작성자: Samuel Benzaquen
최종 업데이트: 2023년 5월 8일
빠른 링크: abseil.io/tips/93
Google에서는 소유하지 않은 문자열을 처리할 때 string_view
를 함수 매개변수와 반환 타입으로 사용하는 것이 일반적입니다.
이렇게 하면 API가 더 유연해지고, string
으로 불필요한 변환을 피함으로써 성능이 개선될 수 있습니다. (팁 #1 참조)
string_view
와 유사한 더 일반적인 도구로 absl::Span
이 있습니다.
(google3/third_party/absl/types/span.h
에서 제공)
참고로 absl::Span
은 C++20에 포함된 std::span
과 비슷하지만, 두 타입은 호환되지 않습니다.
Span<const T>
는 std::vector<T>
에 대한 읽기 전용 인터페이스를 제공하며,
벡터가 아닌 타입(예: 배열, 초기화 리스트)에서도 요소 복사 없이 생성할 수 있습니다.
const
를 생략하면, Span<T>
는 배열 요소에 대한 비-상수(non-const) 접근을 허용합니다.
그러나 상수가 아닌 스팬은 명시적으로 생성해야 합니다.
함수 매개변수로 사용하기
Span
을 함수 매개변수로 사용할 때 얻을 수 있는 이점은 string_view
를 사용하는 경우와 유사합니다.
- 호출자는 원래 벡터의 일부분을 전달하거나 배열을 직접 전달할 수 있습니다.
absl::InlinedVector
,absl::FixedArray
,google::protobuf::RepeatedField
등 다른 배열 유사 컨테이너와도 호환됩니다.- 함수 매개변수로 사용할 때는 보통 값으로 전달(by value) 하는 것이 더 낫습니다. 이렇게 하면 약간 더 빠르고 코드 크기도 작아집니다.
예제:
void TakesVector(const std::vector<int>& ints);
void TakesSpan(absl::Span<const int> ints);
void PassOnlyFirst3Elements() {
std::vector<int> ints = MakeInts();
// 임시 벡터를 생성해야 하며, 추가적인 할당 및 복사가 발생합니다.
TakesVector(std::vector<int>(ints.begin(), ints.begin() + 3));
// Span을 사용하면 복사나 추가 할당이 발생하지 않습니다.
TakesSpan(absl::Span<const int>(ints.data(), 3));
}
void PassALiteral() {
// 임시 std::vector<int>를 생성합니다.
TakesVector({1, 2, 3});
// Span은 임시 할당 및 복사가 필요 없으므로 더 빠릅니다.
TakesSpan({1, 2, 3});
}
void IHaveAnArray() {
int values[10] = ...;
// 또다시 임시 std::vector<int>가 생성됩니다.
TakesVector(std::vector<int>(std::begin(values), std::end(values)));
// 배열을 직접 전달합니다. Span은 크기를 자동으로 감지합니다.
// 복사가 발생하지 않습니다.
TakesSpan(values);
}
포인터 벡터의 상수 안전성(Const Correctness)
std::vector<T*>
를 함수 사이에서 전달할 때 가장 큰 문제는 포인터 대상(pointee)을 const로 만들 수 없다는 점입니다.
const std::vector<T*>&
를 전달받는 함수는 벡터를 수정할 수 없지만, T
는 수정할 수 있습니다.
이는 const std::vector<T*>&
를 반환하는 접근자에도 적용됩니다. 호출자가 T
를 수정하지 못하도록 방지할 방법이 없습니다.
일반적인 “해결책”으로는 벡터를 복사하거나 적절한 타입으로 캐스팅하는 방법이 있습니다.
그러나 이 방법은 복사로 인해 느려지거나 캐스팅으로 인해 정의되지 않은 동작(UB)을 유발할 수 있으므로 피해야 합니다. 대신 Span
을 사용하세요.
예제: 함수 매개변수
아래는 Frob
함수의 다양한 버전입니다:
void FrobFastWeak(const std::vector<Foo*>& v);
void FrobSlowStrong(const std::vector<const Foo*>& v);
void FrobFastStrong(absl::Span<const Foo* const> v);
const std::vector<Foo*>& v
가 필요할 때 선택할 수 있는 세 가지 옵션:
// 빠르고 간단하지만 const 안전하지 않음
FrobFastWeak(v);
// 느리고 코드가 복잡하지만 안전함
FrobSlowStrong(std::vector<const Foo*>(v.begin(), v.end()));
// 빠르고 안전하며 명확함!
FrobFastStrong(v);
예제: 접근자
class MyClass {
public:
// 이 함수는 const로 동작해야 합니다.
// Foos를 수정하지 마세요.
const std::vector<Foo*>& shallow_foos() const { return foos_; }
// 진정한 깊은 const.
absl::Span<const Foo* const> deep_foos() const { return foos_; }
private:
std::vector<Foo*> foos_;
};
void Caller(const MyClass* my_class) {
// MyClass::shallow_foos()의 계약을 의도치 않게 위반.
my_class->shallow_foos()[0]->SomeNonConstOp();
// 이 코드는 컴파일되지 않습니다.
// my_class->deep_foos()[0]->SomeNonConstOp();
}
결론
absl::Span
을 적절히 사용하면 다음과 같은 장점이 있습니다:
- 디커플링(Decoupling): 데이터 소유와 함수 인터페이스를 분리할 수 있음.
- 상수 안전성(Const Correctness): 더 안전한 코드를 작성할 수 있음.
- 성능 개선: 불필요한 복사와 할당을 피할 수 있음.
하지만 주의할 점은 Span
이 string_view
와 유사하게 외부에서 관리하는 데이터에 대한 참조라는 것입니다.
Span
이 참조하는 데이터보다 오래 살아남으면 안 됩니다.