[C++] 사용자 정의 타입 enum을 입/출력 하기

반응형

enum 타입의 값을 문자열로 출력하기

enum 타입의 열거자( enumerator )들은 사실 이름을 가진 정수형( integral ) 상수입니다.

 

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

두 가지 값만 가질 수 있는 타입 boolbool 타입은 C++의 기본적인 타입( fundamental type )의 하나입니다.이 타입은 키워드로 정해진 두 값 true와 false만을 가질 수 있는데, 이 상징적인 상수 true는 사실

codingbonfire.tistory.com

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

그래서, std::cout를 통해 이 열거자를 출력해 보면, 열거자의 이름이 아니라 숫자가 출력하는 것을 볼 수 있습니다.

#include <iostream>

int main(){

    std::cout << red << '\n';

    Stoplight state{ green };
    std::cout << "Stoplight state is " << state << '\n';
}

▼출력

0
Stoplight state is 2

그리고 이렇게 되는 것은, 컴파일러가 사용자 정의 타입인 Stoplight 타입을 << 연산자를 이용해서 출력하는 방법을 아직 모르기 때문입니다.

이런 경우, 컴파일러는 Stoplight를 암시적으로 변환 가능하고, std::cout을 통해서 출력할 수 있는 다른 타입이 있는지를 검토합니다.

enum 타입은 정수형 타입( 그리고, 대부분의 경우 int 타입 )으로 변환이 가능하고, 정수형 타입은 C++에서 << 연산자를 통해 출력하는 방법을 이미 제공합니다.

그래서, Stoplight 타입의 값이 정수로 출력되는 것입니다.

 

하지만 "Stoplight state is 2"라고 출력하는 것보다, 좀 더 표현적인 문자열을 출력하는 것이 더 나아 보입니다.

아래의 코드는 Stoplight 타입을 문자열로 출력하는 방법을 보여줍니다.

#include <iostream>
#include <string_view>  // for std::string_view

using std::string_view;

constexpr string_view getStateString( const Stoplight& sl){
    
    switch( sl ){
        case red: return "stop right now";
        case yellow: return "be careful";
        case green: return "go ahead";
        default: return "unknown";
    }
}

int main(){

    constexpr Stoplight state{ green };
    std::cout << "Stoplight state is " << getStateString( state ) << '\n';
}

▼출력

Stoplight state is go ahead

그리고, 위의 코드에 관해서는 몇 가지 얘기할 것이 있습니다.

 

첫 번째, Stoplight 열거형( enumerated type )이 가질 수 있는 값은, 사실 red, yellow, green 뿐 아니라 바탕 타입( underlying type )이 가질 수 있는 범위의 모든 값들입니다.

enum Stoplight : short { // short가 바탕 타입
    red,   
    yellow,
    green, 
};

그리고, Stoplight 타입을 정의할 때, 이 enum 타입의 바탕 타입을 따로 지정하지 않으면, 보통 int 타입을 바탕 타입으로 삼기 때문에, 아래와 같은 명시적 변환을 통해 state는 값 1000을 가질 수도 있습니다.

int main(){

    Stoplight state{ static_cast< Stoplight >(1000) };	// 명시적 변환
    std::cout << "Stoplight state is " << getStateString( state ) << '\n';
}

그렇기 때문에, getStateString 함수 내의 switch 구문에서 default 항목을 사용한 것입니다.

 

두 번째, C-style의 문자열 리터럴( literal )을 함수에 전달하거나 반환할 때 가장 적합한 타입이 std::string_view라는 것입니다.

 

[C++] 문자열을 읽기 전용으로 참조하는 std::string_view

std::string_viewstd::string_view는 문자열을 사용하면서 발생하는, 무거운 복사 과정을 줄이고자 만든 클래스입니다. C++ 17 이상의 버전에서 사용할 수 있는, 이 클래스는 실제 원본 문자열을 읽기 전용

codingbonfire.tistory.com

이 타입은 원본을 수정할 수는 없지만, 원본 문자열을 복사하는 하는 과정이 없이도, 함수에 전달될 수 있고, 함수에서 반환될 수 있는 장점이 있기 때문입니다.

그리고, C-style 리터럴은 당연히 상수이므로, 이 문자열을 수정하는 것은 원래 불가능한 데다가, 이 상수는 프로그램 시작할 때부터 종료할 때까지 파괴되지 않습니다.

즉, std::string_view가 참조하는 대상이 파괴될 것과 이를 통해서 잘못된 동작을 할 걱정할 필요가 없다는 것입니다.

 

세 번째, getStateString 함수를 constexpr 함수로 지정함으로써, 컴파일러가 다음과 같은 최적화를 수행하도록 도울 수 있습니다.

int main(){

    //constexpr Stoplight state{ green };
    //std::cout << "Stoplight state is " << getStateString( state ) << '\n';
    
    // 위의 문장이 아래와 같이 최적화될 수 있습니다.

    std::cout << "Stoplight state is " << "go ahead" << '\n';
}

이것은 getStateString 함수와 인자인 state 모두 상수 표현식( const expression )이기 때문에 가능한 것입니다.

 

하지만, << 연산자를 이용해서 int 타입을 출력할 때와는 달리, Stoplight 타입을 문자열로 출력하기 위해선, getStateString 함수롤 호출해야 하는 불편함이 남아 있습니다.

 

이 문제를 해결하기 위해서, C++에서 제공하는 오버로딩( overloading ) 기능을 사용할 수 있습니다.

이를 통해서, 다음과 같이 << 연산자가 Stoplight 타입을 인수로 전달받는 함수를 만들 수 있습니다.

// << 연산자 오버로딩
std::ostream& operator<< ( std::ostream& os, const Stoplight& sl){

    return os << getStateString(sl);
}

위에서 getStateString 함수는 std::string_view를 반환하고, C++ 표준 라이브러리는 이 반환된 std::string_view<< 연산자를 통해서 출력하는 아래와 같은 방법을 이미 제공하고 있습니다.

std::ostream& operator<< ( std::ostream& os, string_view sv );

그리고, 이 std::string_view를 매개변수를 사용하는 << 연산자 오버로딩 함수 역시 std::ostream& 참조 타입을 반환합니다.

이렇게 같은 타입을 반환함으로써, std::cout( 정확하게는 std::ostream ) 객체는 한 줄에 여러 타입의 객체들을 연쇄적으로 출력하는 기능을 갖게 되는 것입니다.

 

이제, Stoplight 사용자 타입도 다른 일반 타입을 출력하는 것과 같은 방식으로 출력할 수 있습니다.

#include <iostream>
#include <string_view>  // for std::string_view

using std::string_view;

enum Stoplight { 
    red,    
    yellow, 
    green,  
}; 

constexpr string_view getStateString( const Stoplight& sl){
    
    switch( sl ){
        case red: return "stop right now";
        case yellow: return "be careful";
        case green: return "go ahead";
        default: return "unknown";
    }
}

// << 연산자 오버로드
std::ostream& operator<< ( std::ostream& os, const Stoplight& sl){

    return os << getStateString(sl);
}

int main(){

    Stoplight state{ green };
    std::cout << "Stoplight state is " << state << '\n';
}

▼출력

Stoplight state is go ahead

 

 

문자열을 입력받아 enum 변수의 값 설정하기

컴파일러는 >> 연산자를 통해 Stoplight 사용자 정의 타입의 데이터를 입력받는 방법을 알 수가 없기 때문에, 다음과 같은 코드는 오류를 발생합니다.

int main(){

    Stoplight state;
    std::cin >> state;  // error !!
}

그래서, 이전 항목에서 말했던 것과 같이 >> 연산자를 오버로딩( overloading ) 해, 입력 값을 Stoplight 타입의 값으로 변환해야 합니다.

// >> 연산자 오버로드
std::istream& operator>> ( std::istream& is, Stoplight& sl){

    std::string str;
    std::getline( is, str);	// 줄 단위 입력
        
    std::optional<Stoplight> ret = getState(str);
    if ( ret ){
        sl = *ret;
    }
    else{
        is.setstate( std::ios_base::failbit);	// 잘못된 문자열 입력 시
    }

    return is;
}

사용자가 입력하는 값은 std::cin을 통해 여러 타입( int, bool, std::string... )으로 입력받을 수 있는데, 여기서는 이전 항목의 출력하는 방식에 맞춰 std::string 타입으로 입력받는 것으로 정했습니다.

그리고, 입력된 문자열에 공백이 있는 경우( 예를 들어, "be careful" )도 처리할 생각으로 std::getline 함수를 사용했습니다.

 

참고로, std::cin >> str과 같이 입력을 받는 경우, 공백 앞부분까지만 입력됩니다.

예를 들어, "be careful"를 입력하면, str에는 "be"만 입력되고, 나머지는 입력 버퍼에 그대로 남아있게 되는 것입니다.

그러나, std::getline 함수를 기본값( 구분자가 '\n' )으로 사용하면, 줄 단위로 입력을 받게 됩니다.

 

그리고, 이 입력받은 문자열을 Stoplight 타입으로 변환하는 getState 함수를 호출합니다.

이 함수는 입력된 문자열이 특정한 문자열인 경우, 그에 해당하는 열거자( enumerator )를 반환하는 함수입니다.

#include <string_view>  // for std::string_view
#include <optional> // for std::optional

constexpr std::optional<Stoplight> getState( string_view sv){
    
    if ( sv == "stop") return red;
    if ( sv == "be careful") return yellow;
    if ( sv == "go") return green;

    return {};  // 반환값 없음
}

이 함수는 std::optional 객체를 반환하는데, 이 객체는 위 경우처럼 원하는 결과 값이 없을 수도 있는 경우( 입력된 문자열이 "stop", "be careful", "go"가 아닌 경우 )에 적합한 타입으로, 원하는 값이 없는 경우와 있는 경우를 구분할 뿐만 아니라, 원하는 값 자체를 저장할 수 있습니다.

 

그래서, 이 객체로부터 다음처럼 함수 호출의 결과를 손쉽게 처리할 수 있습니다.

std::istream& operator>> ( std::istream& is, Stoplight& sl){

    std::string str;
    std::getline( is, str);	// 줄 단위 입력
        
    std::optional<Stoplight> ret = getState(str);
    if ( ret ){	// bool 타입으로 암시적 변환
        sl = *ret;
    }
    else{
        is.setstate( std::ios_base::failbit);	// 잘못된 문자열 입력 시
    }

    return is;
}

std::optional 객체는 암시적으로 bool 타입으로 변환되므로( 원하는 값을 저장하고 있는 경우 true로 변환 ), if (ret)처럼 if 구문에 바로 사용할 수 있고, * 연산자를 사용하여 원하는 결과 값에 접근할 수 있습니다.

 

반대로, 만약 결과 값이 없는 경우, std::istream 객체에 상태 플래그( std::ios_base::failbit )를 설정하여, 외부로부터 잘못된 입력이 있었음을 표시하고 있습니다.

 

그리고 이 함수가, std::cout에 출력할 때와 마찬가지로, 연쇄적인 입력 기능이 수행되도록 std::istream& 참조 타입을 반환하도록 하는 것이 자연스러운 방법입니다.

 

이제, 위에서 작성한 >> 연산자 오버로딩 함수를 사용하는 main 함수입니다.

int main(){

    std::cout << "input stoplgiht state( stop, be careful, go ):\n";
    
    Stoplight sl;
    std::cin >> sl;	// 바로 입력받을 수 있습니다.

    if ( std::cin ){
        std::cout << sl;  // 입력받은 값 확인
    }
    else{

        // 스트림 버퍼에 남아 있는 모든 데이터 삭제
        std::cin.ignore( std::numeric_limits<std::streamsize>::max(), '\n');  

        std::cin.clear();   // 플래그 초기화

        std::cout << "invalid state was inputed\n";
    }
}

위에서, 중간 단계 없이 Stoplight 타입을 std::cin 객체로부터 바로 입력받을 수 있는 것을 볼 수 있습니다.

그러므로, Stoplight 타입을 이용하는 사용자는 더 이상 getState 같은 내부 함수를 알 필요가 없게 됩니다.

 

참고로, std::cin ( std::istream ) 객체는 bool 타입으로 암시적으로 변환되는데, 이 변환 시 내부 플래그를 검사하여, 오류 플래그가 있으면 false, 그렇지 않으면 true로 변환됩니다.

그래서, if (std::cin) 구문이 제대로 동작하는 것입니다.

 

물론, 입력 과정에서 ( 신호등의 상태를 설명할 수 없는 ) 잘못된 문자열이 들어있을 수 있습니다.

그래서, 위 예제에서는 이 경우에 스트림 버퍼에 남아 있는 데이터를 삭제하도록 했습니다.

std::cin.ignore( std::numeric_limits<std::streamsize>::max());

함수 ignore는 삭제하고자 하는 문자의 개수를 입력받는데, 만약 이 개수에 max() 값을 사용하면, 이는 매우 큰 수를 의미하는 것이 아니라, 제한이 없다는 뜻으로 해석합니다.

따라서, 이 경우 함수는 입력 버퍼에 있는 모든 데이터를 삭제하게 됩니다.

 

끝으로, 다시 정상적인 입력을 받기 위해서, clear 함수를 사용해서 std::cin 객체의 내부 플래그를 다시 초기화해야 합니다.

 

입 / 출력 소스 코드

#include <iostream>
#include <string_view>  // for std::string_view
#include <optional> // for std::optional
#include <limits>   // for std::numeric_limits

using std::string_view;

enum Stoplight { 
    red,    
    yellow, 
    green,  
};  

constexpr string_view getStateString( const Stoplight& sl){
    
    switch( sl ){
        case red: return "stop right now";
        case yellow: return "be careful";
        case green: return "go ahead";
        default: return "unknown";
    }
}

// Stoplight 타입 출력
std::ostream& operator<< ( std::ostream& os, const Stoplight& sl){

    return os << getStateString(sl);
}

constexpr std::optional<Stoplight> getState( string_view sv){
    
    if ( sv == "stop") return red;
    if ( sv == "be careful") return yellow;
    if ( sv == "go") return green;

    return {};  // 반환값 없음
}

// Stoplight 타입 입력
std::istream& operator>> ( std::istream& is, Stoplight& sl){

    std::string str;
    std::getline( is, str);
        
    std::optional<Stoplight> ret = getState(str);
    if ( ret ){
        sl = *ret;
    }
    else{
        is.setstate( std::ios_base::failbit);
    }

    return is;
}

int main(){

    std::cout << "input stoplgiht state( stop, be careful, go ):\n";
    
    Stoplight sl;
    std::cin >> sl;

    if ( std::cin ){
        std::cout << sl;  // 입력 확인
    }
    else{

        // 스트림 버퍼에 남아 있는 모든 데이터 삭제
        std::cin.ignore( std::numeric_limits<std::streamsize>::max());  

        std::cin.clear();   // 플래그 초기화

        std::cout << "invalid state was inputed\n";
    }
}

 

 

정리

  • <<, >> 연산자 오버로딩( overloading )을 통해서, enum 같은 사용자 정의 타입( user defined type )을 다른 타입과 같은 방식으로 입/출력할 수 있습니다.

 

 

 

 

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