Abseil Tip 227 빈 컨테이너와 부호 없는 정수 연산 주의하기

제목: “이번 주의 팁 #227: 빈 컨테이너와 부호 없는 정수 연산 주의하기”

원문 게시일: 2023년 11월 16일
업데이트: 2024년 3월 11일

작성자: James Dennett

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


인덱스 기반 반복문: 여전히 유용한 경우가 있다

범위 기반 for 루프가 등장하면서 인덱스 기반 for 루프는 현대 C++ 코드에서 덜 자주 사용됩니다. 그러나 여전히 인덱스가 필요한 경우가 있습니다. 예를 들어, 여러 컨테이너를 병렬로 순회하거나, 하나의 컨테이너에서 인접한 여러 요소를 처리하려는 경우입니다. 이번 팁에서는 두 번째 경우에 발생할 수 있는 문제를 살펴봅니다.


문제 있는 코드

다음은 올바를 것처럼 보이는 코드입니다:

for (int64_t i = 0; i < v.size() - 1; ++i) {
  ProcessPair(v[i], v[i+1]);
}

이 코드는 ProcessPair()를 호출하기 전에 유효한 인덱스를 확인하는 것처럼 보입니다. 하지만 이 코드는 “올바를 것처럼 보일 뿐”입니다. 예를 들어, 컨테이너 v가 비어 있는 경우를 고려하지 않은 것입니다. 만약 v가 비어 있는 상태로 이 루프가 실행된다면, C++는 예기치 않은 동작을 일으킬 수 있습니다.


부호 없는 정수의 문제

C++ 표준에서는 컨테이너 크기를 나타내기 위해 부호 없는 정수 타입을 사용합니다 (v.size()unsigned 타입). 그러나 부호 없는 정수는 연산 시 문제가 발생할 수 있습니다.

예를 들어, v.size()가 0인 경우, v.size() - 1은 -1이 아니라 최대값(예: std::size_t의 경우 18446744073709551615)으로 변환됩니다. 결과적으로, 조건 i < v.size() - 1은 잘못 평가될 수 있습니다. 즉, 루프가 실행되고 v[i]와 같은 코드에서 정의되지 않은 동작(UB)이 발생할 수 있습니다.


올바르게 수정하기

문제를 해결하기 위해 코드의 의도를 더 명확히 표현하면 문제가 사라집니다.

의도를 더 직접적으로 표현하기

루프 조건의 목적은 ii + 1이 모두 유효한 인덱스인지 확인하는 것입니다. 이를 C++로 직접적으로 번역하면 다음과 같이 표현됩니다:

  • 인덱스 i가 유효하려면 i < v.size()여야 합니다.
  • 인덱스 i + 1이 유효하다면, 이는 i < v.size() - 1과 동등합니다. 하지만 i + 1 < v.size()로 표현하면 더 안전합니다.

새로운 조건인 i + 1 < v.size()v.size()에서 1을 빼는 연산을 피하므로, 부호 없는 정수의 오버플로를 방지합니다.


수정된 코드

수정된 코드는 다음과 같습니다:

for (int64_t i = 0; i + 1 < v.size(); ++i) {
  ProcessPair(v[i], v[i+1]);
}

이 변경으로 인해:

  1. v가 비어 있을 때에도 루프가 안전하게 작동합니다.
  2. 인덱스 범위 확인이 더 명확하고 직관적으로 표현됩니다.

요약

수정된 코드는 몇 바이트만 바뀌었지만, 중요한 몇 가지 교훈을 제공합니다:

  • 부호 없는 정수 연산에 주의하세요. 부호 없는 정수는 예상치 못한 결과를 초래할 수 있습니다.
  • 컨테이너 크기(container.size())는 부호 없는 타입을 반환한다는 점을 기억하세요.
  • 코드의 올바름을 가능한 한 로컬하게 확인하세요.
  • 의도를 직접적으로 표현하세요. 코드는 사람이 읽기 쉬워야 합니다.

이 팁을 통해 부호 없는 정수와 관련된 오류를 방지하고 더 안전한 코드를 작성할 수 있기를 바랍니다.