Abseil Tip 224 vector.at() 사용 피하기

제목: “이번 주의 팁 #224: vector.at() 사용 피하기”

원문 게시일: 2023년 8월 24일
업데이트: 2024년 1월 24일

작성자: Titus Winters

빠른 링크: abseil.io/tips/224


at()는 무엇을 하나요?

at(size_type pos) 메서드의 동작은 다음과 같이 정의됩니다:

지정된 위치 pos의 요소에 대한 참조를 반환하며, 범위를 초과하면 std::out_of_range 예외를 던집니다.

이 메서드의 계약(contract)을 두 가지 동작으로 나눌 수 있습니다:

  1. pos >= size()인 경우 std::out_of_range 예외를 던짐.
  2. 그렇지 않은 경우 지정된 pos에 있는 요소를 반환.

at()를 언제 사용할 수 있을까요?

at() 사용 여부를 결정할 때 두 가지 상황을 고려해야 합니다:

  1. 인덱스가 유효하다는 것을 이미 알고 있는 경우
  2. 인덱스가 유효하지 않을 수 있는 경우

1. 인덱스가 유효한 경우

이미 범위가 충분히 크고 조회가 성공할 것임을 알고 있다면, 추가적인 범위 검사는 오버헤드입니다. 대부분의 경우 vector 접근은 0부터 size()까지 루프를 도는 형태이므로, 범위 검사가 필요하지 않습니다. 이런 경우는 operator[]를 사용하는 것이 더 적합합니다:

// 비효율적인 코드
for (int i = 0; i + 1 < vec.size(); ++i) {
  ProcessPair(vec.at(i), vec.at(i + 1));
}

// 효율적인 코드
for (int i = 0; i + 1 < vec.size(); ++i) {
  ProcessPair(vec[i], vec[i + 1]);
}

2. 인덱스가 유효하지 않을 수 있는 경우

범위가 충분히 크지 않다면, 예외를 던지는 것이 적절한 처리 방법일까요? 대부분의 경우 그렇지 않습니다. 특히 Google 스타일의 빌드에서는 예외가 프로그램을 강제로 종료시키는 결과를 초래할 수 있습니다.

다음 예제를 살펴보겠습니다:

// 비효율적인 코드
std::vector<absl::string_view> tokens = absl::StrSplit(user_string, ByChar(','));
LOG(INFO) << "Got leading token " << tokens.at(0);

이 코드는 아래와 같이 개선할 수 있습니다:

// 더 나은 코드: 명시적으로 범위 확인
std::vector<absl::string_view> tokens = absl::StrSplit(user_string, ByChar(','));
if (tokens.empty()) {
  return absl::InvalidArgumentError("Invalid user_string, expected ','");
}

또는 프로그램 종료가 적절하다면:

// 명시적으로 오류를 확인하고 종료
std::vector<absl::string_view> tokens = absl::StrSplit(user_string, ByChar(','));
CHECK(!tokens.empty()) << "Invalid user_string "
                       << std::quoted(user_string)
                       << ", expected at least one ','";

UB(정의되지 않은 동작)를 피하려면?

at()는 UB를 피하는 방법으로도 고려될 수 있습니다. 하지만 at()는 예외 처리로 인해 추가적인 비용이 발생하며, 이상적인 해결책은 아닙니다. 더 나은 해결책은 다음과 같습니다:

  • operator[]를 사용하되, 다른 방법으로 UB를 줄이는 조치를 취합니다.
  • ASAN(AddressSanitizer)과 같은 도구를 사용해 범위 초과 접근을 탐지합니다.

맵에서의 at() 사용

std::map이나 std::unordered_mapat() 사용은 다른 논리가 적용됩니다. 이러한 연관 컨테이너는 “범위 초과 검사” 대신 필연적으로 키를 조회하기 때문입니다. 예외를 던지지 않을 것임을 확신하는 경우에 at()를 사용할 수 있지만, 이는 드문 경우입니다. 대부분의 경우 키가 없는 상황을 처리하는 로직이 더 적합합니다.


C++ 예외를 사용하는 환경에서는?

예외가 활성화된 환경에서도 at()는 제한적으로 사용해야 합니다. 예외를 방어적인 방식으로 사용하는 것이 UB를 줄일 수는 있지만, 숨겨진 예외 처리가 성능과 가독성을 해칠 수 있습니다. 대부분의 경우 명시적인 범위 검사(if 조건문)를 사용하는 것이 더 적합합니다.


마무리

컨테이너에 접근할 때는 다음을 명심하세요:

  1. 인덱스가 구조적으로 올바른지 확인.
  2. 올바르지 않을 수 있다면, 명시적으로 확인하여 처리.

이 두 가지 경우 모두 std::vector<T>::at() 대신 더 적합한 방법이 존재합니다. std::optional<T>::value()absl::StatusOr<T>::value()와 같은 예외 기반 API에서도 비슷한 접근 방식을 취할 수 있습니다. “확인하고 나아가기” 원칙을 지키며, 자체적으로 검사를 포함하는 API 사용은 피하는 것이 좋습니다.