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 )을 다른 타입과 같은 방식으로 입/출력할 수 있습니다.