주간 팁 #147: Exhaustive switch
문을 책임감 있게 사용하기
원래 게시일: 2018년 4월 25일
작성자: Jim Newsome
최종 업데이트: 2020년 4월 6일
바로가기: abseil.io/tips/147
소개
-Werror
컴파일러 플래그를 사용하면 enum
타입의 값을 대상으로 하는 switch
문에 default
레이블이 없을 경우, 모든 열거자가 case
문으로 처리되지 않으면 컴파일 오류가 발생합니다. 이를 exhaustive 또는 defaultless switch
문이라고 부르기도 합니다.
Exhaustive switch
문은 특정 enum
의 모든 열거자가 명시적으로 처리되었는지 컴파일 시점에 확인할 수 있는 훌륭한 도구입니다. 그러나 변수가 열거자 외의 값(법적으로 허용됨)을 가질 수 있는 경우를 처리하고 다음 중 하나를 충족해야 합니다.
enum
의 소유자가 새로운 열거자를 추가하지 않을 것을 보장하거나,enum
의 소유자가 새로운 열거자를 추가할 때마다 우리 코드를 수정할 의지와 능력이 있거나(예:enum
정의가 동일 프로젝트 내에 있을 경우),enum
의 소유자가 새로운 열거자를 추가했을 때, 우리 코드의 빌드가 깨져도 그들이 차단되지 않으며, 최신 버전으로 업데이트할 때 우리 코드의switch
문을 수정할 의지가 있을 경우.
초기 시도
예를 들어, 특정 enum
의 각 열거자를 std::string
으로 매핑하는 함수를 작성한다고 가정해 봅시다. 모든 열거자가 누락되지 않았는지 확인하기 위해 exhaustive switch
문을 사용해 봅니다.
std::string AnEnumToString(AnEnum an_enum) {
switch (an_enum) {
case AnEnum::kFoo:
return "kFoo";
case AnEnum::kBar:
return "kBar";
case AnEnum::kBaz:
return "kBaz";
}
}
AnEnum
이 세 가지 열거자만 가지고 있다고 가정하면 이 코드는 컴파일되고 원하는 효과를 얻을 수 있는 것처럼 보입니다. 그러나 두 가지 중요한 문제가 있습니다.
열거자 외의 값을 가진 enum
C++에서 enum
은 명시적으로 정의된 열거자 외의 값을 가질 수 있습니다. 모든 enum
은 명시된 열거자를 모두 표현할 수 있는 크기의 정수형 타입의 값들을 가질 수 있으며, enum class
와 같은 고정된 기본 타입을 사용하는 enum
은 해당 타입으로 표현 가능한 모든 값을 가질 수 있습니다. 이는 비트 필드로 enum
을 사용하거나 컴파일 시점에 존재하지 않던 열거자를 나타내기 위해 의도적으로 활용되기도 합니다.
위 코드에서 an_enum
이 처리되지 않은 값일 경우 어떻게 될까요?
switch
문에 매칭되는 case
가 없고 default
레이블도 없는 경우, 실행은 switch
문을 통과하여 다음 코드로 넘어갑니다. 이로 인해 예기치 못한 동작이 발생할 수 있으며, 위 예제에서는 함수의 반환값이 없는 상태에서 종료되어 정의되지 않은 동작(undefined behavior)이 발생합니다.
이 문제는 switch
문에서 처리되지 않은 경우를 명시적으로 다루어 런타임에 항상 정의된 동작을 보장함으로써 해결할 수 있습니다. 아래는 수정된 코드 예제입니다.
std::string AnEnumToString(AnEnum an_enum) {
switch (an_enum) {
case AnEnum::kFoo:
return "kFoo";
case AnEnum::kBar:
return "kBar";
case AnEnum::kBaz:
return "kBaz";
}
LOG(ERROR) << "Unexpected value for AnEnum: " << static_cast<int>(an_enum);
return kUnknownAnEnumString;
}
이제 an_enum
이 어떤 값을 가지더라도 정의된 동작을 보장합니다. 그러나 여전히 잠재적인 문제가 남아 있습니다.
새로운 열거자가 추가되면 어떻게 될까?
AnEnum
에 새로운 열거자가 추가되었다고 가정해 봅시다. 이로 인해 AnEnumToString
함수는 더 이상 컴파일되지 않습니다. 이것이 문제가 되는지 여부는 AnEnum
의 소유자가 누구인지와 그들이 제공하는 보장 사항에 따라 달라집니다.
동일한 프로젝트 내 enum
AnEnum
이 동일 프로젝트의 일부인 경우, 새로운 열거자를 추가하는 엔지니어는 컴파일 오류로 인해 변경사항을 제출하기 전에 AnEnumToString
을 수정해야 할 것입니다. 이는 일반적으로 수정 가능하므로 문제가 되지 않습니다.
다른 프로젝트의 enum
AnEnum
이 다른 프로젝트의 일부인 경우, 새로운 열거자를 추가해도 우리 코드는 그들의 작업을 차단하지 않습니다. 다만, 우리 팀이 그들의 최신 버전으로 업데이트할 때 문제가 발견될 수 있습니다.
권장 사례
switch
문을 사용하여 enum
을 처리하려는 경우, 다음 중 하나가 충족되는지 확인해야 합니다.
enum
소유자가 새로운 열거자를 추가하지 않을 것을 명시적으로 보장하거나,- 새로운 열거자를 추가할 때마다 수정할 준비가 되어 있거나,
- 빌드가 깨지더라도 소유자가 차단되지 않도록 할 것.
결론
Exhaustive switch
문은 강력한 도구이지만 다음을 반드시 지켜야 합니다.
enum
이 열거자 외의 값을 가질 경우를 명시적으로 처리하기.- 사용하는
enum
타입이 확장 가능성을 명확히 정의했는지 확인하기.
추가적인 정보는 protobuf 문서를 참조하세요.