Post List

2015년 3월 30일 월요일

item 10: scoped enum을 사용하자.

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점








Effective Modern C++ - Scott Meyers
Item 10: Prefer  scoped enums to unscoped enus.

Item 10. Scoped enum을 사용하자.

scoped enum은 머가 scoped 라는 말인지...
unscoped enum은 머가 unscoped 라는 말인지, 그것부터 알아 보도록 하죠.

C++ 에서 중괄호 { } 안에 선언된 지역변수는 그 외부에서는 사용이 불가능합니다.
즉, 범위(Scope)를 가지고 있다고 이야기 합니다.

C++98 Stlye의 enum은 이 scope가 적용되지 않는, 'unscoped enum' 이라고 합니다.

enum Color { BLACK, WHITE, RED };
auto WHITE = false; // error : 'WHITE' redefinition

C++11 에서는 'scoped enum'이 가능해졌습니다.

enum class Color { BLACK, WHITE, RED };

auto WHITE = false; // OK

Color C1 = BLACK; // error : 'BLACK' is undefined
Color C2 = Color::WHITE;

int I1 = Color::RED; // error : 'Color' can't be initialize an entity of 'int'
int I2 = static_cast<int>(Color::RED); // OK

enum class라고 표기하기 때문에 열거형 클래스라고 말하기도 합니다.
Color 안에서 정의한 내용은 Color의 scope 안에서만 사용이 가능합니다. ( Color:: )
그러므로, 범위 밖에서 같은 이름으로 선언도 가능합니다.
대신 사용할 때 반드시 scope 연산자 ( :: )를 붙여줘야만 하며,
다른 타입으로의 암묵적 형변환이 이루어지지 않아서 반드시 static_cast<int>등을 이용하여 casting 해줘야 합니다.

이게 새로 생겼단건 알겠는데, 뭐가 더 좋길래 쓰라고 할까요 ?

2가지는 이미 위에서 나와있습니다.

1. namespace 를 지켜주므로, 보다 직관적으로 변수선언등이 가능합니다.

2. 암묵적 형변환을 막아줍니다.

이게 왜 장점이냐구요 ?
정수형 타입으로의 형변환은 나름 편리하기도 하겠지만,
실수형 타입으로도 형변환이 이루어집니다.
double과 비교하고, 해당 변수에 대입하는 등의 행위는 문제가 될 수 있습니다.

3. 전방 선언(forward-declaration)이 가능해 집니다.

class의 전방선언은 모두들 많이 사용하셔서 아실꺼라 생각합니다.
enum class도 class이므로 전방선언이 가능합니다.

enum class Color;

C++11 에서는 unscoped enum 도 전방선언이 가능합니다만, 단 타입명을 미리 지정해줘야 합니다.

enum Status : std::uint32_t;

unscoped enum의 경우 컴파일러가 내부 요소들을 보고 최소한의 정수형 타입으로 결정합니다.

enum Color { BLACK, WHITE, RED };

위의 경우는 요소가 3개라서 Color의 내부 타입 (underlying type)으로 char가 됩니다.
그럼 다음과 같은 경우는 어떨까요 ?

enum Status
{
    SUCCESS = 0,
    FAIL = 1,
    INCOMPLETE = 100,
    CORRUPT = 200,
    UNDEFINED = 0xFFFFFFFF
};

필요로하는 Data의 범위가 0 ~ 0xFFFFFFFF 까지이므로 내부 타입으로 int가 되어야 합니다.
컴파일러는 메모리의 효율적 사용을 위하여 가장 효율적인 범위 내의 타입을 사용하기도 하고,
또는 빠른 처리 속도를 위하여 보다 큰 크기의 타입을 사용하기도 합니다.
unscoped enum의 경우  C++98에서는 이 모든 처리를 컴파일러가 알아서 했으므로 전방선언이 불가능했으며,
C++11에서는 내부 타입을 지정해주면 전방선언이 가능했습니다.

전방선언을 사용하기 위해서는 내부 타입의 크기를 알아야하는데, 그럼 scoped enum은 아직 요소들을 정의하지 않았는데도 어떻게 내부 타입의 크기를 알아서 정의할까요 ?
그냥 따로 지정하지 않는 이상 default는 int로 처리합니다.

그럼 이때까지 왜 전방선언이 되었냐에 대해서 알아보았구요.
전방선언을 했을 때의 장점은 무엇이 있을까요 ?
그냥 Code를 깔끔하게 정리할 수 있다는건 일단 생략하겠습니다.

enum Status : std::uint32_t;
{
    SUCCESS = 0,
    FAIL = 1,
    INCOMPLETE = 100,
    CORRUPT = 200,
    AUDITED = 200,
    UNDEFINED = 0xFFFFFFFF
};

특정 Code 한곳에서만 사용하는 요소를 하나 추가했을 경우,
전방 선언이 안되는 경우라면 해당 enum을 사용하는 모든 Code를 새로 빌드해야하지만,
전방 선언을 하고 내부 요소는 각 Code별로 따로 정의가 가능해지므로,
사용하는 곳 거기만 빌드해서 해결이 가능합니다.

그렇다고 모든 경우에 대해서 다 scoped enum이 편할까요 ?
enum 값을 배열이나 std::tuple 의 인덱스로 사용하는 경우는 unscoped enum이 더 편리합니다.

// name, address, score
using UserInfo = std::tuple<std::string, std::string, std::size_t>;

UserInfo UI;

auto sName = std::get<0>(UI);

enum UserInfoFields { EM_NAME, EM_ADDR, EM_SCORE };

auto sAddr = std::get<EM_ADDR>(UI);

enum class UserInfoFields_ { EM_NAME, EM_ADDR, EM_SCORE };

auto nScore = std::get<static_cast<std::size_t>(UserInfoFields_::EM_SCORE)>(UI);

위 예제를 보면 std::tuple로 선언된 타입의 3가지 요소를 각각 다른 방법으로 가져오고 있습니다.
첫번째 name 값은 숫자를 그대로 인덱스로 넣어주었습니다. 흔히 사용하는 방법입니다.
두번째 address 값은 unscoped enum을 이용하여 가져오고 있습니다.
숫자를 바로 쓰는것보다는 훨씬 더 직관적입니다.
세번째 score 값은 scoped enum으로 가져오고 있습니다.
이또한 직관적이........... 기 전에 아놔 ;; 너무 복잡합니다.

그래도 굳이 scoped enum을 쓰고 싶다는 분이 계실껍니다.
세상에는 많고 많은 사람이 있고... 회사마다 또라X 불변의 법칙이라고... 에헴 ;;;
그럴땐 enum을 입력받아 std::size_t타입을 반환하는 함수를 만들면 됩니다.
그럴려면 생각할께 좀 있습니다.
std::get는 template 함수이므로... 여기에 들어가는 인자도 컴파일 타임에는 정해져야 합니다.
컴파일 타임에 미리 계산을 해서 답을 내는 방법으로는 constexpr을 이용하는 방법이 있습니다.
그런데 앞에도 얘기했지만 우리에게 필요한건 std::size_t 값이 아니라 std::size_t타입이 필요합니다.
타입을 반환하는게 되나요 ? 이게 Java나 C#도 아니고...

std::nderlying_type를 이용하는 방법이 있다. Scott 형님께서 얘기 하십니다.
#include <type_traits>를 해야 합니다.)

해당 함수를 한번 살펴 볼까요 ? (솔직히 이해는 잘.... )

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용 코드입니다. 그런데 바로 앞에 아이템에서 배웠죠 ?
typename std::underlying_type<E>::type 이렇게 쓰는것 보다는 더 간단하게 using으로 구현한걸 쓰면 읽기 편해집니다.

template <typename E>
constexpr std::underlying_type_t<E> toUType(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

좀 더 간결해진듯 하네요.
코드가 몇줄 되지도 않는데, 같은 타입을 반복해서 사용했네요.
auto는 요롤때 써야 제맛이죠.

template <typename E>
constexpr auto toUType(E enumerator) noexcept
{
  return static_cast<std::underlying_type_t<E>>(enumerator);
}

훨씬 더 간결해 졌습니다.
하지만 중요한건 이 함수를 간결하게 하는게 아니죠 ?
원래 사용하던 scoped enum을 사용하던 코드가 다음과 같이 됩니다.

auto nScore = std::get<toUType(UserInfoFields_::EM_SCORE)>(UI);

그래도 scoped enum의 UserInfoFields_::는 어쩔 수 없습니다.
그냥 unscoped enum이 훨씬 편한것 같습니다.

Things to Remember

* C++98 Style의 enum을 unscoped enum이라고 한다.

* scoped enum안에 정의한 값을 scope 밖에서 사용하려면 scope 연산자 ( :: ) 가 필요하며,
  다른 타입으로 사용하려면 형변환이 필요하다.

* scoped enum과 unscoped enum의 내부 타입 (underlying type) 지정이 가능하다.
  scoped enum의 내부 타입의 default는 int이고, unscoped enum은 기본 지정이 없다.

* scoped enum은 항상 전방 선언이 가능하며, unscoped enum은 내부 타입이 지정되어야만 가능하다.

댓글 없음:

댓글 쓰기