[EMC++] Item 10. unscoped enum보다 scoped enum을 선호해라
Item 10. unscoped enum보다 scoped enum을 선호해라
enum (unscoped enum)
enum
은 열거형(enumerated type)의 약자로 가능한 모든 값이 기호 상수로 정의되는 자료형을 의미한다.
enum
은 코드를 문서화하고 가독성을 높이는 목적으로 매우 유용하다.
enum Color {
BLACK, // 0 할당
WHITE, // 1 할당
RED, // 2 할당
};
Color paint(WHITE);
std::cout << paint; // 2
enum Animal {
CAT = -3,
DOG, // -2 할당
BIRD, // -1 할당
CHICKEN = 5,
ELEPHANT // 6 할당
};
Animal animal = 5; // 에러, 정수를 열거형으로 암시적 형변환 불가
Aniaml animal = static_cast<Animal>(5); // 명시적 형변환은 가능
// 열거형은 고유한 자료형으로 간주되어 열거형에 다른 열거형을 할당하려고 하면 컴파일 에러가 발생한다
Animal animal = RED; // 에러
enum
은 범위 없는 열거형(unscoped enum)이라고 불린다.
그 이유는 일반적으로 한 중괄호 쌍 안에서 어떤 이름을 선언하면 그 이름의 가시성은 해당 중괄호 쌍이 정의하는 범위로 한정되어야 하는데 enum
은 그 범위를 벗어나기 때문이다.
enum Color { black, white, red };
auto white = false; // 에러, 이 범위에 이미 white가 선언되어 있다고 인식
enum class(scoped enum)
기존 enum
의 문제를 해결하기 위해서 C++11부터 범위 있는 열거형(scoped enum)이라 불리는 enum class
가 등장했다.
enum class의 장점
enum class
의 장점은 다음과 같다.
namespace의 오염을 줄여줌
enem class
는 ::
(범위 연산자) 없이는 범위를 벗어나서 사용할 수 없다.
enum class Color { black, white, red };
auto white = false; // 가능, 이 범위에 다른 white는 없음
Color c = white; // 에러, 이 범위에 white라는 이름의 열거자가 없음
Color c = Color::white; // 가능, ::(범위 연산자) 사용
auto c = Color::white; // 가능
열거자들의 형식이 강력하게 적용됨
기존 enum
은 열거자들이 암묵적으로 정수 타입으로 변환된다.
enum Color { black, white, red }; // 범위 없는 enum
std::vector<std::size_t> primeFactors(std::size_t x); // x의 소인수들을 돌려주는 함수
Color c = red;
if (c < 14.5) { // Color를 double과 비교
auto factors = primeFactors(c); // Color의 소인수들을 계산
}
따라서 위와 같이 문제 있는 코드도 동작하게 된다.
하지만 enum class
는 열거자들이 암묵적으로 다른 타입으로 변환되지 않는다.
enum class Color { black, white, red };
Color c = Color::red;
if (c < 14.5) { // 에러, Color와 double을 비교할 수 없음
auto factors = primeFactors(c); // 에러, std::size_t를 기대하는 함수에 Color를 전달할 수 없음
}
// 정말 바꿔서 사용하고 싶으면 명시적으로 타입 변환을 해야한다
if (static_cast<double>(c) < 14.5) { // 이상한 코드이지만 어쨌든 유효함
auto factors = primeFactors(static_cast<std::size_t>(c)); // 의심스럽지만 컴파일은 됨
}
전방 선언(forward declaration) 가능
enum class
를 사용하면 열거자들을 지정하지 않고 열거형 이름만 미리 선언하는 전방 선언(forward declaration)이 가능하다.
enum Color; // 에러
enum class Color; // 가능
왜 enum
은 전방 선언이 안되고 enum class
는 되는지 이해하려면 열거형의 바탕 타입(underlying type)에 대한 이해가 필요하다.
바탕 타입(underlying type)
열거형의 바탕 타입(underlying type)은 컴파일러가 결정한 열거형의 타입을 말한다.
바탕 타입은 enum
(unscoped enum)과 enum class
(scoped enum) 모두 지원하며 열거형에 특별한 타입을 명시하지 않고 정의하면 컴파일러가 내부적으로 최적화된 타입을 결정한다.
// 1bit로 열거형을 표현 가능하기 때문 컴파일러가 내부적으로 바탕 타입을 char로 결정
enum Color { black, white, red };
// 1bit로 열거형을 표현할 수 없으므로 컴파일러가 내부적으로 바탕 타입을 char보다 더 큰 타입으로 결정
enum Status {
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
열거형을 전방 선언(forward declaration)할 경우 컴파일러는 열거형의 값을 보고 바탕 타입을 결정할 수 없다.
이때 enum class
는 기본 바탕 타입 int를 지원하지만 enum
은 기본 바탕 타입이 없다.
따라서 enum
을 전방 선언할 경우 컴파일러가 바탕 타입을 결정할 수 없어 에러가 발생하는 것이다.
enum Color; // 에러, enum은 기본 바탕 타입이 없음
enum class Color; // 가능, enum class는 기본 바탕 타입이 int
enum
을 전방 선언하고 싶으면 바탕 타입을 직접 명시해줘야 한다.
enum Color: std::uint8_t; // enum의 전방 선언, 바탕 타입은 std::uint8_t
enum class Status; // enum class의 전방 선언, 기본 바탕 타입은 int
// 기본 바탕 타입도 명시적으로 지정 가능
enum class Status: std::uint32_t; // enum class의 전방 선언, 바탕 타입은 std::uint32_t
// enum을 정의할 때에도 바탕 타입을 지정할 수 있음
enum class Status: std::uint32_t {
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
enum
을 조금만 수정해도 enum
을 쓰는 모든 코드를 다시 컴파일해야 하는 불상사를 막기 위해서는 전방 선언이 꼭 필요하다.
unscoped enum이 유용한 경우
C++11의 std::tuple
안에 있는 필드들을 지칭할 때는 enum class
보다 enum
을 사용하는 것이 더 유용할 수 있다.
어떤 소셜 네트워크 웹사이트의 사용자 이름과 이메일 주소, 그리고 평판치(reputation value)를 저장하는 std::tuple
을 구현해보자.
using UserInfo = std::tuple<std::string, // 사용자 이름
std::string, // 이메일 주소
std::size_t>; // 평판치
UserInfo uInfo; // tuple 타입의 객체
auto val = std::get<1>(uInfo); // 필드 1의 값을 얻는다.
주석에는 std::tuple
의 각 필드가 뜻하는 의미가 나와 있지만, 다른 소스 파일에서 다음과 같은 코드와 마주친다면 해당 필드가 무엇을 의미하는지를 바로 알기 힘들다.
이럴 때 enum
으로 필드 번호를 필드 이름과 연관시키면 그런 것들을 일일이 기억할 필요가 없다.
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<uiEmail>(uInfo); // 이메일 필드의 값을 얻음이 명확함
물론 enum class
를 사용해서 구현할 수도 있지만 enum
을 사용한 방식보다 훨씬 장황하다.
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);