열거형 타입 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를 이용해도 되겠다.

+ Recent posts