본문 바로가기
컴퓨터/Modern Effective C++ 정리

Modern Effective C++ Item 10 : enum <<< enum class

by 김짱쌤 2015. 3. 24.

Item 10 : enum <<< enum class

enum을 선언하면 enum의 값들은 범위 없이 날뛴다.

그래서 C++98에서부터 사용되던 enum은 unscoped enum이라고 부른다.

enum Color{ black, white, red };
...
auto white = false; //white가 이미 사용중인 변수 명이기 때문에 에러발생

이런 무분별한 영역 침범을 막기 위해서 c++11 부터 분명한 영역을 설정하는 이른바 scoped enum을 지원하게 되었다.

enum class Color{black, white, red}; //선언시 enum class로 선언
...
auto white = false;    //문제없이 동작한다.
Color c = white;       //영역설정이 안되어있으므로 에러
Color c = Color::white //이렇게 쓴다.
auto c = Color::white  //auto 추론도 문제없이 작동

기존의 enum은 정수형으로 마구잡이 형변환 되었다.

enum의 의미와는 동떨어진 연산을 사용할 가능성이 있었다.

enum Color { black, white, red }; //옛 enum쓰기
std::vector<std::size_t>
  primeFactors(std::size_t x);    //소인수들을 리턴하는 함수.

Color c = red;
...
if(c < 14.5){                     //색깔을 double과 비교하면??
   auto factor = primeFactor(c);  //색깔의 소인수들을 구한다??
...
}

하지만 enum class를 쓰면 위와같은 암묵적 형변환들을 금지할 수 있다.
enum class는 따로 형변환을 하지 않는 이상 반드시 enum으로 사용하게 한다.

enum class Color { black, white, red }; //새 enum쓰기
std::vector<std::size_t>
  primeFactors(std::size_t x);    //소인수들을 리턴하는 함수.

Color c = red;
...
if(c < 14.5){                     //에러 발생
   auto factor = primeFactor(c);  //에러 발생
...
}

if(static_cast<double>(c) < 14.5){
   auto factor = primeFactor(static_cast<std::size_t>(c));
 //번거롭지만 이렇게 써서 안정성을 높일 수 있다.
...                              
}

enum은 전방선언이 안된다.

정확하게 말하면 안되었다. 약간의 추가적인 작업을 통해 C++11부터는 unscoped enum들도 전방선언이 가능하다. 일단 이 문제의 근원은 기존 enum의 타입이 때에따라 달라진다는 것이다.

enum Color{black, white, red}; 
//필요 표현량이 적으므로 complier는 Color를 char로 취급한다.

enum Status { good = 0,
              failed = 1,
              incomplete = 100,
              corrupt = 200,
              indeterminate = 0xFFFFFFFF
            };                 
//필요 표현범위가 0~0xFFFFFFFF 이므로 Status는 int타입으로 취급한다.

메모리 최적화를 위하여 컴파일러가 해당 enum에 필요한 최소 타입을 enum의 내부 타입으로 사용하므로, 내용을 파악하기전에 구체적인 타입도 모르는 enum을 선언할 수 없는 것이다. enum의 실제 타입이 컴파일러에 의존하므로, enum의 내용이 변경되면 컴파일러가 다시 enum의 타입을 추론해야하는 문제가 발생한다. 만일 enum이 많은 파일이 include하는 헤더에 정의되어 있을때, enum값 하나만 바꿔도 거의 전체를 다시 빌드해야할수 있다.

enum Status { good = 0, failed = 1, incomplete = 100, corrupt = 200, audited = 500, //위의 enum Status에서 값하나 추가되었을 뿐인데... indeterminate = 0xFFFFFFFF }; //하지만 내용이 바뀌었으니 re컴파일해보지 않고는 어떤 타입이 될지 알 수 없는것...

하지만 enum class는 그 타입을 고정시켜서 사용하기 때문에, 전방선언이 가능하며 내용을 바꿔도 다시 컴파일해야되는 문제가 없다.

enum class Status; //기본적으로 int형을 사용한다. 전방선언에 문제 없다.

메모리 최적화를 포기한 대신 편리하게 사용할 수 있게한다. 물론 이런 경우를 대비하여 만약 사용자가 원한다면 직접 내부 타입을 설정할 수 있다.

enum class Status: std::uint32_t //enum의 내부 타입을 std::uint32_t로 직접 설정

이제는 unscoped enum(일반 enum)도 내부 타입을 명시할 수 있게 바뀌었기 때문에

enum Status: std::uint8_t; //이렇게 전방선언이 가능하다.

enum class에도 약점은 있다.

암시적 형변환을 강력하게 막고 있기 때문에, enum을 index처럼 사용하는 경우 큰 짜증을 유발하게 된다.

using UserInfo = std::tuple<std::string, std::string, std::size_t>;

enum class UserInfoField 
{ uiName, 
  uiEmail, 
  uiReputation 
}; //enum class가 좋다고 해서 한번 써보았습니다.
UserInfo uInfo;
...
auto val = std::get<uiEmail>(uInfo); //에러가 딱!
auto val = std::get<static_cast<std::size_t>
                    (UserInfoField::uiEmail)>(uInfo); //제대로 쓰려면..ㅠㅠ

약간의 트릭으로 이 문제를 극복할 수 있다. enum class가 가지고 있는 내부 타입으로 직접 형변환하여 리턴하는 템플릿 함수를 만드는 것! std::underlying_type<T>이라는 표준함수를 통해서 enum class의 내부 타입을 확인 할 수 있다. constexpr과 noexcept가 다시 등장하지만 차후 설명할 것이므로 여기선 설명을 넘긴다.

template<typename E>
constexpr typename std::underlying_type<E>::type
   toUType(E enumerator) noexcept
{
   return
     static_cast<typename std::underlying_type<E>::type>
                 (enumerator);
}

C++ 14부터는 std::underlying_type<E>::type std::underlying_type_t<E>로 대체해서 쓸 수 있다. 또한 리턴 타입을 auto로 추론하는 것도 가능하므로 위의 템플릿 함수를 보다 가독성있게 변경하는 것이 가능하다.

template<typename E> // C++14의 우월함
constexpr auto
   toUType(E enumerator) noexcept
{
   return static_cast<std::underlying_type_t<E>>(enumerator);
}

이 템플릿 함수의 도움을 받아 우린 좀 덜 성가신 방법으로 enum class를 형변환 하여 사용할 수 있다.

auto val = std::get<toUType(UserInfoField::uiEmail)>
                (uInfo); //적어도 이전버젼보다는 훌륭하다.

여전히 불편해 보이지만 기존 enum의 불편함과 불안정함을 고려해보았을때, enum class를 쓰는것을 강하게 추천하고 싶다.