C++0x 미리보기 4, 새로운 열거형

Twitter icon류광, 2008-01-31 17:01
C의 유물 중 하나인 enum의 여러 문제점들을 해결하기 위한 "enum class"에 대한 제안입니다.

(이 글은 N2347를 기초로 합니다. 항상 그렇듯이, 특별한 언급이 없는 한 코드 예제들은 해당 제안 문서에서 뽑아온 것입니다.)

C++의 여러 약점들과 마찬가지로, C++의 열거형(enum)은 C 시절의 약점을 그대로 가지고 있습니다. N2347이 지적하는 enum의 문제점은 크게 세 가지입니다.

이 외에 이런 문제를 해결하기 위한 컴파일러 확장들이 중구난방이라는 점도 문제점으로 지적하고 있습니다만, 그 점은 표준화의 필요성을 이야기하기 위한 것일 뿐이므로 여기서 자세히 이야기할 필요는 없을 것 같습니다.

그럼, 우선 int로의 암묵적 변환이 어떤 문제인지 봅시다:

enum Color { ClrRed, ClrOrange, ClrYellow, 
    ClrGreen, ClrBlue, ClrViolet };

enum Alert { CndGreen, CndYellow, CndRed };

Color c = ClrRed;
Alert a = CndGreen;
a = c; // (1) 오류
a = ClrYellow; // (2) 오류

bool armWeapons = 
    ( a >= ClrYellow ); // (3) 오류가 아님!

ColorAlert는 다른 형식이므로 (1)과 (2)가 오류인 것은 당연합니다. 그런데 이상하게도 (3)은 오류가 아닙니다. >= 때문에 aClrYellow가 암묵적으로 int 값으로 변환이 되었기 때문입니다. 이는 간단히 말하면 형식 시스템의 “구멍”입니다. 강한 형식 언어가 되려면 (3)도 오류가 되어야 합니다.

다음으로 바탕 형식을 명시하지 못하는 문제를 봅시다. 여기서 바탕 형식(underlying type)이란 열거형 상수가 프로그램 안에서 실제로 저장되는 정수 형식을 말합니다. 표준에 따르면 열거형의 바탕 형식은 구현(즉 컴파일러)이 결정합니다. 열거형에서 가장 큰 상수를 담기에 충분한 크기의 정수 형식이기만 하면 어떤 것이라도 상관이 없습니다.

컴파일러가 알아서 해 주는 게 뭐가 문제겠냐고 생각할 수도 있지만, 열거형 멤버를 가진 구조체의 크기가 컴파일러마다 다를 수 있으면 이식성이 깨집니다. 특히 객체의 정확한 크기가 중요한 파일 I/O나 네트워크 관련 코드에서는 큰 문제입니다. 예를 들어 다음의 경우 Packet 객체의 크기는 컴파일러가 Version의 바탕 형식을 무엇으로 선택하느냐에 따라 달라져 버립니다.

enum Version { Ver1 = 1, Ver2 = 2 };

struct Packet {
  Version ver; // 구현에 따라 크기가 다를 수 있음
  // ... 그 외의 자료 멤버들 ...
  Version getVersion() const { return ver; }
};

바탕 형식의 크기뿐만 아니라 부호 여부도 중요합니다. 예를 들어

enum E { E1 = 1, E2 = 2, Ebig = 0xFFFFFFF0U };

에서 Ebig의 값은 무엇일까요? E의 바탕 형식이 signed라면 -16, unsigned라면 4294967280입니다. 표준은 바탕 형식의 부호 여부 역시 컴파일러가 선택하도록 되어 있습니다. N2347의 2.2.2절에 다양한 컴파일러들에서의 해당 값이 나와 있는데, 예상대로 컴파일러 제조사마다 결과가 다양합니다. 심지어 MS의 컴파일러들은 버전 6.0과 7.1이 다르고 7.1과 8.0(alpha)이 다릅니다. if (a > Ebig) ... 같은 아주 간단한 문장이 컴파일러에 따라 전혀 반대의 의미를 가질 수 있는 것입니다.

마지막으로 이상한 범위 적용 문제를 봅시다. 이번에는 N2347의 예 대신 실제 프로그래머가 겪은 예를 들겠습니다. 다음은 GpgStudy 포럼의 중복된 enum 을 namespace 로 개선하기에서 가져온 것입니다.

enum eJustifyHType
{
    TOP,
    CENTER,
    BOTTOM
};

enum eJustifyVType
{
    LEFT,
    CENTER, // 여기서 오류가 남
    RIGHT
};

이런 문제가 생기는 것은 enum에 정의된 이름들이 이상하게도 enum 자체의 범위 바깥에 들어가기 때문입니다. 위의 eJustifyHTypeeJustifyVType가 전역 이름공간에 정의되었다고 할 때, TOP, CENTER 등은 모두 전역 범위의 이름들이 되어 버리며, 그러다보니 한 범위에 CENTER가 두 번 정의되어서 오류가 생긴 것입니다. { ... } 범위를 사용하는 다른 구조들(이름공간, 클래스, 함수, 함수 안의 지역 범위)은 모두 해당 이름이 해당 범위 안에 존재하는데 유독 이넘(^^)은 그렇지 않습니다. 물론 이렇게 된 이유는 C와의 호환성 때문이고요.

이상의 세 가지 문제 모두 enum 자체로는 해결할 수 없습니다. 가장 그럴듯한 대안은 enum을 포기하고 enum과 비슷하게 행동하되 문제점들은 모두 해결된 형태의 사용자 정의 클래스를 만드는 것입니다. 그러나 그러한 클래스를 만드는 것은 번거로운 일이고, 사실 열거형처럼 언어에서 제대로 지원을 해야 마땅한 기본적인 표현 수단을 사용자 코드에 맡길 수는 없는 일입니다.

이러한 논점에 근거해서 제안자들은 새로운 종류의 열거형인 enum class를 제안했습니다. enum class는 위의 세 가지 문제를 모두 해결합니다. enum 대신 enum class로 정의된 열거형 값들은 암묵적으로 int로 변환되지 않으며, 이름공간이나 클래스와 같은 방식으로 범위가 적용됩니다. 또한 클래스의 상속 구문과 비슷한 방식으로 바탕 형식을 지정할 수 있습니다.

enum class E : unsigned long  // (1) 바탕 형식 지정
    { E1 = 1, E2 = 2, Ebig = 0xFFFFFFF0U };

E e1 = E1; // (2) 컴파일 오류 
E e2 = E::E2; // OK

void f( E e ) {

    if( e >= 100 ) //(3) 컴파일 오류
       ...
}

위의 코드에서 (1)은 열거형의 바탕 형식을 명시적으로 지정하는 방법을 보여줍니다. 지정하지 않은 경우에는 enum과 마찬가지 방식으로 컴파일러가 바탕 형식을 결정합니다. 바탕 형식으로는 wchar_t를 제외한 임의의 정수 형식을 지정할 수 있습니다. (2)는 enum class가 상식적인 범위 적용 방식을 따른다는 점을 보여주고요. (3)은 int로의 암묵적 변환이 일어나지 않음을 보여줍니다.

한편, 제안자들은 기존 코드를 망가뜨리지 않고도 enum 자체의 규칙을 바꿀 수 있는 부분을 지적하고 개선안을 제시했습니다. 두 가지인데, 하나는 enum class처럼 콜론(:)으로 바탕 형식을 지정할 수 있게 하는 것이고 또 하나는 상식적인 범위 한정을 사용할 수 있게 하자는 것입니다. 예를 들면:

enum E : unsigned long // enum 자체에 바탕 형식 지정
    { E1 = 1, E2 = 2, Ebig = 0xFFFFFFF0U };

E e1 = E1; // OK
E e2 = E::E2; // 이것도 OK

이상이 제가 나름대로 파악한 이 제안의 핵심입니다. 표준 문구 수정안을 비롯한 좀 더 자세한 내용은 N2347을 참고하시길~

태그: 프로그래밍 C++ C++0x

comments powered by Disqus

예전 댓글(읽기 전용)