스토리텔링 개발자

[Effective Modern C++] 10. enum class 본문

Effective C++/Effective Modern C++

[Effective Modern C++] 10. enum class

김디트 2025. 2. 18. 11:16
728x90

항목 10. 범위 없는 enum보다 범위 있는 enum을 선호하라

 

 

 

enum과 enum class의 범위
  • 일반적으로는 중괄호가 범위이다.
  • 헌데, C++98 스타일의 enum은 이 일반적인 규칙이 적용되지 않는다.
enum Color { black, white, red }; // 범위 없는 enum(unscoped enum)

auto white = false; // 이미 white가 선언되어 있다는 에러!
  • C++11의 범위 있는 enum(scoped enum, enum class)는 이름 누수가 발생하지 않는다.
enum class Color { black, white, red }; // 범위 있는 enum(scoped enum)

auto white = false; // 성공

Color c = white; // white라는 이름의 enum을 찾을 수 없다.

Color c = Color::white; // 성공

 

 

 

강력한 타입 적용
  • 범위 없는 enum은 암시적 변환이 널널한데 비해, 범위 있는 enum은 그를 허용하지 않는다.
// 범위 없는 enum 버전
enum Color { black, white, red };

std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;

if(c < 14.5) // 암시적 변환으로 int로 치환되어 성공
{
    auto factors = primeFactors(c); // 암시적 변환으로 역시 성공
    ...
}

// 범위 있는 enum 버전
enum class Color { black, white, red };

std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;

if(c < 14.5) // 컴파일 에러! Color와 double을 비교할 수 없다.
{
    auto factors = primeFactors(c); // 컴파일 에러! std::size_t에 Color을 전달할 수 없다.
    ...
}
  • enum class의 컴파일 에러는 캐스팅으로 해결할 수 있으므로,
    • 의도치 않은 암시적 변환이 발생하지 않는 enum class 쪽이 안전하다.

 

 

 

전방 선언
  • 범위 있는 enum은 전방 선언(forward declaration)이 가능하다.
enum Color; // 에러!
enum class Color; // 성공
  • 헌데 사실 추가작업을 조금 해주면 C++11에서는 범위 없는 enum도 전방 선언이 가능하다.
    • C++에서의 모든 enum은 컴파일러가 결정하는 기저 타입(underlying type)이 정수 타입이라는 사실을 이용한다.
  • 기저 타입 결정
    • enum Color { black, white, red }; // 표현값이 세개 뿐이므로 char 선택
      
      enum Status { good = 0,
                    failed = 1,
                    incomplete = 100,
                    corrupt = 200,
                    indeterminate = 0xFFFFFFF
                  }; // char보단 큰 정수 타입을 선택해야 할 것이다.
    • 메모리를 효율적으로 사용하기 위해 주어진 enum의 범위를 표현할 수 있는 가장 작은 기저 타입을 선택하는 경향이 있다.
    • 그러나 경우에 따라서는 컴파일러가 크기 대신 속도를 위한 최적화를 적용하므로, 꼭 그렇지 않은 상황도 있다.
    • 아무튼 기저 타입을 미리 선택하여 최적화를 적용하길 원하며, 따라서 C++98은 enum 정의만 지원하고 enum 선언은 지원하지 않는다.
  • 전방 선언을 하지 못하면 컴파일 의존 관계 문제가 발생한다.
enum Status { good = 0,
              failed = 1,
              incomplete = 100,
              corrupt = 200,
              indeterminate = 0xFFFFFFF
            }; // 시스템 전반에 쓰이는 enum
            
// 만약 아래처럼 요소가 추가된다면?

enum Status { good = 0,
              failed = 1,
              incomplete = 100,
              corrupt = 200,
              audited = 500,
              indeterminate = 0xFFFFFFF
            }; // 시스템 전체를 재컴파일!!!
            
            
// 만약 enum class라면 헤더 전방 선언으로 재컴파일을 막을 수 있다.

enum class Status; // 전방 선언

void continueProcessing(Status s); // 전방 선언된 enum 사용
  • 그래서 C++11에서 범위 없는 enum을 전방 선언 하는 방법은?
    • 범위 있는 enum의 기저 타입은 컴파일러가 언제라도 알 수 있다.
      • 기저 타입은 기본적으로 int이다.
    • 모든 버전의 enum의 기저 타입은 프로그래머가 직접 정의할 수 있다.
      • enum class Status : std::uint32_t; // 기저 타입 재지정
        
        enum Status : std::uint32_t; // 범위 없는 버전에도 지원된다.
        
        enum class Status : std::uint32_t { good = 0,
                                            failed = 1,
                                            incomplete = 100,
                                            corrupt = 200,
                                            audited = 500,
                                            indeterminate = 0xFFFFFFF
                                          }; // 정의와 함께 지정할 수도 있다.
    • 즉, 기저 타입을 지정하면서 전방 선언하면 된다.

 

 

 

범위 없는 enum이 유용한 상황
  • C++11의 std::tuple 안에 있는 필드들을 지칭할 때.
using UserInfo = std::tuple<std::string, // 사용자 이름
                            std::string, // 이메일 주소
                            std::size_t>; // 평판치
                            
UserInfo uInfo;
...

auto val = std::get<1>(uInfo); // 해당 필드가 무엇인지 직관적이지 않다.

// 하지만 범위 없는 enum을 사용하면 직관적으로 사용할 수 있다.

enum UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
...

auto val = std::get<uiEmail>(uInfo); // 직관적이다.

// 범위 있는 enum은 너무 장황해진다.

enum class UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
...

auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);
  • enum class를 사용하고 장황함을 줄여보고자, std::size_t를 돌려주는 함수를 작성한다?
    • std::get에서 받는 것은 템플릿 인수이다.(꺽쇠 안에 들어가는 인수)
    • 즉, 열거자를 std::size_t로 변환하는 함수는 그 결과를 컴파일 중에 산출해야 한다.
    • 그를 위해선 그 함수는 반드시 constexpr 함수여야 한다.(항목 15 참조)
    • 또한 그 함수는 어떤 종류의 enum에서도 동작해야 하므로 함수 템플릿이어야 한다.
      • 그렇다면 std::size_t를 리턴할 것이 아니라 enum의 기저 타입을 리턴해야 한다.
      • std::underlying_type 특성 정보(trait)를 사용하면 해결할 수 있다.(항목 9 참조)
    • 그리고 그 함수는 noexcept(항목 14 참조)로 선언해야 한다.
      • 결코 예외를 던지지 않을 것이기 때문이다.
// C++11 버전
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 버전
template<typename E>
constexpr std::underlying_type_t<E>::type toUType(E enumerator) noexcept
{
    return static_cast<typename std::underlying_type<E>::type>(enumerator);
}

// C++14의 auto를 사용하는 버전
template<typename E>
constexpr auto toUType(E enumerator) noexcept
{
    return static_cast<typename std::underlying_type<E>::type>(enumerator);
}

// 이제 아래처럼 사용할 수 있다.

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
  • 그래도 안정성이 확보되는 enum class 쪽을 선호하는 편이 낫지 않을까?
728x90
Comments