Abseil Tip 141 bool로의 암시적 변환에 주의하라

주간 팁 #141: bool로의 암시적 변환에 주의하라


원래 게시일: 2018년 1월 19일 (TotW #141)
작성자: Samuel Freilich
최종 업데이트: 2020년 4월 6일
빠른 링크: abseil.io/tips/141


두 가지 널 포인터 검사 방법

널 포인터를 역참조하기 전에 검사는 충돌과 버그를 방지하기 위해 중요합니다. 이 검사는 두 가지 방법으로 수행할 수 있습니다:

if (foo) {
  DoSomething(*foo);
}
if (foo != nullptr) {
  DoSomething(*foo);
}

두 조건문의 의미는 foo가 포인터인 경우 동일하지만, 후자의 경우 타입 검사가 조금 더 엄격합니다.
C++에서는 많은 타입이 bool로 암시적 변환될 수 있으므로,
특히 포인터 대상 타입 자체가 bool로 변환될 수 있는 경우에는 주의가 필요합니다.

다음 코드를 살펴보세요. 이 코드는 두 가지로 해석될 수 있습니다:

bool* is_migrated = ...;

// 이것은 `is_migrated`가 null이 아닌지 확인하는 것인가, 아니면
// 실제로 `*is_migrated`가 true인지 확인하려는 것인가?
if (is_migrated) {
  ...
}

이 코드는 더 명확하게 작성할 수 있습니다:

// bool*에 대한 null 포인터 검사를 수행합니다.
if (is_migrated != nullptr) {
  ...
}

Google C++ 스타일에서는 두 스타일 모두 허용됩니다.
기본 타입이 bool로 암시적 변환되지 않는 경우 주변 코드 스타일을 따르세요.
스마트 포인터(std::unique_ptr 등)를 사용하는 경우에도 유사한 원칙이 적용됩니다.


선택적 값(Optional Values) 및 범위 제한

std::optional 같은 선택적 값을 사용할 때는 더욱 신중하게 고려해야 합니다.

예를 들어:

std::optional<bool> b = MaybeBool();
if (b) { ... }  // 함수가 `std::optional(false)`를 반환하면 어떻게 될까요?

if 문에서 변수 선언을 하면 변수의 범위를 제한할 수 있지만,
값이 암시적으로 bool로 변환되기 때문에 어떤 불리언 속성이 테스트되는지 명확하지 않을 수 있습니다.

다음과 같이 작성하면 의도를 더 명확하게 표현할 수 있습니다:

std::optional<bool> b = MaybeBool();
if (b.has_value()) { ... }

실제로 위 코드와 아래 코드는 동등합니다.
std::optionalbool로의 변환은 optional이 값이 있는지 여부만 확인하며, 내용물 자체는 확인하지 않습니다.
독자는 optional(false)true로 평가된다는 사실을 직관적으로 이해하기 어려울 수 있지만,
optional(false)가 값을 가지고 있다는 사실은 명확합니다.
기본 타입이 bool로 암시적 변환될 수 있는 경우에는 특히 주의가 필요합니다.


선택적 반환값을 사용하는 패턴

if 문에 변수 선언을 넣는 패턴은 변수의 범위를 제한할 수 있지만, 암시적 bool 변환을 포함합니다:

if (std::optional<Foo> foo = MaybeFoo()) {
  DoSomething(*foo);
}

참고: C++17에서는 if 문에 초기화 구문을 포함할 수 있으므로,
변수의 범위를 제한하면서도 암시적 변환을 피할 수 있습니다:

if (std::optional<Foo> foo = MaybeFoo(); foo.has_value()) {
  DoSomething(*foo);
}

“불리언과 유사한” 열거형

팁 #94의 권장사항에 따라 함수 시그니처에서 가독성을 위해 bool 대신 enum을 사용했다고 가정해 봅시다.
이러한 리팩토링은 함수 정의에서 암시적 변환을 초래할 수 있습니다:

void ParseCommandLineFlags(
    const char* usage, int* argc, char*** argv,
    StripFlagsMode strip_flags_mode) {
  if (strip_flags_mode) {  // 이 값이 true일 때 어떤 의미인가요?
    ...
  }
}

암시적 변환 대신 명시적 비교를 사용하면 더 명확해집니다:

void ParseCommandLineFlags(
    const char* usage, int* argc, char*** argv,
    StripFlagsMode strip_flags_mode) {
  if (strip_flags_mode == kPreserveFlags) {
    ...
  }
}

요약

bool로의 암시적 변환은 명확성을 떨어뜨릴 수 있으므로, 보다 명시적인 코드를 작성하는 것이 좋습니다:

  1. 포인터 타입을 검사할 때는 nullptr과 비교하세요.
    (특히 포인터 대상 타입이 암시적으로 bool로 변환될 수 있는 경우)
  2. 컨테이너 비어 있음을 검사할 때는 std::optional<T>::has_value() 같은 불리언 함수를 사용하세요.
    (if의 선택적 초기화 형식을 사용하여 변수 범위를 제한하세요.)
  3. 열거형은 특정 값과 비교하세요.