Introduction
C 언어 프로그래밍에 익숙하신 분들은 enum을 한 번쯤은 들어보셨을 겁니다. 저도 참 유용하게 사용했던 기억이 있는데요. C++에서는 기존 enum의 문제점들을 보완한 enum class를 주로 사용한다는 사실을 접하게 되었습니다. 그러면서 또다시 충격을 받았죠.
역시 난 C++을 C처럼 사용하고 있었어!
이번 포스트에서는 기존 C style enum의 문제점들을 짚어보고, C++에서 권장되는 enum class는 무엇이 다른지 살펴볼 것입니다.
C style enum, what's wrong?
아래는 우리가 이미 친숙한 C 스타일의 enum(unscoped-enum) 사용법입니다. 사용자 정의 타입으로 enum 선언을 통해 원하는 타입을 정의할 수 있습니다.
#include <iostream>
using namespace std;
enum Color { RED, BLUE };
int main() {
Color color = RED;
if (color == RED) cout << "color is red\n";
return 0;
}
우리는 enum을 사용함으로써 다음의 이득을 얻을 수 있습니다.
- 코드의 가독성을 높여 소스코드에 대한 이해를 도울 수 있습니다.
- 코드 사이즈가 큰 경우 한 곳에서 관련된 값들을 확인할 수 있습니다.
- enum type 변수에는 enum argument만 대입 가능해 실수를 방지합니다.
- 숫자가 아닌 문자열을 value에 mapping 해 시간이 지난 후에도 기억이 가능합니다.
그런데 이렇게 좋은 C style enum에도 몇 가지 문제가 있습니다. 문제점들을 말하기에 앞서, C++에서의 enum의 역사를 먼저 짚고 넘어가도록 하겠습니다. C++에도 역시 enum이 존재합니다. C 언어에서 넘어와 C++98까지는 완전히 같은 문법으로 존재했습니다. 그러나 이제부터 말할 몇 가지 문제점들 때문에 이를 개선한 enum class가 C++의 사용자 정의 타입의 표준으로 권장되고 있습니다.
- 열거자들이 열거형과 같은 유효 범위(namespace)를 갖습니다.
이는 다른 열거형에서 같은 이름의 열거자를 사용할 수 없음을 의미합니다. 이 경우 compiler마다 처리 방식이 다를 수 있습니다. 우선, 재정의를 해버리는 compiler가 있을 수 있으며, 제 경우 에러 메시지를 출력하며 컴파일이 되지 않습니다. 아래는 같은 이름을 갖는 열거자들이 다른 열거형에 있는 경우와 이를 해결하기 위한 C style guide입니다.
// Error case
// redefinition of enumerator 'RED', 'BLUE'
enum Color { BLACK, WHITE, RED, BLUE };
enum RGB { RED, GREEN, BLUE };
// Sol #1
// modify the name related to the enum type
enum Color { cBLACK, cWHITE, cRED, cBLUE };
enum RGB { rgbRED, rgbGREEN, rgbBLUE };
// Sol #2
// differentiate two enum type using keyword 'namespace'
namespace Color {
enum Enum { BLACK, WHITE, RED, BLUE };
}
namespace RGB {
enum Enum { RED, GREEN, BLUE };
}
int main() {
Color::Enum color = Color::RED;
if (color == Color::RED) cout << "color is red\n";
return 0;
}
Sol #1은 이름을 바꾸는 것입니다. 하지만 이렇게 되면 각각의 열거자들의 이름이 굉장히 더러워지겠죠. Sol #2는 좀 더 깔끔하게 열거자들을 정리할 수 있습니다. 바로 namespace를 이용하는 방법인데요. 모든 열거형들이 같은 namespace에 있는 게 문제라면 다른 namespace를 할당해 주자! 이게 철학인 방법이죠. 하지만 이 방법은 처음 선언하는 과정이 꽤나 번거롭습니다. 그리고 namespace와 enum의 이름이 각각 존재해 좀 귀찮습니다. 이런 문제는 추후 소개드릴 enum class에서 개선됩니다.
- 열거자가 암시적으로 정수로 변환됩니다.
앞에서 enum의 장점으로 열거형 변수에는 열거자만 대입할 수 있어 실수를 방지한다고 했습니다. 하지만 반대로 열거형 변수가 아닌 일반 변수에는 열거자를 대입할 수 있습니다. 이게 어떻게 가능할까요? 블로그의 다른 포스트에서 type casting을 다룬 적이 있었는데, 그중 암시적 변환은 별도의 선언 없이 compiler가 묵시적으로 형 변환을 해버립니다.
#include <iostream>
using namespace std;
enum Color { BLACK, WHITE, RED, BLUE };
int main() {
int myInt = BLACK;
cout << myInt;
return 0;
}
이를 활용해 프로그래머는 많은 꼼수를 쓸 수 있습니다. 대신, Human Error가 발생할 확률이 커졌습니다. 위에서 예로 든 코드 역시 프로그래머가 의도적으로 열거자를 대입했다고 생각하기 쉽지 않습니다. 무언가 다른 값을 자동완성을 통해 대입하려 했으나, 잘못된 값이 완성됐다고 추측하는 게 오히려 더 자연스럽습니다. 문제는 compiler가 이런 Human Error를 잡아주지 않는다는 것입니다. 이럴 경우 심하면 line by line debugging을 해야 할 수도 있는 거죠.
형 변환에서도 이미 알 수 있듯이 C style expression들은 프로그래머들에게 정말 이래도 되나 싶을 정도로 높은 자유도를 부여합니다. 그래서 수많은 꼼수를 통해 flexible 한 코딩이 가능하죠. 반대로 코드의 가독성이나 의도를 파악하는 것은 점점 어려워집니다. 또한, 의도를 알지 못한 채 수정이 필요한 경우나 꼼꼼하지 못한 사람이 작업을 할 때에는 높은 확률로 Human Error가 발생할 수 있습니다.
C++은 이런 부분들을 점진적으로 배제하려 하는 움직임을 보여왔으며, enum class의 등장 역시 그 일환 중 하나였습니다.
enum class
C++11부터 등장한 enum class(scoped-enum)는 기존 enum의 문제점들을 보완하는데 주력합니다. 그렇기에 가장 큰 특징이 C style enum의 문제점들과 1대 1 대응이 될 수밖에 없죠. 우선 enum class의 syntax부터 살펴보고, 특징을 하나씩 살펴보도록 하겠습니다.
- enum class syntax
enum class name { enumerator = constexpr , enumerator = constexpr , ... };
cppreference.com에서는 enum class 선언법을 위와 같이 정의하고 있습니다. 기존 enum 대비 달라진 점이라면 class keyword가 추가되었다? 이 정도입니다. 반면에 열거자를 호출할 때는 [열거형 이름]::[열거자 이름]으로 호출을 해야 합니다. 마치 enum을 namespace와 같이 사용할 때와 같죠. 이와 관련해 아래 특징에서 보다 자세히 살펴보도록 하겠습니다.
- 열거자 이름에 중복이 허용됩니다.
C style enum과 가장 큰 차이점은 열거자 이름에 대해 중복이 허용된다는 점입니다. 왜냐하면 모든 열거형 타입들이 같은 namespace에 존재했던 기존과 달리 각각의 enum class는 독자적인 namespace를 할당받습니다. 그렇기 때문에 의도치 않은 재정의 나 오류를 방지할 수 있으며, 열거자 이름에 대한 고민을 할 필요가 없어졌습니다. (위 예제의 Sol#1)
#include <iostream>
using namespace std;
enum class Color { BLACK, WHITE, RED, BLUE };
enum class RGB { RED, GREEN, BLUE };
int main() {
Color color = Color::RED;
if (color == Color::RED) cout << "color is red\n";
return 0;
}
위 예제에서 Color와 RGB의 RED, BLUE가 중복된 이름을 갖고 있음에도 compiler는 경고나 오류를 출력하지 않습니다. 한 가지 더 살펴볼 내용은 열거자를 호출하는 방법입니다. Color::RED 와 같이 열거형 이름::열거자 이름 의 방법으로 사용할 수 있습니다. 독립적인 namespace를 사용하는 만큼 compiler의 추측이 아닌 명시적 호출을 통해 사용하도록 하고 있습니다.
- 타입 변환에 좀 더 까다롭습니다.
C style enum의 문제점 중 하나는 묵시적 형 변환이 이루어져 Human Error 발생 확률이 크다는 것이었습니다. 이러한 점을 enum class는 제약을 줌으로써 해결을 하는데요. 이제 더 이상 암시적 int type casting은 발생하지 않습니다. 개발자가 의도를 갖고 형 변환을 활용해 꼼수를 부리는 경우, 직접 명시적으로 type casting을 해주어야 합니다.
#include <iostream>
using namespace std;
enum class Color { BLACK, WHITE, RED, BLUE };
enum class RGB { RED, GREEN, BLUE };
// Error case example
// error: invalid operands to binary expression ('Color' and 'int')
int main() {
Color color = Color::RED;
if (color == 1) cout << "color is red\n";
return 0;
}
// Type casting using static_cast example
int main() {
Color color = Color::RED;
if (static_cast<int>(color) == 2) cout << "color is red\n";
return 0;
}
// Recommended usage of enum class
int main() {
Color color = Color::RED;
if (color == Color::RED) cout << "color is red\n";
return 0;
}
위 예제를 보면 enum class 변수를 정수 1과 비교하는 경우 에러 메시지를 출력하게 됩니다. 이 경우 명시적 형 변환 static_cast를 이용해 type casting을 하면 문제를 해결할 수 있죠. 이건 내가 의도적으로 사용하는 것이니 이로 인해 발생하는 문제는 내가 책임지겠다. 뭐 이런 메시지를 코드에 남기는 거라고 저는 생각합니다. 그런데 정말 저 방법들이 enum class를 잘 활용하는 것일까요? 위 예제에서 우리는 하나의 시나리오를 써볼 수 있습니다.
누군가 C style enum을 통해 legacy code를 짰고, 귀찮으니 정수와 비교를 했다고 추측을 해봅시다. 이번에 새로운 열거자 BLACK을 추가할 일이 생겨 작업을 하는 김에 새로운 enum class로 수정을 해보기로 했습니다. 막상 수정을 하니 기존에 정수와 비교하는 부분에서 오류를 뱉고 있죠. 오류를 수정했는데 이번에는 원하는 결괏값이 나오지 않습니다. 원소를 추가함으로 인해 RED의 순서가 밀렸기 때문에 발생한 문제였습니다. 그래서 우리는 1을 2로 수정해 원하는 결과를 얻었습니다.
그렇다면 이제 끝인 건가요? 앞으로 유지, 보수를 할 때마다 같은 시행착오를 반복할 것입니다. 이번 기회에 enum을 사용하는 본 이유인 열거자를 활용한 가독성 및 flexibility 확보를 진행해 봅시다. 앞으로 원소를 어떻게 추가하든 간에 RED를 지우지만 않으면, 이 코드는 돌아갈 것입니다.
- 전방 선언이 가능합니다.
전방 선언은 선언을 먼저 하고 정의를 나중에 하는 행위를 뜻합니다. C++에서 전방 선언은 꽤나 자주 활용됩니다. 특히 함수나 멤버 함수를 선언 및 구현할 때 많이 사용하죠. 이제 enum class에서도 해당 동작을 지원하는데, 이는 코드 가독성에 도움이 됩니다. 많은 부분을 전방 선언으로 처리할수록, 중요한 부분이 어디인지 쉽게 알 수 있습니다. 또한, C++의 끔찍한 compile 시간을 아시는 분은 쌍수를 들고 환영할만한 일이죠.
#include <iostream>
using namespace std;
enum class Color;
enum RGB : int;
int main() {
// Code
}
enum class Color { BLACK, WHITE, RED, BLUE };
enum RGB : int { GREEN };
물론, 기존의 enum에서도 전방 선언이 아예 불가능한 행위는 아니었습니다. 그러나 enum은 그 자체로는 compiler가 어느 정도의 메모리를 미리 할당받아야 하는지를 결정할 수 없어 전방 선언이 불가능했습니다. 그래서 전방 선언을 위해서는 예제처럼 type define을 해주어야 합니다. 나 이만큼만 사용할 테니 메모리 미리 할당해 줘! Compiler에게 메시지를 전달하는 거죠.
반대로 enum class는 이러한 underlying type이 int로 설정되어 있어 별도의 type define이 없어도 전방 선언이 가능합니다. 물론 int 이외의 type으로 사용하고 싶으면 type define을 해주면 됩니다.
Reference