열거형

열거형 (Enumeration)

값이 특정 범위(아래에서 자세히 설명)로 제한되는 별개의 타입을 열거형이라고 해요. 여러 개의 이름 붙은 상수("열거자(enumerator)")를 포함할 수 있죠. 열거자들의 값은 열거형의 **기반 타입(underlying type)**이라고 불리는 정수 타입의 값이에요.

출처: cppreference

본문

열거형은 기반 타입과 같은 크기, 값 표현, 정렬 요구 사항을 가져요. 게다가 열거형의 각 값은 기반 타입의 대응하는 값과 같은 표현을 가져요.

열거형은 다음 문법으로 (재)선언돼요:

enum-key attr (optional) enum-head-name (optional) enum-base (optional)
{ enumerator-list (optional) }                       (1)
enum-key attr (optional) enum-head-name (optional) enum-base (optional)
{ enumerator-list , }                                (2)
enum-key attr (optional) enum-head-name enum-base (optional) ;  (3)  (C++11)
  • enum-key — C++11 이전에는 enum만, C++11 이후에는 enum, enum class, enum struct 중 하나.
  • attr — (C++11 이후) 임의 개수의 속성 시퀀스.
  • enum-head-name — 선언되는 열거형의 이름. C++11 이전에는 생략 가능. C++11 이후에는 이름 앞에 중첩 이름 지정자(nested-name-specifier, ::로 끝나는 이름 시퀀스)가 올 수 있고, 스코프 없는 비-불투명 열거형 선언에서만 생략 가능.
  • enum-base — (C++11 이후) 콜론(:) 뒤에 오는 정수 타입을 가리키는 type-specifier-seq. 이 타입이 열거형의 고정 기반 타입이 돼요 (cv 한정은 무시됨).
  • enumerator-list — 쉼표로 구분된 열거자 정의 목록. 각각은 고유 식별자, 또는 고유 식별자에 상수 표현식(identifier = constant-expression)을 붙인 것.
  1. enum-specifier — 선언 문법의 decl-specifier-seq에 나타나며 열거형 타입과 그 열거자들을 정의해요.
  2. enumerator-list 뒤에 후행 쉼표가 올 수 있어요.
  3. 불투명 열거형 선언 — 열거형 타입은 정의하지만 열거자는 정의하지 않아요. 이 선언 후에는 타입이 완전한 타입이 되고 크기를 알 수 있어요.

스코프 없는 열거형 (Unscoped enumerations)

enum name (optional) { enumerator = constant-expression , ... }      (1)
enum name (optional) : type { enumerator = constant-expression , ... } (2)  (C++11)
enum name : type ;                                                    (3)  (C++11)
  1. 기반 타입이 고정되지 않은 스코프 없는 열거형을 선언해요. 이 경우 기반 타입은 모든 열거자 값을 표현할 수 있는 구현 정의 정수 타입이에요. 이 타입은 열거자 값이 intunsigned int에 못 들어가지 않으면 int보다 크지 않아요. enumerator-list가 비어 있으면, 마치 값 0인 열거자가 하나 있는 것처럼 기반 타입이 정해져요. 모든 열거자 값을 표현할 정수 타입이 없다면 열거형은 ill-formed예요.
  2. 기반 타입이 고정된 스코프 없는 열거형을 선언해요.
  3. 스코프 없는 열거형의 불투명 선언은 이름과 기반 타입을 모두 지정해야 해요.

각 열거자는 열거형 타입의 이름 붙은 상수가 되어 둘러싼 스코프에 보이게 되고, 상수가 필요한 곳이면 어디서든 쓸 수 있어요.

enum Color { red, green, blue };
Color r = red;

switch(r)
{
    case red  : std::cout << "red\n";   break;
    case green: std::cout << "green\n"; break;
    case blue : std::cout << "blue\n";  break;
}

각 열거자는 기반 타입의 값 하나와 연관돼요. enumerator-list에 =가 주어지면 그 연관 상수 표현식으로 값이 정해져요. 첫 번째 열거자에 =가 없으면 연관 값은 0이에요. 다른 열거자에 =가 없으면 이전 열거자의 값 +1이 돼요.

enum Foo { a, b, c = 10, d, e = 1, f, g = f + c };
//a = 0, b = 1, c = 10, d = 11, e = 1, f = 2, g = 12

스코프 없는 열거형의 이름은 생략할 수 있어요. 그런 선언은 둘러싼 스코프에 열거자만 도입해요:

enum { a, b, c = 0, d = a + 2 }; // defines a = 0, b = 1, c = 0, d = 2

스코프 없는 열거형이 클래스 멤버라면, 그 열거자는 클래스 멤버 접근 연산자 .->로 접근할 수 있어요:

struct X
{
    enum direction { left = 'l', right = 'r' };
};
X x;
X* p = &x;

int a = X::direction::left; // allowed only in C++11 and later
int b = X::left;
int c = x.left;
int d = p->left;

(C++11 이후) 멤버 선언의 선언 지정자 안에서, enum enum-head-name : 시퀀스는 항상 열거형 선언의 일부로 파싱돼요:

struct S
{
    enum E1 : int {};
    enum E1 : int {}; // error: redeclaration of enumeration,
                      // NOT parsed as a zero-length bit-field of type enum E1
};

enum E2 { e1 };

void f()
{
    false ? new enum E2 : int(); // OK: 'int' is NOT parsed as the underlying type
}

링크를 위한 열거형 이름

링크 목적의 typedef 이름이 없고 열거자가 하나 이상 있는 이름 없는 열거형은, 링크 목적상 기반 타입과 첫 번째 열거자로 표기돼요. 이런 열거형은 "링크 목적의 이름으로 열거자를 가진다"고 해요.

스코프 있는 열거형 (Scoped enumerations)

enum struct|class name { enumerator = constant-expression , ... }    (1)
enum struct|class name : type { enumerator = constant-expression , ... } (2)
enum struct|class name ;                                              (3)
enum struct|class name : type ;                                       (4)
  • classstruct 키워드는 정확히 동등해요.
  1. 기반 타입이 int인 스코프 있는 열거형을 선언해요.
  2. 기반 타입이 type인 스코프 있는 열거형을 선언해요.
  3. 기반 타입이 int인 스코프 있는 열거형의 불투명 선언.
  4. 기반 타입이 type인 스코프 있는 열거형의 불투명 선언.

각 열거자는 열거형 타입(즉 name)의 이름 붙은 상수가 되고, 열거형의 스코프 안에 포함되어 스코프 결정 연산자로 접근할 수 있어요. 스코프 있는 열거자 값에서 정수 타입으로의 암시적 변환은 없어요. 다만 static_cast로 열거자의 숫자 값을 얻을 수는 있어요.

#include <iostream>

int main()
{
    enum class Color { red, green = 20, blue };
    Color r = Color::blue;

    switch(r)
    {
        case Color::red  : std::cout << "red\n";   break;
        case Color::green: std::cout << "green\n"; break;
        case Color::blue : std::cout << "blue\n";  break;
    }

    // int n = r; // error: no implicit conversion from scoped enum to int
    int n = static_cast<int>(r); // OK, n = 21
    std::cout << n << '\n'; // prints 21
}

(C++11 이후)

열거형은 리스트 초기화를 통해 캐스트 없이 정수로부터 초기화될 수 있어요. 다음 조건을 모두 만족할 때 가능해요:

  • **직접 리스트 초기화(direct-list-initialization)**여야 함.
  • 초기화 목록에 단 하나의 요소만 있어야 함.
  • 열거형이 스코프 있는 것이거나, 기반 타입이 고정된 스코프 없는 것이어야 함.
  • 변환이 **비-내로잉(non-narrowing)**이어야 함.

이 덕분에 기반 정수 타입과 같은 기존 호출 규약을 누리는 새 정수 타입(예: SafeInt)을 도입할 수 있어요. 심지어 구조체를 값으로 전달/반환하는 것을 불리하게 여기는 ABI에서도요.

enum byte : unsigned char {}; // byte is a new integer type; see also std::byte (C++17)
byte b{42};        // OK as of C++17 (direct-list-initialization)
byte c = {42};     // error
byte d = byte{42}; // OK as of C++17; same value as b
byte e{-1};        // error

struct A { byte b; };
A a1 = {{42}};     // error (copy-list-initialization of a constructor parameter)
A a2 = {byte{42}}; // OK as of C++17

void f(byte);
f({42}); // error (copy-list-initialization of a function parameter)

enum class Handle : std::uint32_t { Invalid = 0 };
Handle h{42}; // OK as of C++17

(C++17 이후)

using enum 선언

using enum using-enum-declarator ;   (C++20)

선언자는 열거형 타입을 가리키는 (가능하면 한정된) 식별자 또는 단순 템플릿 식별자(C++26 이후에는 splice 타입 지정자)예요. 선언자는 비-의존적인 열거형 타입을 가리켜야 해요. 열거형 선언은 선언자가 한정인지에 따라 타입 전용(type-only) 보통의 한정/비한정 조회로 찾아요.

enum E { x };

void f()
{
    int E;
    using enum E; // OK
}

using F = E;
using enum F; // OK

template<class T>
using EE = T;

void g()
{
    using enum EE<E>; // OK
}

using enum 선언은 이름 붙은 열거형의 열거자 이름들을, 각 열거자에 대해 using 선언을 한 것처럼 도입해요. 클래스 스코프에 있으면, using enum 선언은 이름 붙은 열거형의 열거자들을 멤버로 그 스코프에 추가해서 멤버 조회로 접근 가능하게 해요.

enum class fruit { orange, apple };

struct S
{
    using enum fruit; // OK: introduces orange and apple into S
};

void f()
{
    S s;
    s.orange;  // OK: names fruit::orange
    S::orange; // OK: names fruit::orange
}

같은 이름의 열거자 두 개를 도입하는 using enum 선언 두 개는 충돌해요.

enum class fruit { orange, apple };
enum class color { red, orange };

void f()
{
    using enum fruit;    // OK
    // using enum color; // error: color::orange and fruit::orange conflict
}

(C++20 이후)

Notes

스코프 없는 열거형 타입의 값은 정수 타입으로 승격되거나 변환될 수 있어요:

enum color { red, yellow, green = 20, blue };
color col = red;
int n = blue; // n == 21

정수, 부동소수점, 열거형 타입의 값은 static_cast를 써서 아무 열거형 타입으로나 변환할 수 있어요. 변환 후의 값이 반드시 그 열거형에 정의된 이름 붙은 열거자 중 하나와 같을 필요는 없어요:

enum access_t { read = 1, write = 2, exec = 4 }; // enumerators: 1, 2, 4 range: 0..7
access_t rwe = static_cast<access_t>(7);
assert((rwe & read) && (rwe & write) && (rwe & exec));

access_t x = static_cast<access_t>(8.0); // undefined behavior since CWG 1766
access_t y = static_cast<access_t>(8);   // undefined behavior since CWG 1766

enum foo { a = 0, b = UINT_MAX }; // range: [0, UINT_MAX]
foo x = foo(-1); // undefined behavior since CWG 1766,
                 // even if foo's underlying type is unsigned int
기능 테스트 매크로 표준 기능
__cpp_enumerator_attributes 201411L (C++17) 열거자에 대한 속성
__cpp_using_enum 201907L (C++20) using enum

키워드

enum,
struct,
class,
using

예제

여기서 확인할 것: 기반 타입을 지정한 16비트 열거형, 값이 0부터 시작하지 않는 스코프 있는/없는 열거형, 이름 없는 열거형, 그리고 열거형 타입에 연산자를 오버로딩하는 패턴을 보는 코드예요.

#include <cstdint>
#include <iostream>

// enum that takes 16 bits
enum smallenum: std::int16_t
{
    a,
    b,
    c
};

// color may be red (value 0), yellow (value 1), green (value 20), or blue (value 21)
enum color
{
    red,
    yellow,
    green = 20,
    blue
};

// altitude may be altitude::high or altitude::low
enum class altitude: char
{
    high = 'h',
    low = 'l', // trailing comma only allowed after CWG 518
}; 

// the constant d is 0, the constant e is 1, the constant f is 3
enum
{
    d,
    e,
    f = e + 2
};

// enumeration types (both scoped and unscoped) can have overloaded operators
std::ostream& operator<<(std::ostream& os, color c)
{
    switch(c)
    {
        case red   : os << "red";    break;
        case yellow: os << "yellow"; break;
        case green : os << "green";  break;
        case blue  : os << "blue";   break;
        default    : os.setstate(std::ios_base::failbit);
    }
    return os;
}

std::ostream& operator<<(std::ostream& os, altitude al)
{
    return os << static_cast<char>(al);
}

// The scoped enum (C++11) can be partially emulated in earlier C++ revisions:

enum struct E11 { x, y }; // since C++11

struct E98 { enum { x, y }; }; // OK in pre-C++11

namespace N98 { enum { x, y }; } // OK in pre-C++11

struct S98 { static const int x = 0, y = 1; }; // OK in pre-C++11

void emu()
{
    std::cout << (static_cast<int>(E11::y) + E98::y + N98::y + S98::y) << '\n'; // 4
}

namespace cxx20
{
    enum class long_long_long_name { x, y };

    void using_enum_demo()
    {
        std::cout << "C++20 `using enum`: __cpp_using_enum == ";
        switch (auto rnd = []{return long_long_long_name::x;}; rnd())
        {
#if defined(__cpp_using_enum)
            using enum long_long_long_name;
            case x: std::cout << __cpp_using_enum << "; x\n"; break;
            case y: std::cout << __cpp_using_enum << "; y\n"; break;
#else
            case long_long_long_name::x: std::cout << "?; x\n"; break;
            case long_long_long_name::y: std::cout << "?; y\n"; break;
#endif
        }
    }
}

int main()
{
    color col = red;
    altitude a;
    a = altitude::low;

    std::cout << "col = " << col << '\n'
              << "a = "   << a   << '\n'
              << "f = "   << f   << '\n';

    cxx20::using_enum_demo();
}

가능한 출력:

col = red
a = l
f = 3
C++20 `using enum`: __cpp_using_enum == 201907; x

결함 보고 (Defect reports)

다음의 동작 변경 결함 보고는 이전에 발표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 377 C++98 모든 열거자 값을 표현할 정수 타입이 없을 때 동작이 미지정이었음 이 경우 열거형은 ill-formed
CWG 518 C++98 enumerator-list 뒤에 후행 쉼표가 허용되지 않았음 허용됨
CWG 1514 C++11 기반 타입이 고정된 열거형의 재정의가 클래스 멤버 선언에서 비트 필드로 파싱될 수 있었음 항상 재정의로 파싱됨
CWG 1638 C++11 불투명 열거형 선언의 문법이 템플릿 특수화에 대한 사용을 금지했음 nested-name-specifier 허용
CWG 1766 C++98 기반 타입이 고정되지 않은 열거형에 범위 밖 값을 캐스팅하는 결과가 미지정이었음 동작은 정의되지 않음
CWG 1966 C++11 CWG 1514의 해결로 조건 표현식의 :가 enum-base의 일부가 되었음 멤버 선언 지정자에만 적용
CWG 2156 C++11 열거형 정의가 using 선언으로 열거형 타입을 정의할 수 있었음 금지됨
CWG 2157 C++11 CWG 1966의 해결이 한정 열거형 이름을 다루지 않았음 다룸
CWG 2530 C++98 enumerator-list가 같은 식별자의 여러 열거자를 포함할 수 있었음 금지됨
CWG 2590 C++98 열거형의 크기, 값 표현, 정렬 요구 사항이 기반 타입에 의존하지 않았음 모두 기반 타입과 동일함
CWG 2621 C++20 using enum 선언에 사용된 열거형 이름 조회가 불명확했음 명확해짐
CWG 2877 C++20 using enum 선언에 사용된 열거형 이름 조회가 type-only가 아니었음 type-only가 됨

참고 문헌

  • C++23 표준 (ISO/IEC 14882:2024): 9.7.1 Enumeration declarations [dcl.enum]
  • C++20 표준 (ISO/IEC 14882:2020): 9.7.1 Enumeration declarations [dcl.enum]
  • C++17 표준 (ISO/IEC 14882:2017): 10.2 Enumeration declarations [dcl.enum]
  • C++14 표준 (ISO/IEC 14882:2014): 7.2 Enumeration declarations [dcl.enum]
  • C++11 표준 (ISO/IEC 14882:2011): 7.2 Enumeration declarations [dcl.enum]
  • C++03 표준 (ISO/IEC 14882:2003): 7.2 Enumeration declarations [dcl.enum]
  • C++98 표준 (ISO/IEC 14882:1998): 7.2 Enumeration declarations [dcl.enum]

더 알아보기 (Learn more)