일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 오블완
- exception
- virtual function
- universal reference
- implicit conversion
- more effective c++
- Effective c++
- 게임
- UE4
- effective stl
- lua
- 상속
- 언리얼
- reference
- 반복자
- 보편 참조
- effective modern c++
- 스마트 포인터
- 예외
- 티스토리챌린지
- operator new
- resource management class
- std::async
- 영화
- c++
- Smart Pointer
- 영화 리뷰
- iterator
- 참조자
- 암시적 변환
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 10. enum class 본문
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의 기저 타입은 컴파일러가 언제라도 알 수 있다.
범위 없는 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
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 12. override 키워드와 참조 한정사(reference qualifier) (0) | 2025.02.20 |
---|---|
[Effective Modern C++] 11. 삭제된 함수(deleted function) (0) | 2025.02.19 |
[Effective Modern C++] 9. typedef vs using (0) | 2025.02.17 |
[Effective Modern C++] 8. nullptr (0) | 2025.02.14 |
[Effective Modern C++] 7. 괄호 초기화 vs 중괄호 초기화 (0) | 2025.02.13 |
Comments