열거형 타입 enum에 대해 이전에 포스팅하였는데, 편리하지만 생각보다 단점이 많이 존재한다. 이를 어느 정도 해소시켜줄 수 있는 것이 바로 열거형 클래스 enum class이다. 영역 제한 열거형이라고도 부르며 enum struct도 존재하지만 주로 enum class만 사용한다.


enum이 발생시키는 상황

 

enum은 마치 #define 매크로처럼 제한이 없이 넓은 영역에 영향을 미친다고 볼 수 있다. 서로 다른 enum일지라도 이름을 같게 해서는 안되며, 서로 다른 이름에 다른 enum일지라도 같은 값을 가지면 조건문에서 쓸 때 같다고 판단해버린다.

#include <iostream>

int main()
{
	using namespace std;

	enum Color
	{
		RED,
		BLUE
	};

	enum Fruit
	{
		BANANA,
		APPLE
	};

	Color color = RED;
	Fruit fruit = BANANA;

	if(color==fruit)
		cout << "Color is fruit ? " << endl;

	return 0;

}

위의 코드를 실행하면 if문에서 참으로 판단하고 출력을 수행한다.


enum class

 

enum class를 사용하면 애초에 위와 같은 조건문을 사용할 수 없도록 막아버린다. 아래는 enum class로 변경한 코드이다.

#include <iostream>

int main()
{
	using namespace std;

	enum class Color
	{
		RED,
		BLUE
	};

	enum class Fruit
	{
		BANANA,
		APPLE
	};

	Color color = Color::RED;
	Fruit fruit = Fruit::BANANA;
	
	if(color==fruit)
		cout << "Color is fruit ? " << endl;

	return 0;
}

위의 코드는 실행되지 않는다. 실행할 경우 다음과 같은 에러 메시지가 뜬다.


Error C2676 binary '==': 'main::Color' does not define this operator or a conversion to a type acceptable to the predefined operator

서로 다른 클래스의 멤버이기 때문에 막혀버린다.

if (static_cast<int>(color) == static_cast<int>(fruit))

물론 조건문을 위처럼 static_cast를 사용하여 int로 변경한 뒤 사용할 수 있겠지만 좋은 생각은 아니다.

 

enum class를 통해 같은 enum class 내부에 있는 값들로만 비교할 수 있게 되므로 휴먼 에러를 줄일 수 있는 방법이 되겠다.

  • 4.7.1 열거형 구현 방식 안 좋은 예
  • 4.7.2 Enumerated Types
  • 4.7.3 열거형 타입 사용 시 주의사항

 


4.7.1 열거형 구현 방식 안 좋은 예

 

어떤 특성에 대해 여러 종류로 분류하는 경우가 굉장히 많다. 실무에서도 입자의 성분을 분류하거나 계산 축을 설정하기 위해 정수형으로 나열해서 쓰곤 했다. 그냥 하드코딩으로 된 것도 있으며, 매크로를 사용하는 경우도 많았다. 아래의 예시를 보자.

int computeDamage(int weapon_id)
{
	if (weapon_id == 0) // sword
	{
		return 1;
	}

	if (weapon_id == 1) // hammer
	{
		return 2;
	}

	// ... 이런식으로 구현하면 힘들어진다.
}

무기별로 데미지를 다르게 해서 구현을 하려 했다. 0을 sword로 생각하고 구현했으며, 주석을 달아서 나름 친절하게 구현을 했다고 볼 수 있겠다. 하지만 무기 특성을 데미지 계산뿐만 아니라 다른 곳에서도 쓴다면 계속 위처럼 조건문을 작성해야 하는데 모든 곳에 주석을 달기엔 비효율적이다.


4.7.2 Enumerated Types

 

위와 같은 고민을 해결해줄 수 있는 것이 열거형 타입인 enum이 되겠다. 바로 적용된 버전의 코드를 보자.

enum Weapon
{
	WEAPON_SWORD,
	WEAPON_HAMMER,	// 마지막 쉼표 있어도 된다.
};

int computeDamage(int weapon_id)
{
	if (weapon_id == WEAPON_SWORD)
	{
		return 1;
	}

	if (weapon_id == WEAPON_HAMMER)
	{
		return 2;
	}

	// ...
}

아까보다 주석이 줄었으며, 가독성도 좋아졌다. enum의 경우 직접 수치를 입력하지 않을 경우 0부터 1씩 커지는 숫자가 enum 안에 들어간다. 여기서 WEAPON_SWORD가 0이고 다음이 1이 들어가 있다. enum은 헤더에 넣어두고 include하는 방식으로 사용하며, enum 마지막엔 ;으로 끝맺음을 해줘야 한다.

 

열거형 타입을 반드시 0부터 시작되게 사용하지 않아도 된다. 프로그래머가 값을 입력하면 그 값을 입력한 다음 항목에 대해서는 1씩 증가하게 구현이 된다.

#include <iostream>
#include <typeinfo>

enum Color
{
	COLOR_BLACK = -3,
	COLOR_RED,
	COLOR_BLUE = 5,
	COLOR_GREEN,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	// 초기화 방식은 다른 타입들과 같다.
	Color paint = COLOR_BLACK;
	Color house(COLOR_BLUE);
	Color apple{ COLOR_RED };

	Color my_color = COLOR_BLACK;

	cout << my_color << " " << COLOR_BLACK << endl;

	return 0;

위의 예시는 순서대로 -3, -2, 5, 6, 7이 할당된다. 이처럼 사용자가 할당할 수는 있지만 권장하지 않는 방식이다.


4.7.3 열거형 타입 사용 시 주의사항

 

하나의 enum에 같은 수를 배정할 수 있지만 의도와는 다르게 작동할 수 있다. 아래의 예시를 수행해보면 조건문이 참으로 되고 두 개가 다른 항목이지만 같다고 판단하는 꼴이 된다.

#include <iostream>

enum Color
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE = 5,
	COLOR_GREEN = 5,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	if (COLOR_BLUE == COLOR_GREEN)
		cout << "equal" << endl;

	return 0;
}

 

다른 enum일지라도 같은 이름을 가지면 안된다. 아래 예시처럼 int 자료형에도 저장이 가능하기 때문에 같은 이름인 BLUE를 저장할 경우 C2365 에러가 생긴다. 만약 같은 이름을 다른 열거형에서 사용하고싶다면 C++11부터 생긴 enum class를 활용하자.

#include <iostream>

enum Color
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
	COLOR_SKYBLUE,
	BLUE
};
enum Feeling
{
	HAPPY,
	JOY,
	TIRED,
	BLUE,
};

int main()
{
	using namespace std;

	int color_id = BLUE;

	cout << color_id << endl;

	return 0;
}

BLUE가 중복되었다.

 

위에서처럼 int 자료형에 enum을 넣는 것은 가능하지만 반대는 안된다. 아래 주석 처리된 부분은 바로 에러가 뜬다. static_cast를 사용하면 넣을 수 있겠지만 애초에 숫자로 넣는 모호한 방식을 사용하지 않기 위해 사용된 것이 열거형이므로 이런 식으로 사용하는 경우는 없을 것으로 생각된다.

enum Color
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	int color_id = COLOR_BLUE;
	cout << color_id << endl;

	// 그냥 넣는건 안된다. 애초에 안그러기 위해 만들어진게 enum
	//Color test = color_id;
	//Color test = 3;

	Color my_color = static_cast<Color>(3);

	return 0;
}

 

cin으로 바로 enum 자료형에 값을 넣는 것은 안된다. 입력 받으려면 우회해야 한다. 입력을 정수로 받고 해당 정수와 enum의 숫자를 일치하는지 조건을 체크하여 구현하는 방법이 최선일 것이다.

#include <iostream>
#include <typeinfo>

enum Color // user-defined data types
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	Color my_color = static_cast<Color>(3);

	//cin >> my_color;

	// 우회하는 방법
	int in_number;
	cin >> in_number;

	if (in_number == 0) my_color = COLOR_BLACK;
	// ...

	return 0;
}
  • 4.6.1 C 스타일 문자열
  • 4.6.2 string library header
  • 4.6.3 string 자료형 사용 시 유의사항

4.6.1 C 스타일 문자열

 

이미 C 언어를 학습한 사람이라면 C 스타일의 문자열을 다룬 기억이 있을 것이다. C 스타일 문자열은 char 자료형을 배열로 지니는 형태이며, 문자열이 담길 경우 마지막에 널 문자('\0')가 들어간다.

#include <iostream>

int main()
{
	using namespace std;

	// 커서를 가져다 올리면 13 길이 배열이라고 표시됨.
	cout << "Hello, World" << endl;
	const char my_strs[] = "Hello, World";

	return 0;
}

문자열의 길이를 확인해보면 12가 아닌 13이라는 것을 알 수 있다.

보이는 길이보다 1 길다.


4.6.2 string library header

 

C++에서는 string 라이브러리를 활용하여 문자열을 다룬다. string은 프로그래머의 편의를 위해 만들어진 사용자 정의 자료형으로 볼 수 있다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	const string my_hello = "Hello, World";
	//const string my_hello("Hello, World");
	//const string my_hello{ "Hello, World" };

	cout << my_hello << endl;

	return 0;
}

입력받는 방법은 위에 나열된 3가지 모두 가능하다.

 

숫자를 문자열로 취급하여 받을 수도 있는데, string은 정수를 암시적으로 형 변환을 해주진 않는다. 위에서 언급하였듯이 너무 친숙하여 기본 자료형처럼 보이지만 사용자 정의 자료형이기 때문에 암시적 형 변환이 불가능하다.

#include <string>

int main()
{
	using namespace std;

	// ""를 삭제하고 넣으면 에러가 뜬다.
	const string my_ID = "123";

	return 0;
}

 

또한 string 자료형은 + 연산자에 대한 오버로딩이 구현되어 있어서 문자열을 이어주는 것도 쉽게 수행할 수 있다. 이처럼 문자열을 붙여주는 것을 append라고 하며, 프로그래밍을 공부하다 보면 자주 만나게 될 용어이다. string으로 표현하는 문자열과 C 스타일 문자열의 가장 큰 차이점은 바로 문자열의 길이이다. string 문자열은 널 문자를 가지고 있지 않기 때문에 같은 문자열이라도 길이가 C 스타일 문자열보다 1 짧게 된다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	string a("Hello, ");
	string b("World");

	string hw = a + b; // append

	// 12 길이(C 스타일과 길이가 1 차이남.)
	cout << hw.length() << endl;

	hw += " I'm good";
	cout << hw << endl;


	return 0;
}

4.6.3 string 자료형 사용 시 유의사항

 

string을 입력 받을 때는 생각했던 대로 작동하지 않는 경우가 종종 있다. 다음의 예시들을 보자. 아래 예시에서 name 부분에 입력할 문자열을 띄어쓰기가 포함된 문자열을 입력해보자.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	cout << "Your name ? : ";
	string name;
	cin >> name; // 빈칸이 있으면 이미 다받은 것으로 판단.

	cout << "Your age ? : ";
	string age;
	cin >> age;

	cout << name << " " << age << endl;

	return 0;
}

출력도 해주지 않고 끝나버린다.

문자열을 입력할 때 cin은 띄어쓰기를 만나는 순간 입력이 끝난 줄 알고 저장해준 뒤 다음으로 넘어간다. 이를 해결하는 방법은 여러 가지가 있는데, 그중 하나는 바로 getline을 활용하는 것이다.

 

getline은 enter를 치기 전까지 전부 입력을 받아준다. 이를 이용하면 문자열에 띄어쓰기가 있는 것까지 온전히 받아낼 수 있다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	cout << "Your name ? : ";
	string name;
	getline(cin, name);

	cout << "Your age ? : ";
	string age;
	getline(cin, age);

	cout << name << " " << age << endl;

	return 0;
}

 

정수를 먼저 받으면 생기는 오작동도 있다. 이번에는 나이를 먼저 받게 구현을 하면 나이를 입력하자마자 바로 프로그램이 종료되는 것을 볼 수가 있다. int 자료형에 저장하는 경우엔 getline을 사용할 수 없게 되면서 cin으로 사용할 수밖에 없게 되고 숫자를 입력 후 enter를 치는 행위가 getline을 통과하는 조건으로 수행되면서 이러한 문제가 생긴다.

#include <iostream>
#include <string>
#include <limits>

int main()
{
	using namespace std;

	cout << "Your age ? : ";
	int age;
	cin >> age;
	
	// 해결법
	//cin.ignore(32767, '\n'); // 2 byte 가장 큰 signed 정수 32767
	//cin.ignore(numeric_limits<streamsize>::max(), '\n');

	cout << "Your name ? : ";
	string name;
	getline(cin, name); // enter칠때까지 입력 받을 수 있음.

	cout << name << " " << age << endl;

	return 0;
}

이를 해결하기 위해 ignore라는 함수를 활용할 수 있다. ignore 함수의 첫 번째 인자가 입력 받는 길이가 되며, 두 번째 인자가 입력받으면 끝낼 문자가 되겠다. 입력받는 수가 얼마나 길지 모르기 때문에 2byte 가장 큰 signed 정수인 32767을 넣어주었다. 32767이라는 남들이 보기에 이해 못 할 수 있는 숫자를 넣는 것은 다른 사람들을 힘들게 하기 때문에 limit 라이브러리에 있는 streamsize max를 이용해도 되겠다.

  • 4.5.1 typeinfo library header
  • 4.5.2 암시적 형 변환(Implicit Type Conversion(coersion))
  • 4.5.3 명시적 형 변환(Explicit Type Conversion(casting))

4.5.1 typeinfo library header

 

형 변환에 대해서 다루기 전에 유용한 라이브러리 하나를 소개하고자 한다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	cout << typeid(0.0).name() << endl;
	cout << typeid(0).name() << endl;

	bool variable = true;
	cout << typeid(variable).name() << endl;

	return 0;
}

typeinfo 라이브러리에 있는 함수 중 하나인 typeid().name()는 변수나 리터럴이 어떤 타입인지 알려주는 함수이다. 형 변환하면서 제대로 형 변환이 되었는지 체크하기 위해 쓰면 유용한 함수이다.


4.5.2 암시적 형 변환(Implicit Type Conversion(coersion))

 

암시적 형 변환은 컴파일러가 알아서 강제로 형 변환을 해주며, 프로그래머가 아무것도 하지 않아도 자동으로 수행하는 것이다. 아래의 예제에서는 프로그램이 실행되지만 다음과 같은 에러 메시지가 뜬다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	int a = 123.0;
    
	cout << typeid(a).name() << endl;

	return 0;
}
warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data

C4244 에러는 데이터 손실이 있다고 언급을 하지만 프로그램이 실행되도록 둔다. 개발자가 실수로 값을 잘못 넣는 경우에 발생하니 발견하면 해당 변수에 대해 파악을 하고 수정해두는 것이 좋겠다.

 

빌드는 되지만 신경 쓰이는 에러 메시지가 뜬다.

 

암시적 형변환 중 numeric promotion이라 불리는 형 변환이 있다. numeric promotion은 작은 메모리를 가지는 데이터가 큰 메모리에 담기는 경우를 의미한다. 아래의 예시는 float 자료형이 double로 numeric promotion되었다. 우리말로 숫자 승격이라고 쓸 수 있겠다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	float	f = 1.0f;
	double	d = f;

	cout << typeid(f).name() << endl;
	cout << typeid(d).name() << endl;

	return 0;
}

 

또 다른 암시적 형 변환으로 numeric conversion이 존재한다. 숫자 변환이란 의미를 가지는 이 암시적 형 변환은 서로 다른 자료형 간에 변환이나 큰 메모리를 가지는 데이터가 작은 메모리에 담기는 경우를 말한다. 큰 메모리에 담긴 데이터일지라도 작은 메모리가 감당 가능한 범위의 값이라면 컴파일러가 충분히 처리해준다.

#include <iostream>
#include <typeinfo>
#include <iomanip>

int main()
{
	using namespace std;

	float	d = 3;
	short	s = 2;
	
	cout << typeid(d).name() << endl;
	cout << typeid(s).name() << endl;

	int		i = 30000; // short 범위를 넘어가면 문제지만 안넘어가면 괜찮다.
	char		c = i;

	cout << static_cast<int>(c) << endl; // 48

	double	dd = 0.123456789;
	float		ff = dd;

	cout << setprecision(12) << dd << endl;
	cout << setprecision(12) << ff << endl;

	return 0;
}

4.5.3 명시적 형 변환(Explicit Type Conversion(casting))

 

명시적 형 변환은 프로그래머가 변환하겠다는 강력한 의사표현을 하는 것이다. 아래와 같이 C 스타일과 C++ 스타일, 그리고 최근 자주 쓰는 스타일이 있다.

int main()
{
	using namespace std;

	int i = int(4.0);			// c++ style
	i = (int)4.0;			// c style
	i = static_cast<int>(4.0);	// 최신
	
	return 0;
}

'Programming Language > C++' 카테고리의 다른 글

Section 4.7. 열거형 타입 enum  (0) 2021.11.28
Section 4.6. string 문자열 사용  (0) 2021.11.27
Section 4.4. using namespace  (0) 2021.11.13
Section 4.3. 외부 연결  (0) 2021.11.11
Section 4.2. 전역 변수와 정적 변수  (0) 2021.11.10

+ Recent posts