제목: “이번 주의 팁 #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)을 두 가지 동작으로 나눌 수 있습니다:
pos >= size()
인 경우std::out_of_range
예외를 던짐.- 그렇지 않은 경우 지정된
pos
에 있는 요소를 반환.
at()
를 언제 사용할 수 있을까요?
at()
사용 여부를 결정할 때 두 가지 상황을 고려해야 합니다:
- 인덱스가 유효하다는 것을 이미 알고 있는 경우
- 인덱스가 유효하지 않을 수 있는 경우
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_map
의 at()
사용은 다른 논리가 적용됩니다. 이러한 연관 컨테이너는 “범위 초과 검사” 대신 필연적으로 키를 조회하기 때문입니다. 예외를 던지지 않을 것임을 확신하는 경우에 at()
를 사용할 수 있지만, 이는 드문 경우입니다. 대부분의 경우 키가 없는 상황을 처리하는 로직이 더 적합합니다.
C++ 예외를 사용하는 환경에서는?
예외가 활성화된 환경에서도 at()
는 제한적으로 사용해야 합니다. 예외를 방어적인 방식으로 사용하는 것이 UB를 줄일 수는 있지만, 숨겨진 예외 처리가 성능과 가독성을 해칠 수 있습니다. 대부분의 경우 명시적인 범위 검사(if
조건문)를 사용하는 것이 더 적합합니다.
마무리
컨테이너에 접근할 때는 다음을 명심하세요:
- 인덱스가 구조적으로 올바른지 확인.
- 올바르지 않을 수 있다면, 명시적으로 확인하여 처리.
이 두 가지 경우 모두 std::vector<T>::at()
대신 더 적합한 방법이 존재합니다. std::optional<T>::value()
나 absl::StatusOr<T>::value()
와 같은 예외 기반 API에서도 비슷한 접근 방식을 취할 수 있습니다. “확인하고 나아가기” 원칙을 지키며, 자체적으로 검사를 포함하는 API 사용은 피하는 것이 좋습니다.