[Modern C++] 항목 10: 범위 없는 enum보다 범위 있는 enum을 선호하라

게시:     수정

카테고리:

태그: ,

이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역

가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩

📦 3. 현대적 C++에 적응하기

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

🔍 열거자 범위

// 범위 없는 enum
enum Color { black, white, red };
auto white = false; // ⚠️ 에러!
  • enum에 있는 열거자들이 범위 밖으로 새어나간다.
    • 범위 없는 enum이라고 부르며, C++98 스타일이다.
// 범위 있는 enum
enum class Color { black, white, red };
auto white = false;
Color c = white; // ⚠️ 에러!
Color c = Color::white; // ✅
  • enum에 있는 열거자들이 현재 중괄호 범위 내로 한정된다.
    • 범위 있는 enum 혹은 enum 클래스라고 부르며, C++11에서 추가되었다.
    • 범위 없는 enum도 여전히 C++11에 존재한다.
    • namespace 오염을 줄여준다.

🔄 암묵적 변환

// 범위 없는 enum
enum Color { black, white, red };
vector<size_t> primeFactors(size_t x);

Color c = red;
...

if(c < 14.5) { // ⚠️ Color를 double과 비교
	auto factors = primeFactors(c); // ⚠️ Color의 소인수 계산
	...
}
  • 범위 없는 enum의 열거자들은 암묵적으로:
    • 열거자 타입정수 타입으로 변환
    • 정수 타입부동소수점 타입으로 변환
  • 이로 인해, 의도치 않은 동작이 발생할 수 있다.
// 범위 있는 enum
enum class Color { black, white, red };
vector<size_t> primeFactors(size_t x);

Color c = Color::red;
...

if(c < 14.5) { // ⚠️ 에러! Color와 double 비교!
	auto factors = primeFactors(c); // ⚠️ 에러! Color를 size_t로 암묵적 변환 불가!
	...
}

if(static_cast<double>(c) < 14.5) {
	auto factors = primeFactors(static_cast<size_t>(c));
  • 범위 있는 enum암묵적 변환이 되지 않는다.
    • 변환하고 싶다면, 캐스팅을 사용한다.

📋 전방 선언

// 범위 없는 enum
enum Color; // ⚠️ 에러! 전방 선언 실패!

// 범위 있는 enum
enum class Color; // ✅ 전방 선언
  • 범위 없는 enum은 추가 작업 없이, 전방 선언이 불가능하다.
  • enum이 사용되기 전, 컴파일러가 enum의 바탕 형식을 알고 있어야 하기 때문이다.
  • 바탕 형식(underlying type): 열거형 요소들이 메모리에 저장될 때 실제 크기와 타입을 결정하는 기반 정수형
// C++98
enum Color { black, white, red }; // 바탕 형식: char

enum Status {
	good = 0,
	failed = 1,
	incomplete = 100,
	corrupt = 200,
	indeterminate = 0xFFFFFFFF
}; // 바탕 형식: int 이상
  • 범위 없는 enum에서 enum 정의는 괜찮다.
    • 컴파일러가 enum의 바탕 형식을 선택할 수 있기 때문이다.
  • 컴파일러는 메모리 효율을 위해 허용되는 가장 작은 바탕 형식을 선택한다.
    • 속도 최적화 시, 보다 큰 바탕 형식을 선택할 수도 있다.

전방 선언을 못하는 경우 생기는 단점:

// 범위 없는 enum
enum Status {
	good = 0,
	failed = 1,
	incomplete = 100,
	corrupt = 200,
	audited = 500, // 추가된 열거자
	indeterminate = 0xFFFFFFFF
};

void continueProcessing(Status s);

// 범위 있는 enum
enum class Status; // 전방 선언
void continueProcessing(Status s);
  • 전방 선언을 못하면: 컴파일 의존 관계가 늘어나고, 열거형이 변경되면 시스템 전체를 다시 컴파일해야 하는 상황이 발생한다.
  • 전방 선언을 하면: Status 열거형의 정의가 바뀌어도, 해당 선언을 담은 헤더는 다시 컴파일할 필요가 없다.

C++11 바탕 형식 지원:

// 범위 있는 enum의 기본 바탕 형식은 int
enum class Status;

// 바탕 형식 지정 가능
enum class Status : uint32_t;

// 범위 있는 enum을 정의할 때도 바탕 형식 지정 가능
enum class Status : uint32_t {
	good = 0,
	failed = 1,
	incomplete = 100,
	corrupt = 200,
	audited = 500,
	indeterminate = 0xFFFFFFFF
};

// 범위 없는 enum에 바탕 형식 지정 시 전방 선언 가능
enum Color : uint8_t;
  • 범위 있는 enum이 전방 선언이 가능한 이유는, 바탕 형식이 지정되어 있기 때문이다.
  • 범위 없는 enum도 바탕 형식을 지정해주면, 전방 선언이 가능해진다.

✅ 범위 없는 enum이 유용한 경우

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

UserInfo uInfo;
auto val = std::get<1>(uInfo); // 1번 필드가 무엇인지 바로 알 수 없음
// 범위 없는 enum 사용
enum UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
// uiEmail → size_t로의 암묵적 변환 발생
auto val = std::get<uiEmail>(uInfo);
  • 범위 없는 enum을 사용하면, 해당 필드가 무엇을 뜻하는지 바로 알 수 있다.
// 범위 있는 enum 사용
enum class UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
auto val =
	std::get
		static_cast<size_t>(UserInfoFields::uiEmail)
	>(uInfo);
  • 범위 있는 enum에서 비슷하게 하려면 복잡해진다.
  • static_cast<size_t>(UserInfoFields::uiEmail)를 줄이고 싶다면, 열거자size_t를 돌려주는 함수 toUType을 작성해야 한다.

toUType 함수 조건:

  • std::get<..>(..)은 템플릿 함수이기에, 컴파일 도중 산출해야 한다. → constexpr 함수여야 한다.
  • 반환 값으로 enum의 바탕 형식을 반환해야 한다. → 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: underlying_type_t 사용 (항목 9 참고)
template<typename E>
constexpr std::underlying_type_t<E>
	toUType(E enumerator) noexcept {
	return static_cast
		std::underlying_type_t<E>
	>(enumerator);
}
// C++14: auto 반환 타입 사용 (항목 3 참고)
template<typename E>
constexpr auto
	toUType(E enumerator) noexcept {
	return static_cast
		std::underlying_type_t<E>
	>(enumerator);
}

auto val = std::get
	toUType(UserInfoFields::uiEmail)
>(uInfo);

🧐 정리

  • C++98 스타일의 enum을 범위 없는 enum이라 부른다.
  • 범위 있는 enum의 열거자들은 현재 중괄호 범위 내에서만 유효하다.
    • 암묵적 변환은 허용되지 않으며, 캐스팅을 통해서만 다른 형식으로 변환된다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기