KoreanFoodie's Study
Effective Modern C++ | 항목 10 : 범위 없는 enum 보다 범위 있는 enum 을 선호하라 본문
Effective Modern C++ | 항목 10 : 범위 없는 enum 보다 범위 있는 enum 을 선호하라
GoldGiver 2022. 10. 26. 09:52
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 10 : 범위 없는 enum 보다 범위 있는 enum 을 선호하라
핵심 :
1. C++98 스타일의 enum 을 이제는 범위 없는 enum 이라고 부른다.
2. 범위 있는 enum 의 열거자들은 그 안에서만 보인다. 이 열거자들은 오직 캐스팅을 통해서만 다른 형식으로 변환된다.
3. 범위 있는 enum 과 범위 없는 enum 모두 바탕 형식 지정을 지원한다. 범위 있는 enum 의 기본 바탕 형식은 int 이다. 범위 없는 enum 에는 기본 바탕 형식이 없다.
4. 범위 있는 enum 은 항상 전방 선언이 가능하다. 범위 없는 enum 은 해당 선언에 바탕 형식을 지정하는 경우에만 전방 선언이 가능하다.
범위 없는(unscoped) enum 의 경우, enum 내부에서 선언된 변수는 자신을 정의하는 enum 의 범위로 새어 나간다. 예시를 보자.
enum Color { black, white, red };
// 오류! 이 범위에 이미 white 가 선언되어 있음
auto white = false;
이에 대응되는 C++11 의 새로운 열거형인 범위 있는(scoped) enum 에서는 그러한 이름 누수가 발생하지 않는다.
enum class Color { black, white, red };
// 이 범위에 다른 "white" 는 없음
auto white = false;
// 오류! 이 범위에 "white" 라는 이름의 열거자는 없음
Color c = white;
Color c = Color::white;
auto c = Color::white;
"enum class" 라는 구문으로 선언한다는 점 때문에, 범위 있는 enum 을 enum 클래스라고 부르기도 한다.
enum class 는 범위를 오염시키지 않는 장점 뿐만 아니라, 범위 없는 enum 의 열거자들이 암묵적으로 정수 형식으로 변환된다는, 잠재적인 재앙을 방지한다. 예시를 보자.
/* 범위 없는 enum 을 쓰는 경우 */
enum Color { black, white, red };
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
// ?? Color 를 double 과 비교!
if (c < 14.5) {
// ??? Color 의 소인수들을 계산!
auto factors = primeFactors(c);
}
/* 범위 있는 enum 을 쓰는 경우 */
enum class Color { black, white, red };
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = Color::red;
// 에러! Color 와 double 을 비교할 수 없음
if (c < 14.5) {
// 에러! std::size_t 에 Color 를 전달할 수 없음
auto factors = primeFactors(c);
}
범위 없는 enum 의 경우, 다음과 고치면 정말로 이상하지만 컴파일은 된다! 다시금 강조하지만, 정말 이상하긴 하다.
/* 범위 없는 enum 을 쓰는 경우 */
enum Color { black, white, red };
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
// 컴파일은 된다, 컴파일은...
if (static_cast<double>(c) < 14.5) {
// 마찬가지로 컴파일은 된다...
auto factors = primeFactors(static_cast<std::size_t>(c));
}
추가적으로, enum class 의 경우 enum 과 다르게 전방 선언이 가능하다는 장점이 있다.
사실 C++11 에서는 범위 없는 enum 도 전방 선언이 가능하긴 하다.
enum Color { black, white, red };
위 코드는 표현해야 할 값이 3 개 뿐이므로 컴파일러는 바탕 형식(underlying type) 으로 char 을 선택해야 할 것이다. 하지만 다음과 같이 범위가 훨씬 큰 enum 은 어떨까?
enum Status {
good = 0;
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
enum 이 나타내는 값들의 범위는 0 에서 0xFFFFFFFF 까지이다. char 가 적어도 32 비트인 경우를 제외하면, Status 값을 표현하기 위해 컴파일러는 char 보다 큰 정수 형식을 선택해야 한다. 메모리의 효율적 사용을 위해, 컴파일러들은 enum 값들을 표현할 수 있는 가장 작은 바탕 형식을 선택하는 경향이 있다. 그러나 경우에 따라서는 컴파일러가 크기 대신 속도를 위한 최적화를 적용하며, 이를 위해 C++98 은 오직 enum 의 정의만 지원하고 선언은 허용하지 않는다. enum 이 실제로 쓰이기 전에 컴파일러가 enum 의 바탕 형식을 선택할 수 있기 때문이다.
그러나 enum 을 전방 선언할 수 없으면 몇 가지 단점이 생기는데, 가장 주목할 만한 것은 아마도 컴파일 의존 관계가 늘어난다는 것이다.
enum test {
a = 0;
// b = 0; 을 추가?
};
즉 전방 선언이 불가능할 경우, 위와 같이 변수 하나를 추가했을때 전체 시스템 중 해당 enum 을 의존하는 모든 파일을 다시 컴파일해야 한다. 하지만 전방 선언이 가능할 경우, 해당 enum 의 정의가 바뀌어도 이 선언들을 담을 헤더는 다시 컴파일 할 필요가 없고, 기존 구현에 영향을 미치지 않는다면 구현부도 역시 다시 컴파일할 필요가 없다.
바탕 형식은 다음과 같이 지정할 수 있다. 범위 있는 enum 의 바탕 형식은 기본적으로 int 이다. 또한, 바탕 형식이 지정된 범위 없는 enum 은 전방 선언을 지원한다!
// 바탕 형식은 int
enum class EStatus;
// 바탕 형식 새로 지정
enum class EStatus: std::uint32_t;
// 정의할 때도 지정 가능
enum class EStatus: std::uint32_t { a = 0 };
// 범위 없는 enum 바탕 형식 지정 (이제 전방 선언이 가능)
enum EStatus: std::uint8_t;
그런데 다음과 같은 경우에는 실제로 enum 이 유용할 때가 있다. 바로 C++11 의 std::tuple 안에 있는 필드들을 지칭할 때이다.
using UserInfo =
std::tuple<
std::string,
std::string,
std::size_t>;
enum UserInfoFields { uiName, uiEmail, uiReputation };
int main()
{
UserInfo uInfo;
// 필드 1 이 어떤 형식인지 알기 힘들다
auto val = std::get<1>(uInfo);
// 이메일 필드의 값을 얻음이 명확함
auto valWithEnum = std::get<uiEmail>(uInfo);
}
위의 경우, 범위 없는 enum 을 쓰면 어떤 필드의 값을 얻는지가 명확히 보이게 된다. enum class 를 사용하면 코드가 조금 복잡해진다.
enum class UserInfoFields2 { uiName, uiEmail, uiReputation };
...
// enum class 사용시 코드가 복잡해짐
auto val3 = std::get<static_cast<std::size_t>(UserInfoFields2::uiEmail)>(uInfo);
물론 열거자를 받아 std::size_t 를 돌려주는 함수를 작성할 수는 있다. 그런데 std::get 은 템플릿이며, 여기에 넘겨주는 것은 함수 인수가 아니라 템플릿 인수이다. 따라서 열거자를 std::size_t 가 아닌, '해당 enum 의 바탕 형식' 으로 반환해야 하며, 이 함수는 결과를 컴파일 도중에 산출해야 한다(constexpr 함수이어야 함). std::underlying_type 으로 형식 특질을 사용해야 하고, noexcept 로 선언해야 한다. 결코 예외를 던지지 않을 것을 알고 있기 때문이다. 코드는 다음과 같다.
// 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>
toUType2(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
// C++ 14 버전, auto 사용
template<typename E>
constexpr auto
toUType3(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
...
// 적용 예시
auto val4 = std::get<toUType(UserInfoFields2::uiEmail)>(uInfo);
위의 경우, 코드가 조금 길긴 하지만, 그래도 이름공간 오염을 피하고 열거자들과 관련된 의도치 않은 변환이 방지된다는 장점이 있다!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective Modern C++ | 항목 12 : 재정의 함수들을 override 로 선언하라 (0) | 2022.10.26 |
---|---|
Effective Modern C++ | 항목 11 : 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 9 : typedef 보다 별칭 선언을 선호하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 8 : 0 과 NULL 보다 nullptr 를 선호하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 7 : 객체 생성 시 괄호'( )' 와 중괄호'{ }' 를 구분하라 (0) | 2022.10.26 |