Abseil Tip 93 absl::Span 사용하기


주간 팁 #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): 더 안전한 코드를 작성할 수 있음.
  • 성능 개선: 불필요한 복사와 할당을 피할 수 있음.

하지만 주의할 점은 Spanstring_view와 유사하게 외부에서 관리하는 데이터에 대한 참조라는 것입니다.
Span이 참조하는 데이터보다 오래 살아남으면 안 됩니다.