[C++] 몇 가지 값만 가질 수 있는 타입: unscoped enum

반응형

두 가지 값만 가질 수 있는 타입 bool

bool 타입은 C++의 기본적인 타입( fundamental type )의 하나입니다.

이 타입은 키워드로 정해진 두 값 true와 false만을 가질 수 있는데, 이 상징적인 상수 true는 사실 정수 1을 가리키고, false는 정수 0을 의미합니다.

#include <iostream>

int main(){

    std::cout << true << " " << false << '\n';
}

▼출력

1 0

그렇기 때문에, '그냥 intshort 타입을 사용해도 되는 것 아닌가'라고 생각할 수 있고, 실제로 그렇게들 많이 사용합니다.

하지만, 프로그램 코드의 입장에서 보면, bool 타입의 사용은 크게 두 가지 장점을 갖고 있습니다.

 

첫 번째, bool 타입을 사용함으로써, 코드를 읽고 이해하기 쉽게 만들 수 있습니다.

if ( Calculation() == true ){	// Calculation() == 1
    // do something
}

위와 같이, true 키워드를 사용하면, 이것이 함수의 성공 여부를 나타내는 것임을 쉽게 알 수 있습니다.

하지만, 이 대신 1을 사용하면, 이 값이 계산 결과 값을 말하는 것인지 혹은 다른 어떤 것을 말하는 것인지를 알기 위해서, 문서를 참고하거나, 소스를 읽어야 할 것입니다.

게다가, true라는 이름의 상수를 사용함으로써, 1을 사용하는 것보다 코드를 읽기 쉬게 만듭니다.

 

두 번째, bool 타입은 두 가지의 값만을 가질 수 있기 때문에, 이 타입을 사용함으로써, 이 표현식이 가질 수 있는 상태의 수를 바로 알 수 있습니다.

int getResult(){
    // do something
}

위의 함수가 실제로는 01만을 반환한다고 하여도, 함수의 선언만 보고선, 다른 값을 반환하지 않는다는 것을 알 수가 없습니다.

그래서, 이를 방어하기 위한 코드가 필요한 경우도 있습니다.

하지만, bool 타입의 값을 반환하는 함수의 선언을 하게 되면, 이 함수는 truefalse만을 반환하는 함수임을 바로 알 수 있습니다.

 

 

그런데, 만약 bool 타입의 장점을 얻고 싶지만, true / false 두 개의 상태만을 표현하는 것이 아니라, 신호등처럼 3개의 상태를 나타내는 타입이 필요한 경우엔 어떻게 해야 할까요?

 

 

몇 가지 값만 가질 수 있는 타입 enum

이를 위해서 C++에서는 여러 가지의 값을 가질 수 있는, bool 타입의 확장형인, enum 타입을 제공합니다.

enum 타입은 class나 struct 같이 사용자 정의 타입( user defined type )의 하나로서, 이 타입이 가질 수 있는 값을 나열하는 방식으로 정의됩니다.

enum Stoplight{
    red,    
    yellow,
    green,  // comma로 구분, 마지막에도 일반적으로 comma를 사용

};  // semi-colon으로 종료

이러한 타입을 열거형( enumerated type )이라고 하며, 이 타입이 가질 수 있는 값들을 열거자( enumerator )라고 합니다.

그리고, 이 타입은 다른 사용자 정의 타입과 마찬가지로 세미콜론을 사용해서 정의를 마무리해야 합니다.

 

위의 Stoplight 타입은, bool 타입이 true, false로 이름 붙은 값만을 가질 수 있는 것과 비슷하게, red, yellow, green으로 이름 붙은 상수만을 값으로 가질 있게 됩니다.

int main(){

    Stoplight sl0 = red;
    Stoplight sl1 = 2;      // error !
    Stoplight sl2 = true;   // error !
}

위에서 볼 수 있듯이, Stoplight 변수는 다른 타입의 값( 2, true )을 가질 수 없습니다.

 

참고로, bool 타입은 enum 타입과 다르게, true, false 외의 값을 가질 수 있는 것처럼 보일 수 있는데, 이것은 내부적으로 컴파일러에 의한 암시적 변환이 일어나기 때문입니다.

bool state = 2;	// state의 값은 묵시적으로 1이 됩니다.

 

열거자( enumerator )의 값

키워드 true의 실제값이 1이고, false의 실제값이 0인 것처럼, 열거자의 실제값은 기본적으로 0부터 시작해서, 1씩 증가하는 식으로 정의되어 있습니다.

enum Stoplight{
    red,        // 0
    yellow,     // 1
    green,      // 2
};

int main(){

    std::cout << red << " " << yellow << " " << green << '\n';
}

그래서, 위의 경우 red0, yellow1, green2의 값을 갖게 됩니다.

 

▼출력

0 1 2

하지만, 이러한 열거자( enumerator )의 초기값도 변경이 가능합니다.

enum Color{
    red = 5,
    yellow,     // 6
    blue,       // 7
    green = 40,      
    white = 40,
    black,      // 41
};

int main(){

    Color cr{}; // 0, 값-초기화
}

위의 경우, red 값이 5로 시작했기 때문에, yellowblue는 각각 67을 가지게 됩니다.

여기서, 재밌는 것은 위의 greenwhite처럼, 열거자들은 같은 값을 가질 수 있다는 것입니다.

마지막 blackwhite의 다음에 선언되었으므로( 1 증가해서 ), 41의 값을 갖게 됩니다.

 

그리고 주의할 것은, Color 타입의 열거자들이 위와 같은 값을 갖고 있다고 하더라고, 이 Color 타입의 cr 변수를 값-초기화( value-initialization )하게 되면, cr0의 값을 갖는다는 것입니다.

 

또한, 이러한 열거자( enumerator )들의 값은 enum을 정의할 때 선언되므로, 암묵적으로 상수 표현식( const expression )이 됩니다.

그래서, 아래와 같은 변수의 정의가 가능합니다.

constexpr Color cr{ blue };	// constexpr 변수 정의

위의 blue는 컴파일 시 값을 알 수 있는 상수 표현식입니다.

따라서, 상수 표현식으로만 초기화되어야 하는 constexpr 변수를 정의하는 데 사용될 수 있습니다.

 

범위 없는 enum( unscoped enumeration )

보통, 사용자 정의 타입( class, struct )은 범위 영역( scope region )을 제공합니다.

그래서, 타입의 멤버에 접근하려면, 멤버 선택 연산자( member selection operator )나 범위 지정 연산자( scope resolution operator )를 사용해야 합니다.

struct SerialNumber{
    static int s_Counter;
    int serial;
};

int SerialNumber::s_Counter{ 0 };   // static 멤버의 정의

int main(){

    SerialNumber sn;
    sn.serial = SerialNumber::s_Counter;  // 멤버 선택 연산자

    SerialNumber::s_Counter++;  // 범위 지정 연산자
}

그런데, 아래의 enum은 이러한 범위 영역을 제공하지 않습니다.

그리고, 이러한 enum범위 없는 enum( unscoped enum )이라고 부릅니다.

enum Stoplight{	// 범위 없는 enum
    red,        
    yellow,     
    green,      
};  

int main(){

    std::cout << Stoplight::red << " " << yellow << " " << green << '\n';
}

위에서, Stoplight 타입의 red 열거자를 사용하기 위해서, 타입명과 범위 지정 연산자 ::를 사용할 수도 있지만, yellow처럼 열거자만을 바로 사용할 수도 있습니다.( 그리고, 이 방식이 더 선호됩니다. )

 

그래서, 결과적으로 enum의 열거자( enumerator )는 enum 타입이 정의된 범위와 같은 범위를 갖게 됩니다.

예를 들어, 위와 같이 Stoplight 타입이 전역에 정의되었으면, red와 같은 열거자( enumerator )들도 전역적으로 사용가능하게 됩니다.

enum Stoplight{	// 전역 범위
    red,        // 0
    yellow,     // 1
    green,      // 2
};  

void func(){

    enum Small_light{	// 함수 범위
        black,
        white,
        yellow, // 2
    };

    std::cout << "Small_light yellow: "  << yellow << '\n';
}

int main(){
    
    std::cout << "Stoplight yellow: " << yellow << '\n';
    func();
}

그런데, 함수는 범위 지역 { }을 제공하므로, 함수 내에 정의된 Small_light와 이 타입의 열거자들( black, white, yellow )은 이 함수의 지역 범위를 갖게 됩니다.

그리고, 함수 내에서 지역 변수가 전역 변수를 지우는 것( shadowing )과 마찬가지로, 함수 내의 지역 열거자는 전역 열거자를 지우게 됩니다.

그래서, 위의 yellow 열거자와 같이, 이 열거자는 전역과 지역 모두에서 정의되어 있는데, 함수 내에서는 Small_lightyellow가 사용되는 것을 알 수 있습니다.

 

▼출력

Stoplight yellow: 1
Small_light yellow: 2

 

이름 충돌( name collision )

범위 없는 enum 타입이 범위 지역( scope region )을 제공하지 않기 때문에, 이 타입들을 정의하는 과정에서 열거자의 이름이 충돌하는 경우가 있습니다.

enum Stoplight{
    red,        // 0
    yellow,     // 1
    green,      // 2
};

enum Color{
    red = 5,
    yellow,     // 6
    blue,       // 7
    green = 40,      
    white = 40,
    black,      // 41
};

int main(){
    // do something
}

▼출력

error: 'red' conflicts with a previous declaration

위와 같은 경우, 두 enum에서 red, yellow, green 열거자 모두 이름이 중복되고 있음을 볼 수 있습니다.

이것은 당연히 오류입니다.

 

그리고, 이 문제를 해결하는 가장 간단한 방법은 단지 이름이 중복되지 않도록 수정하는 것입니다.

enum Stoplight{
    sl_red,        // sl은 Stoplight의 약자
    sl_yellow,     
    sl_green,      
};

하지만, 외부 라이브러리를 사용하는 경우처럼, 이 방식을 사용할 수 없는 경우도 있습니다.

그런 경우에도 namespace를 사용해서 이 문제를 해결할 수 있습니다.

namespace Name1{

    enum Stoplight{
        red,        
        yellow,     
        green,      
    };  
}

namespace Name2{
    
    enum Color{
        red = 5,
        yellow,     // 6
        blue,       // 7
        green = 40,      
        white = 40,
        black,      // 41
    };
}

int main(){
    
    std::cout << "Stoplight yellow: " << Name1::yellow << '\n';
    std::cout << "Color yellow: " << Name2::yellow << '\n';
}

▼출력

Stoplight yellow: 1
Color yellow: 6

하지만, 위와 같이 namespace 안에 정의된 이름을 사용할 때는, Name1::yellow처럼 namespace의 이름과 범위 지정 연산자( scope resolution operator )를 함께 사용해야 합니다.

 

[C++] 이름 충돌을 해결하기 위한 namespace

namespacenamespace는, 단어에서 알 수 있듯이, 이름( identifier )이 유일하게 정의된 공간을 말합니다.그리고, 이러한 공간을 만드는 이유는, 생각 외로 자주 발생하는 이름의 중복을 막기 위해서입니다

codingbonfire.tistory.com

 

참고로, 범위 지역을 갖고 있는 enum 타입도 있습니다.

이 enum 타입을 범위 있는 enum( scoped enumeration )이라고 합니다.

이러한 enum은 자체 범위 지역이 있기 때문에, 위의 예제와 달리 이름 충돌이 발생하지 않습니다.

enum class Stoplight{ // 범위있는 enum
    red,        
    yellow,     
    green,      
}; 

enum Color{
    red = 5,
    yellow,     // 6, 위의 yellow와 충돌이 발생하지 않습니다.
    blue,       // 7
    green = 40,      
    white = 40,
    black,      // 41
};

int main(){
    
    // error !! 출력이 안됨. 
    std::cout << "Stoplight yellow: " << Stoplight::yellow << '\n'; 
    std::cout << "Color yellow: " << yellow << '\n';
}

하지만, 이 경우 열거자를 사용하려면, namespace를 사용하는 경우와 마찬가지로, enum 타입 이름과 범위 지정 연산자를 함께 사용해야 합니다.

Stoplight::yellow;	// 타입 이름과 범위 지정 연산자

그리고, 이 예문에서 볼 수 있듯이, 컴파일러는 Stoplight 타입을 << 연산자를 통해 출력하는 방법을 모르기 때문에, 컴파일 오류를 발생시킵니다. ( 물론, 함수를 작성해서 컴파일러에게 출력하는 방법을 알려줄 수는 있습니다. )

 

그럼, 범위 없는 enum의 열거자는 어떻게 << 연산자를 사용해서 출력할 수 있는 걸까요?

 

범위 없는 enum의 변환( conversion )

범위 없는 enum( unscoped enum ) 타입의 변수와 열거자( enumerator )는 실제로 정수형 값( integral value )을 갖고 있으므로, 이를 바탕으로 컴파일러는 enum 타입을 정수형 타입으로 암시적 변환을 수행합니다.

 

그래서, 다음과 같은 문장은 제대로 동작합니다.

namespace Name1{
    enum Stoplight{ // 범위없는 enum
        red,        
        yellow,     
        green,      
    };    
}

int main(){
    
    std::cout << "Stoplight yellow: " << Name1::yellow << '\n'; // 암시적 변환

    Name1::Stoplight sl{ Name1::red };
    std::cout << "Stoplight value: " << sl << '\n'; // 암시적 변환
}

하지만, 정수형 값( integral type )을 사용자 정의 타입인 enum으로는 전환할 수 없습니다.

컴파일러는 이러한 타입에 대한 변환 방법을 미리 귀띔받지 못했기 때문입니다.

그래서, 정수형을 enum 타입으로 변환하려면, 다음과 같이 명시적인 변환 방법을 수행해야 합니다.

enum Stoplight{ // 범위없는 enum
    red,        
    yellow,     
    green,      
}; 

void printState( const Stoplight& sl){
    
    std::string_view sv;
    if ( sl == red) sv = "stop";
    if ( sl == yellow) sv = "be careful";
    if ( sl == green) sv = "go";
    
    std::cout << sv << '\n';
}

int main(){
    
    Stoplight sl{ 1 };   // error !! 암시적인 변환을 할 수 없습니다.
    Stoplight sl2{ static_cast< Stoplight >(1) }; // 명시적 변환
    
    printState( static_cast< Stoplight>(2) );	// 명시적 변환
}

▼출력

go

그리고, 함수를 호출할 때도 마찬가지로 명시적 변환을 통해 정수형 값을 전달해야 합니다.

 

[C++] C-style 타입 변환과 C++의 static_cast

암시적인 타입 변환과 명시적인 타입 변환암시적인 타입 변환( implicit type conversion )은 변수나 표현식의 타입이 일치하지 않는 경우, 컴파일러가 자동으로 필요한 타입으로 변경하는 것을 말합니

codingbonfire.tistory.com

 

여기서 의문의 떠오를 수 있습니다.

위에서 범위 없는 enum은 정수형 값( integral value )을 갖는다고 했습니다.

그런데, C++에서 정수형 값을 갖는 타입은 매우 많습니다. ( int, char, bool, short, unsigned... )

그럼, 구체적으로 enum은 이러한 타입 중에 어떤 타입의 값을 갖는 것일까요?

 

범위 없는 enum의 바탕 타입( underlying type or base )

범위 없는 enum이 가질 수 있는 값을 타입을 바탕 타입( underlying type 혹은 base )이라고 합니다.

그런데, C++ 에선 범위 없는 enum의 바탕 타입을 명시하지 않았습니다.

그래서, 이를 구현하는 컴파일러의 종류에 따라, enum의 바탕 타입은 다를 수 있습니다.

일반적으로는 이 바탕 타입을 int 타입으로 설정하고 있습니다.

enum Stoplight{ // 범위없는 enum
    red,        
    yellow,     
    green,      
};

int main(){

    int sz = sizeof( Stoplight );
    std::cout << "size of Stoplight: " << sz << '\n';
}

▼출력

size of Stoplight: 4

하지만, enum 변수의 크기가 중요한 경우에 명시적으로 바탕 타입을 지정할 수 있습니다.

#include <iostream>
#include <cstdint>	// for int8_t

enum Stoplight : int8_t { // 8 bits 정수로 바탕 타입 지정
    red,        
    yellow,     
    green,      
};  

int main(){

    int sz = sizeof( green );   // 당연히 Stoplight 크기도 동일
    std::cout << "size of Stoplight: " << sz << '\n';
}

▼출력

size of Stoplight: 1

위의 경우, Stoplight 변수나 열거자는 8 bits 정수가 가질 수 있는 범위의 값을 가질 수 있습니다.

그렇기 때문에, Stoplight 변수는 열거자가 가지지 않은 값을 가질 수 있고, 이것은 오류가 아닙니다.

만일, 이 범위 밖의 값을 지정하는 경우, 프로그램은 정의하지 않는 동작을 초래할 수 있습니다.

 

그리고, 이전 항목에서, 정수형 값을 범위 없는 enum으로 변환하려면 명시적인 방법을 사용해야 한다고 했습니다.

Stoplight sl{ static_cast< Stoplight >(5) };	// 명시적 변환

이 방법 외에도, C++ 17 이후에는 위와 같이 enum 타입의 바탕 타입( underlying type )을 지정하는 경우, 리스트 초기화를 통해서도 정수형 값을 enum 값으로 변경할 수 있습니다.

int main(){

    Stoplight sl{ 5 };  // list initialization. ok
    
    Stoplight sl2( 5 ); // direct initialization, error !
    Stoplight sl3 = 5;  // copy initialization, error !

    sl = 5; // 대입 연산은 안됩니다. error !
}

주의할 것은, 이러한 암시적인 변환은 enum 타입의 변수를 초기화할 때만 일어나고, 리스트 초기화( list initialization )를 제외한 다른 초기화 방식에서는 이러한 변환을 할 수 없다는 것입니다.

그리고, 위에서 보듯이, 암시적인 대입 연산도 허용되지 않습니다.

 

 

정리

  • enum은 열거자( enumerator ) 수만큼의 값을 가질 수 있는 사용자 정의 타입( user defined type )입니다.
  • 범위 없는 enum( unscoped enum )의 열거자들은 이 enum 타입이 정의된 범위와 같은 범위를 갖습니다. 그러므로, 범위 지정 연산자::를 사용할 필요가 없습니다.
  • 범위 없는 enum은 정수형( integral type )으로 암시적으로 변환될 수 있지만, 정수형을 범위 없는 enum으로 변환하려면 명시적인 방법을 사용해야 합니다.

 

 

이 글과 관련 있는 글들

 

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유