열거 유형 - 정량적 연구

2023. 2. 20. 19:26프로그래밍

728x90

 

 

Enumeration Types - A Quantitative Survey

A comparison of different techniques when using enumerations, their pro's and con's.

www.codeproject.com

 

 

열거형을 사용할 때 다양한 기술의 장단점을 비교.

 

요약

열거 유형은 사람이 읽기 쉬운 소스 코드를 작성하는 데 필수적인 요소입니다.

이것들의 이런 특별한 특성에 기인해서, 이런 유형 정의들을 어떻게 사용 할지 그리고 더 나아아가서 이러한 유형의 사용에 대한 명시적인 접근 방식을 결정 할 때는 특별한 주의가 반드시 필요 합니다.

이 두 가지 질문에 대한 답은 열거형 유형 구현물의 선택 이며, 즉 언어에서 제공한 열거형 유형 지원을 사용할지 혹은 여타 맞춤형 접근 방식을 사용할지 여부에 따라 결정됩니다.

이 문서에서는 열거형 형식을 구현에 대한 간단한 전처리기 구성에서 보다 정교한 클래스 기반 메서드에 이르기까지 다양한 메서드를 비교 해 봅니다.

이러한 생성자들과 그 의미론적 특성은 C++ 프로그래밍 언어의 컨텍스트 내에서 논의의 대상이 되지만, C# 또는 Java에서는 이들 중 대부분은 그렇게 많은 노력 없이 사용할 수 있습니다.

 

이 문서는 초보 C++ 프로그래머를 대상으로 하는 것이 아니며, 독자는 C++의 정적 클래스 멤버들과 정수 형의 문법과 일반적 객체 지향에 익숙하다고 가정 합니다.

 

I 소개

동기

소프트웨어 디자인과 그 결과물로서의 프로그래밍은 추상적인 개념이나 복잡한 구조를 인간의 눈으로 쉽게 이해할 수 있는 형태로 표현하는 것과 커다란 관심이 있습니다.이것이 소위 고급 프로그래밍 언어가 존재하는 주된 이유 중 하나입니다. 이러한 언어들은 일반적으로 구조화된 데이터 유형, 순환문 또는 클래스와 같은 고급 개념을 제공합니다. 이 들 중 좀 더 원시적인 구조 중 하나가 열거형 유형입니다.

이것의 사용은 일반적으로 int, char 등과 같은 정수 유형의 상수 값을 좀 더 직관적으로 이해 할 수 있는 식별자에 기호로 매핑하는 것입니다.

예를 들어 1이나 2 대신 일반적으로 foo resp, bar와 같이 쓰는 것이 더 나아 보입니다.

이와 비슷하게 가까운 위치에 서로 다른 의미로 쓰이는 동일한 상수값이 있을 때, 열거형 유형들은 이런 특정 상황의 의미를 만들어 주는 것에 도움이 됩니다.

 

대부분의 프로그래밍 언어는 정수 유형을 기반으로 하는 열거 유형에 대한 내장 지원을 제공합니다. 즉, 열거 유형 값은 컴파일러 내부적으로 정수 유형 값으로 표현된다는 것이죠 하지만 열거 유형의 개념을 클래스 또는 구조화된 데이터 유형으로 확장하는 것은 개발자의 몫입니다.

컴파일러에 의해서 암시적으로 유형 변환이 일어나는 것은 또 다른 문제들을 발생 시킵니다.

모든 것을 그렇게 만들어야 할 필요가 없는 것은 유형 안전성을 애쓰는 프로그래머의 노력에 족쇄를 채우거나 엄격한 문법적 규칙 요구를 준수 한다면 문제가 될 수 있습니다.

 

이 섹션의 나머지 부분에서는 열거형 유형의 적용에 대한 간단한 예를 설명 할 것입니다. 다음 섹션에서는 열거형 유형에 대한 원시 데이터 접근, 즉 전처리기 및 기본 제공 언어 지원과 "일반적인" 상수 정수 변수에 대한 비교에 중점을 둘 것입니다.

클래스로 확장할 수 있는 열거형에 대한 보다 복잡한 접근 방식은 세 번째 섹션에 소개 됩니다.

 

간단 예제 하나

그래픽 라이브러리 하나가 구현 됩니다. 요즘은 색상 지원 디스플레이가 일반적이므로, 이 라이브러리는 색상와 색상 조작을 지원할 것 입니다. 따라서 이 지원의 일부는 RGB 색상 모델의 범위 내에서 색상 값의 표현 입니다. 추가 요구 사항으로 흰색, 검정색, 빨간색, 녹색, 파란색, 자홍색, 노란색 및 청록색에 대한 바로 가기를 제공하는 것입니다.

 

RGB 색상 모델의 색상은 각각 빨간색, 녹색 및 파란색의 세 가지 기본 색상의 비율을 나타내는 세 가지 값으로 특정 됩니다. 이러한 값들은 0에서 255 사이의 모두 부호 없는 숫자로 모델링됩니다.

 

이 의미에 대한 구현은 다음과 비슷 할 것입니다:

class RGB {
public:
  unsigned short red;
  unsigned short green;
  unsigned short blue;
};
 

II 정수형 기반 열거형

이 섹션에서는 정수 유형을 기반으로 하는 열거형 유형에 대한 논의를 해 볼 것입니다.

여기에는 간단한 상수 값과 C++의 열거형과 같은 기본 언어 지원 내장 유형이 포함됩니다.

 

전처리기 구문

조잡하지만 그럼에도 불구하고 열거형 값을 정의하는 꽤 일반화 된 방법은 전처리기를 사용하는 것입니다(엄밀히 말하면 이 방법은 C++의 일부가 아닙니다).

각 열거 값에 대해 차례로 확장 될 전처리기 매크로를 정의 하는 것입니다.

 

예를 들어, RGB 인스턴스 배열에서:

RGB cgFundamentalColors[ 8 ] = {
    // insert appropriate initialization values
    };
 

다음과 같은 정의도 될 수 있죠:

#define WHITE 0
#define BLACK 1
#define RED 2
#define GREEN 3
#define BLUE 4
#define MAGENTA 5
#define YELLOW 6
#define CYAN 7
 

따라서 cgFundamentalColors[RED]로 빨간색에 해당하는 RGB 인스턴스를 접근합니다.

 

매크로 RED는 C++ 컴파일러 어디에서도 결코 나타나 있지 않다는 점에 주목해 보는 것이 중요 합니다.

대신, 전처리기는 이를 숫자 값 2로 바꿉니다. 즉, 컴파일러가 만드는 표현식은 cgFundamentalColors[2]가 되는 것입니다. 이 관찰의 간단한 결과는 그러한 매크로가 실제 열거형 값이나 유형이 아니라 이 것들 자체가 표기상의 편의일 뿐이라는 것입니다.

결과적으로 이러한 매크로에는 특별히 연관 되어있는 유형이 없습니다. 그것들은 일반 정수 리터럴과 동일하게 작동하며 해당 유형(예: long, int 또는 char)은 리터럴의 유형입니다.

이 말은 이러한 열거형의 값은 일반적으로 다른 열거형 혹은 어떤 다른 정수 값과 구분할 수 없다는 의미가 되는 것입니다.

특히 두 열거형의 멤버는 그 것들의 의미가 서로 전혀 관련이 없더라도 서로 비교 될 수 있습니다:

#define SOME_VALUE 1000
if ( RED == SOME_VALUE ) ...
 

예 그렇죠, 이 들은 또 서로 바꿔서 사용할 수 있겠죠 다음과 같이:

int someColor = RED;
someColor = SOME_VALUE;
 

리터럴 유형이 다음을 허용하는 경우라면:

#define SOME_CHAR '0'
if ( RED == SOME_CHAR ) ... // warning: condition is constant

char someChar = SOME_VALUE; // warning: truncation of constant value
 

하지만 컴파일러의 경고는 의도 한 문법에서 일어나는 차이를 나타 내는 것일 것입니다.이것이 맞는 경우 인지에 대한 것은 어떤 경우이냐에 따라서 결정되어야 합니다.

 

내장 열거형 유형

대부분의 고급 프로그래밍 언어는 열거형 유형에 대해서 몇 가지 기본 지원을 제공하고 있습니다.

C++에서 열거형 유형은 enum 키워드로 선언됩니다. 열거형으로 구성된 열거형의 멤버를 열거자( #enumerators ) 라고 합니다.

다음을 예를 들면,

enum { white, black, red, green, blue, magenta, yellow, cyan };
 

이 유형과 앞 선 예의 전처리기 매크로 사이의 주요 차이점은 열거자가 실제로 컴파일러가 알 수 있다는 것입니다. 즉, 컴파일러는 실제로 cgFundamentalColors[red]를 봅니다. 하지만, 쉽게 알 수 있듯이 위 선언에는 열거자를 해당 정수 값에 명시적으로 매핑하지 않습니다. 이것은 이러한 유형의 중요한 속성 값을 나타내며 해당 표현과 관계가 없습니다.

 

많은 경우에 원하는 것이 이런 것들이라는 것입니다.그러나 꽤 자주 열거자가 표시되는 방식을 더 잘 제어해야 할 필요가 있습니다. C++에서 각 열거자는 명시적으로 정수 유형의 리터럴로 나타낼 수 있습니다.

 

예를 들어 다음 선언은 :

enum { white = 0, black, red, green, blue, magenta, yellow, blue };
 

정수 값에 대한 매핑이라는 의미에서 보면 전처리기 접근 방식과 동일합니다.

하지만 반면에 전처리기 매크로와 달리 열거자는 유형으로 지정 됩니다.

이는 다음의 코드 조각으로 (전처리기 매크로와) 명확하게 구분이 됩니다:

enum FundamentalColors { white = 0, black, red, 
               green, blue, magenta, yellow, blue };
enum Fruit { apple = 0, orange, peach, cherry };

FundamentalColors aColor = red;
Fruit aFruit = apple;

if ( aColor == RED ) ... // works
if ( aColor == red ) ... // works
if ( aColor == apple ) ... // works

int i = orange; // works
aColor = orange; // doesn't work
 

마지막 할당이 오류가 나는 이유가 바로 매크로에 비해 열거형 선언의 주요 이점입니다. 이 선언으로 할당에서 열거자의 상호 교환 가능성을 제한합니다. 반면 내장 정수 유형 변환으로 인해 enum 유형 간의 의도하지 않은 비교 가능성에 대한 경고는 남아 있습니다.

 

enum-types에는 매우 독특한 열거자 세트(enumerator set)를 가집니다. 따라서 정수 값의 경우와 같이 일반적으로 이 타입들은 비트 논리 연산자와 함께 사용할 수 없습니다.

열거자 집합이 너무 크지 않은 경우에는 연산자 오버로드가 도움이 될 수 있습니다. 열거형 유형의 열거자 집합이 충분히 작은 경우 열거자는 2의 거듭제곱 표현으로 할당 할 수 있습니다.

예를 들면 다음과 같습니다:

enum { white = 1, black = 2, red = 4, blue = 8 /* ... */ };
 

그러나 대부분의 경우 열거자를 배열 인덱서로 사용하는 것은 제외 될 것입니다.

 

열거자 자체에 비트 논리 연산자를 적용하는 대신 이것은 정수 형식 변환의 결과로 적용됩니다. 하지만, 연산자의 결과는 일반적으로 열거형 유형의 열거자 집합에서 제외 될 것입니다.

 

enum-type 들은 유형의 정의 범위에 추가 됩니다. 하나의 enum-type은 자신의 범위내메만 열려 있지 않습니다.그렇기 때문에 enum-type이 전역 네임스페이스에 선언되어 있으면 상대적으로 이름 충돌을 유발하기가 쉽습니다.

enum { enu1, enu2, enu3 };
enum SomeEnum { enu1, enu2, enu3 }; // error
class aClass {
  enum { enu1, enu2, enu3 }; // ok
  };
namespace {
  enum { enu1, enu2, enu3 }; // ok
  }
 

상수 정수 유형 변수들

간단히 말해서 열거자는 상수 값 특성을 가집니다. 그러나 때로는 일반 변수나 인스턴스를 사용하는 것과 유사한 방식으로 열거자를 사용하고 싶을 수도 있습니다. 예를 들어 열거자의 주소에 대한 액세스가 필요한 상황이 있을 수 있습니다. 상수 변수로 이 열거자들을 식별한다면 이러한 상황에서 도움이 될 수 있습니다.

이것은 적절한 정수유형 및 - 일반적으로 - 정적 스토리지 클래스의 상수 변수를 선언하여 쉽게 만들 수 있습니다.

예를 들어 색상 열거형의 열거자는 다음과 같이 선언할 수 있습니다.

const int white = 0;
const int black = 1;
const int red = 2;
const int green = 4;
// ...
 

이것은 엄격한 의미론적 문법에서 열거자 선언이 아니지만 이렇게도 사용할 수 있다는 점에 유의하는 것이 중요합니다. 그들의 특성은 enum-type의 열거자 또는 매크로와 일반 변수의 상수 특성 사이의 어떤 매개자 같은 것입니다. 이 선언들은 상수를 사용할 수 있는 모든 위치에서 사용할 수 있습니다. 반면에 이들은 const 포인터를 통해서만 이들을 주소로 참조할 수 있습니다.

 

단일 정의 규칙(one-definition-rule)이 const 변수에도 적용되므로 이러한 변수의 실제 값은 정확히 한 곳에 저장됩니다.

따라서 const 변수의 값을 변경하면 종속 소스를 다시 컴파일할 필요가 없을 것입니다

(단순화를 위해 다음 형식의 선언 이며,:

extern const type name = value;
 

여기서는 다루지 않습니다).

 

이러한 const 변수 사용의 단점은 개별 유형을 가지지 않는다는 것입니다.

 

열거형을 구현하는 이런 접근 방식은 언뜻 보기에 단순한 학문적 관심으로만 보일 수 있지만 열거자로서의 값에서 유형의 인스턴스로의 전환은 열거형 유형을 구현하는 데 있어 이는 다음 부분의 주제 인 클래스 기반 접근 방식으로 가는 길을 열어줍니다.

 

III 클래스 기반 열거형

지금까지 열거형 형식과 해당 열거자의 구현은 어떤 정수 형식의 표현에만 의존했었습니다. 특히, 열거자의 사용은 상수의 사용으로 요약 되어졌습니다.

하지만 이전 섹션에서 마지막에 설명한 변형으로 이러한 의문에서 상수 변수 또는 해당 유형의 인스턴스를 사용하여 이 원칙을 다소 모호하게 만들었습니다.

이 개념을 일반화하고 정수 유형 대신 클래스를 사용하면 유형 안전성을 유지하고 객체 지향 설계를 지원하면서 앞서 앞서 언급한 접근 방식의 대부분의 속성을 결합하는 열거자를 정의할 수 있습니다.

 

정적 클래스 멤버 열거자 사용

C++(및 대부분의 다른 강력한 형식의 개체 지향 프로그래밍 언어)에서 각 클래스에는 다른 모든 형식과 구별되는 고유한 형식이 있습니다.

명시적으로 정의하지 않으면 이들은 기본(부모) 클래스의 참조 유형으로 변환하는 경우를 제외하고 서로 변환할 수 없습니다.

특히 기본/파생 클래스 포인터 변환을 제외하고 내장 유형의 경우와 같이 암시적 내장 유형 변환 또는 값(숫자) 승격 같은 것은 없습니다.

 

반면에 열거자는 종종 클래스 외부 컨텍스트에서 사용됩니다. 즉, 특정 클래스의 인스턴스가 존재할 필요가 없습니다; 대신 전역 상수/변수 특성을 가집니다.

 

이러한 두 가지 관찰을 활용하여 섹션 1의 RGB 색상 클래스 선언을 다음과 같이 다시 작성할 수 있습니다:

class RGB {
public:
  static const RGB WHITE;
  static const RGB BLACK;
  static const RGB RED;
  static const RGB GREEN;
  static const RGB BLUE;
  static const RGB MAGENTA;
  static const RGB YELLOW;
  static const RGB CYAN;
public:
  RGB( unsigned int red = 255, unsigned int green = 255, 
       unsigned int blue = 255 ) throw();
  unsigned int red;
  unsigned int green;
  unsigned int blue;
};
 

정적 멤버의 정의는 간단하며 RGB의 ctor를 사용하여 색상 값을 설정합니다.

const RGB RGB::WHITE;
const RGB RGB::BLACK( 0, 0, 0 );
const RGB RGB::RED( 255, 0, 0 );
// ... rest of members
 

이 RGB 선언을 통해 RGB 클래스의 두 인스턴스를 서로 비교할 수 있습니다(적절한 비교기가 있다고 가정 한다면). 특히 RGB 인스턴스는 기본 색상을 나타내는 8개의 상수 인스턴스들을 비교할 수 있습니다.

파생 클래스의 인스턴스를 제외하고 RGB 인스턴스는 다른 유형의 인스턴스와 비교할 수 없습니다. 이 특별한 경우는 필요한 경우 비교기 내에서 인스턴스의 클래스를 확인하여 처리할 수 있습니다. 다음 논의의 나머지 부분에서는 RGB는어떠한 파생된 클래스도 가지지 않는다고 가정합니다.

 

정수 유형이 포함된 사용자 정의 변환 연산자를 대상 유형으로 선언할 때 특별한 주의를 기울여야 합니다. 이렇게 하면 기본 제공 열거 유형과 유사한 방식으로 RGB를 사용할 수 있습니다.

 

클래스 RGB 인스턴스의 할당 및 초기화는 일반적으로 복사 생성자 구현 및/또는 할당 연산자 오버로딩을 통해 수행할 수 있습니다. 필요한 경우 비트 논리 연산자를 오버로드할 수도 있습니다. 유일한 제한은 정수 유형만 switch 문의 인수로 사용할 수 있다는 것입니다.

컴파일러 생성 점프 테이블이 없기 때문에 런타임에 약간의 페널티가 있긴 하지만 계단식 if 문이 동일한 작업을 수행할 수 있기 때문에 이 부분은 실제로 큰 문제는 아닙니다.

 

정적 멤버의 런타임 동작

사소한 경우를 제외하고 정적 클래스 멤버가 관련될 때마다 정적 저장소 유형의 변수 또는 인스턴스의 초기화 특성을 기억하는 것이 좋습니다.

 

기본 유형의 경우, 규칙은 매우 간단합니다: 리터럴, 리터럴 표현식 및 이전에 초기화된 기본 유형의 정적 변수가 있는 표현식은 컴파일 타임에 할당됩니다. 이것들이 리터럴인 경우 참조 유형의 정적 변수도 마찬가지입니다. 일반적으로 이는 NULL로 초기화 된다는 것을 의미합니다. 초기화 순서는 변환 단위 내의 정의 순서입니다.

 

상수, 리터럴이 아닌 표현식으로 초기화되는 포인터 유형 변수, 즉 어떤 유형이든 정적 저장 유형의 나머지 변수 주소는 링크 타임에 초기화됩니다. 링크 타임은 정적 링크 타임, 즉 링크 편집기가 명령줄에서 실행될 때(예: nmake, Visual Studio 프로젝트 빌드)의 두 가지 의미를 가질 수 있습니다. 대상 플랫폼과 실행 가능한 유형에 따라 동적 링크 타임을 의미할 수도 있습니다.

예를 들어 DLL 또는 다른 유형의 공유 코드를 실행할 때 모든 변수(점프 테이블 및 프로시저의 내용 포함)의 최종 주소는 런타임 링커가 실행될 때까지 결정되지 않을 것입니다.

마지막으로 소위 ctor-chain이 실행됩니다. ctor-chain동안에 나머지 초기화들은 변환 단위를 통한 정의의 순서대로 실행이 될 것입니다.

 

다음 코드 조각은 이러한 규칙을 요약합니다.

int a = 0; // compile-time
int b = 1 + 1; // compile-time
int c = b + 1; // compile-time

int* pint = NULL; // compile-time
int** ppint = &pint // link-time

void foo();
void (*ptrFoo)(void) = &foo; // link-time

RGB aColor( 10, 10, 10 ); // ctor-chain

RGB* pAColor = new RGB( 10, 10, 10 ); // ctor-chain
 
 

변환 단위 간의 초기화 순서는 컴파일러에 따라 다릅니다.

컴파일 타임에 변환 단위 (즉, 컴파일된 코드)가 링크 편집기에 입력되는 순서에 따라 결정되는 경우가 많습니다.

 

인스턴스 대신 포인터 사용

클래스 기반 열거자는 변환 단위에서만 참조되는 한 정적 멤버로 클래스 인스턴스에 정의되어 있으므로 많은 문제 없이 사용할 수 있습니다. 분명히 이것은 일반적으로 그렇지 않습니다. 정적 멤버는 실행 파일의 코드가 빌드된 변환 단위 전체에서 또는 라이브러리 프로젝트(예: DLL)의 경우 언제 어디서나 참조됩니다.

 

대부분의 경우 정적 클래스 멤버가 클래스의 인스턴스인 경우 이에 대한 사용이 심각하게 제한됩니다.

이 문제에 대한 해결책은 정적 클래스 멤버를 사용하는 대신 포인터 유형을 사용하는 것입니다.

이들은 링크 타임에 초기화되므로 코드를 실행하여 액세스하기 전에 초기화됩니다.

 

예를 들어:

class RGB {
public:
  static const RGB* const WHITE;
  static const RGB* const BLACK;
// ...
};
const RGB* const RGB::WHITE = new RGB;
const RGB* const RGB::BLACK = new RGB( 0, 0, 0 );
// ...
 

포인터가 되는 것을 제외하고 이 열거자 변형은 해당 인스턴스와 같이 사용할 수 있습니다.

메모리 할당, 즉 operator new 호출은 ctor-chain 실행 시 이루어지지만 메모리 해제는 수작업으로 이루어져야 합니다.

그러나 이것은 대부분의 상황에서 실제로 필요하지 않습니다. 왜냐하면 문제의 코드가 호출 프로세스의 주소 공간에 로드될 때 할당이 한 번만 발생하기 때문입니다. 하지만, 수동 할당 해제는 메모리 누수 감지 감시를 만족스럽게 유지 시켜 줄 것입니다.

 

ctor-chain을 실행하면 시간 패널티가 부과됩니다. 이런 경우가 허용되지 않는 상황이 있습니다.

예를 들어, 시간이 중요한 코드이거나 너무 많은 초기화가 발생하는 경우일 수 있을 것입니다.

 

열거자가 기껏해야 같은지 비교되는 불투명 엔티티로만 사용되도록 보장할 수 있는 경우 특수 초기화 표현식을 사용할 수 있습니다:

const RGB* const RGB::WHITE = reinterpret_cast< const RGB* const >( & RGB::WHITE );
const RGB* const RGB::BLACK = reinterpret_cast< const RGB* const >( & RGB::BLACK );
 

이 초기화는 링크 타임에 발생하며 가장 중요한 속성인 고유성(uniqueness)을 유지합니다.

소스 코드 생성기는 이러한 표현식을 자동으로 생성하도록 쉽게 설정할 수 있습니다.

 

단점은 이러한 포인터가 실제 클래스 인스턴스를 가리키지 않는다는 것입니다.

이러한 포인터를 통해 RGB 클래스의 비정적 멤버에 접근 하려는 어떤 시도 든 완벽히 실패 할 것입니다.

따라서 이 기술은 그 결과를 신중하게 고려한 후에 잘 정의 된 문서와 같이 사용해야 합니다.

 

라이선스

이 기사에는 명시적인 라이센스가 첨부되어 있지 않지만 기사 텍스트 또는 다운로드 파일 자체에 사용 조건이 포함될 수 있습니다. 의심스러운 경우 아래 토론 게시판을 통해 작성자에게 문의하십시오.

 

작성자가 사용할 수 있는 라이선스 목록은 여기에서 찾을 수 있습니다.

이상.

 

728x90