일반적으로 중괄호 쌍 안에서 선언된 이름은 해당 중괄호의 범위 안에서만 유효하지만 C++98 스타일의 enum은 예외적으로 규칙이 적용되지 않는다.
enum Color { black, white, red };
auto white = false; // error
위와같은 열거자들은 범위를 새어 나가기 때문에 범위 없는(unscoped) enum라고도 한다.
enum class Color { black, white, red };
auto white = false; // ok
Color c = white; // error
Color c = Color::white; // ok
C++11부터는 열거자들이 범위를 새어나가게 하지 않기 위해 범위 있는(scoped) enum을 추가하였다. enum class라고도 한다.
범위 있는 enum은 열거자들이 범위 밖으로 새어나가지 않기 때문에 이름 공간을 더럽히지 않는것과 더불어서 암시적 타입 변환에 의한 문제도 방지해준다.
범위 없는 enum은 암시적으로 정수 타입으로 변환되는 반면 범위 있는 enum은 암시적 타입 변환이 이루어지지 않기 때문이다.
enum Color { black, white, red };
Color c = red;
if (c < 14.5) { ... } // Color->int->double 암시적 변환
enum class Color { black, white, red };
Color c = Color::red;
if (c < 14.5) { ... } // 컴파일 에러
만약 범위 있는 enum의 타입 변환이 필요하다면 직접 캐스팅을 통해 타입 변환을 해주면 된다.
또한 범위 있는 enum은 전방 선언도 가능하다.
그런데 전방 선언이 왜 장점일까? 그것은 컴파일 의존 관계와 관련이 있다.
enum Status {
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
만약 이런 enum이 존재하고 시스템 전반적으로 사용된다고 가정해보자. 여기서 새 열거자가 추가되는 등의 수정 작업이 이루어지면 시스템 전체가 다시 컴파일되는 극심한 낭비가 발생하게 된다.
enum class Status;
...
void continueProcessing(Status s);
하지만 전방 선언을 사용하면 의존 관계가 크게 줄어들기 때문에 컴파일 타임이 크게 줄어들 수 있다.
그런데 전방 선언은 해당 타입의 크기를 알아야만 사용할 수 있는 문법이다. 컴파일러는 enum의 타입을 어떻게 알 수 있을까?
범위 있는 enum은 기본 타입이 int이기 때문에 별도의 타입을 지정하지 않아도 컴파일러가 타입을 알 수 있다. 하지만 범위 없는 enum은 기본 타입이 없기 때문에 전방 선언이 안된다. 그렇지만 타입을 임의로 지정해주면 범위 없는 enum도 전방 선언이 가능해진다.
enum Status; // 기본 타입 int
enum class Status; // 기본 타입 없음. 전방 선언 X
enum Status : std::uint32_t; // 사용자 정의 타입 uint32_t
enum class Status : std::uint32_t; // 사용자 정의 타입 uint32_t
범위 있는 enum이 범위 없는 enum보다 더 좋아보이기 때문에 범위 없는 enum은 사용할 필요가 아예 없을것처럼 보인다. 하지만 사용할만한 곳이 적어도 하나는 존재한다.
using UserInfo = std::tuple<std::string, std::string, std::size_t>;
UserInfo uInfo;
...
auto val = std::get<1>(uInfo); // 1의 필드는 대체 무슨 뜻?
바로 튜플을 사용할 때이다. 튜플은 각 필드를 std::get을 통해 접근하게 된다. 하지만 리터럴로 접근하면 코드를 읽는 다른 사람이 무슨 뜻인지 바로 알아볼수가 없다.
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
...
auto val = std::get<uiEmail>(uInfo); // email 필드의 값이구나!
범위 없는 enum의 열거자들은 정수 타입으로 암시적 변환되며 각 필드를 접근하게 된다. 코드를 읽는 사람도 무슨 필드인지 명확하게 알 수 있기 때문에 가독성이 좋아진다.
하지만 범위 있는 enum을 사용한다면 반드시 캐스팅을 통한 타입 변환이 이루어져야 하기 때문에 템플릿 인수의 길이가 매우 길어질 수 있다.
범위 없는 enum에 의한 이름 공간 오염이 걱정된다면 열거자 하나를 받아서 std::size_t값을 돌려주는 함수를 직접 작성하면 된다. 그렇지만 꽤 까다로울 것이다.
std::get은 함수 템플릿이라서 컴파일 타임에 평가되기 때문에 함수의 반환값 역시 컴파일 타임에 산출되어야만 한다.
그러려면 해당 함수는 반드시 constexpr이어야 하고 예외를 던지지 않을 것을 알기 때문에 noexcept로 선언되어야 한다. 게다가 모든 종류의 enum에 대해서도 작동해야 하기 때문에 함수 템플릿으로 작성되어야 한다. 함수 템플릿을 고려한다면 반환 타입도 일반화 되어야 하기 때문에 이제는 std::size_t가 아니라 매개변수로 받은 열거자의 기본 타입을 반환시켜주어야 하므로 타입 특성을 이용해야 한다.
즉 1) constexpr, 2) noexcept, 3) 함수 템플릿, 4) type_traits 네가지를 고려해서 작성해야 한다.
template<typename E> // C++11
constexpr typename std::underlying_type<E>::type toUType(E enumerator) noexcept
{
return static_cast<typename std::underlying_type<E>::type>(enumerator);
}
template<typename E> // C++14
constexpr auto toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
enum의 기본 타입을 알아내는 것은 underlying_type을 사용하면 된다.
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
...
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
해당 함수 템플릿을 사용하면 조금 더 번거롭고 코드가 살짝 길어졌지만 범위 없는 enum을 사용했을 때처럼 리터럴 값을 사용하지 않아도 된다. 게다가 범위 있는 enum이기 때문에 이름 공간의 오염과 의도하지 않은 암시적 타입 변환을 걱정할 필요가 없다.
◾ C++98 스타일의 enum을 이제는 범위 없는 enum이라고 부른다.
◾ 범위 있는 enum의 열거자들은 그 안에서만 보인다. 이 열거자들은 오직 캐스팅을 통해서만 다른 타입으로 변환된다.
◾ 범위 있는 enum과 범위 없는 enum 모두 기본 타입 지정을 지원한다. 범위 있는 enum의 기본 타입은 int이다. 범위 없는 enum에는 기본 타입이 없다.
◾ 범위 있는 enum은 항상 전방 선언이 가능하다. 범위 없는 enum은 해당 선언에 기본 타입을 지정하는 경우에만 전방 선언이 가능하다.
'도서 > Effective Modern C++' 카테고리의 다른 글
[3장] 항목 12 : 재정의 함수들을 override로 선언하라 (0) | 2023.01.17 |
---|---|
[3장] 항목 11 : 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하라 (0) | 2022.12.29 |
[3장] 항목 9 : typedef보다 별칭 선언을 선호하라 (0) | 2022.12.28 |
[3장] 항목 8 : NULL보다 nullptr을 선호하라 (0) | 2022.12.28 |
[3장] 항목 7 : 객체 생성 시 괄호와 중괄호를 구분하라 (0) | 2022.12.27 |