공용체

공용체 (Union)

union한 번에 비정적 데이터 멤버 중 하나만 담을 수 있는 특별한 클래스 타입이에요. 같은 메모리 공간을 여러 용도로 번갈아 쓰고 싶을 때 유용해요. 표준 라이브러리의 std::variant처럼 타입 안전한 방법이 생기기 전까지는 C에서 물려받은 저수준적인 선택지를 제공하는 방법이죠.

출처: cppreference

본문

문법

공용체 선언의 클래스 지정자는 class나 struct 선언과 비슷해요.

union attr(optional) class-head-name(optional) { member-specification }
구성 요소 설명
attr -
class-head-name -
member-specification -

공용체는 멤버 함수(생성자·소멸자 포함)를 가질 수 있지만, 가상 함수는 가질 수 없어요.

공용체는 기반 클래스를 가질 수 없고, 기반 클래스로 쓰일 수도 없어요.

| 최대 한 개의 variant 멤버만 기본 멤버 초기화기(default member initializer)를 가질 수 있어요. | (since C++11) |

공용체는 참조 타입의 비정적 데이터 멤버를 가질 수 없어요.

공용체는 비자명(non-trivial) 특수 멤버 함수를 가진 비정적 데이터 멤버를 포함할 수 없어요. (until C++11)
공용체가 비자명 특수 멤버 함수를 가진 비정적 데이터 멤버를 포함하면, 공용체의 해당 특수 멤버 함수는 deleted로 정의될 수 있어요. 자세한 내용은 해당 특수 멤버 함수 문서를 보세요. (since C++11)

struct 선언과 마찬가지로, 공용체의 기본 멤버 접근은 public이에요.

설명

공용체는 가장 큰 데이터 멤버를 담을 만큼은 반드시 커야 하고, 보통 그보다 크지는 않아요. 나머지 데이터 멤버들은 그 가장 큰 멤버의 일부로서 같은 바이트에 할당되는 것으로 의도돼요. 그 할당의 세부 사항은 구현 정의(implementation-defined)인데, 모든 비정적 데이터 멤버가 같은 주소를 가진다는 점만은 예외예요. 가장 최근에 쓰이지 않은 멤버에서 읽는 것은 **정의되지 않은 동작(undefined behavior)**이에요. 많은 컴파일러가 비표준 언어 확장으로 비활성 멤버 읽기를 지원하긴 해요.

#include <cstdint>
#include <iostream>

union S
{
    std::int32_t n;     // occupies 4 bytes
    std::uint16_t s[2]; // occupies 4 bytes
    std::uint8_t c;     // occupies 1 byte
};                      // the whole union occupies 4 bytes

int main()
{
    S s = {0x12345678}; // initializes the first member, s.n is now the active member
    // At this point, reading from s.s or s.c is undefined behavior,
    // but most compilers define it.
    std::cout << std::hex << "s.n = " << s.n << '\n';

    s.s[0] = 0x0011; // s.s is now the active member
    // At this point, reading from s.n or s.c is undefined behavior,
    // but most compilers define it.
    std::cout << "s.c is now " << +s.c << '\n' // 11 or 00, depending on platform
              << "s.n is now " << s.n << '\n'; // 12340011 or 00115678
}

가능한 출력:

s.n = 12345678
s.c is now 0
s.n is now 115678

각 멤버는 마치 클래스의 유일한 멤버인 것처럼 할당돼요.

공용체의 멤버가 사용자 정의 생성자와 소멸자를 가진 클래스라면, 활성 멤버를 바꾸려면 보통 **명시적 소멸자 호출과 배치 new(placement new)**가 필요해요.

#include <iostream>
#include <string>
#include <vector>

union S
{
    std::string str;
    std::vector<int> vec;
    ~S() {} // needs to know which member is active, only possible in union-like class
};          // the whole union occupies max(sizeof(string), sizeof(vector<int>))

int main()
{
    S s = {"Hello, world"};
    // at this point, reading from s.vec is undefined behavior
    std::cout << "s.str = " << s.str << '\n';
    s.str.~basic_string();
    new (&s.vec) std::vector<int>;
    // now, s.vec is the active member of the union
    s.vec.push_back(10);
    std::cout << s.vec.size() << '\n';
    s.vec.~vector();
}

출력:

s.str = Hello, world
1

이 예시는 (since C++11)부터 성립해요.

두 공용체 멤버가 표준 레이아웃(standard-layout) 타입이면, 어떤 컴파일러에서든 그들의 공통 부분열(common subsequence)을 조사하는 것은 정의된 동작이에요.

공용체 멤버의 수명(lifetime)은 그 멤버가 활성화될 때 시작돼요. 이전에 다른 멤버가 활성화되어 있었다면 그 멤버의 수명은 끝나요.

활성 멤버가 E1 = E2 형태의 대입 표현식(내장 대입 연산자 또는 자명(trivial) 대입 연산자를 사용)으로 전환될 때, E1의 멤버 접근·배열 첨자 하위 표현식에 나타나는 각 공용체 멤버 X가 비자명하거나 deleted인 기본 생성자를 가진 클래스가 아니고, 타입 별명 규칙에 따라 X의 수정이 정의되지 않은 동작이 된다면, X 타입의 객체가 지정된 저장소에 암시적으로 생성돼요. 초기화는 수행되지 않고, 그 수명의 시작은 좌·우 피연산자의 값 계산 이후, 대입 이전에 배치돼요.

union A { int x; int y[4]; };
struct B { A a; };
union C { B b; int k; };

int f()
{
    C c;               // does not start lifetime of any union member
    c.b.a.y[3] = 4;    // OK: "c.b.a.y[3]", names union members c.b and c.b.a.y;
                       // This creates objects to hold union members c.b and c.b.a.y
    return c.b.a.y[3]; // OK: c.b.a.y refers to newly created object
}

struct X { const int a; int b; };
union Y { X x; int k; };

void g()
{
    Y y = {{1, 2}}; // OK, y.x is active union member
    int n = y.x.a;
    y.k = 4;   // OK: ends lifetime of y.x, y.k is active member of union
    y.x.b = n; // undefined behavior: y.x.b modified outside its lifetime,
               // "y.x.b" names y.x, but X's default constructor is deleted,
               // so union member y.x's lifetime does not implicitly start
}

공용체 타입의 자명한 이동 생성자, 이동 대입 연산자, (since C++11) 복사 생성자와 복사 대입 연산자는 객체 표현(object representation)을 복사해요. 원본과 대상이 같은 객체가 아니라면, 이 특수 멤버 함수들은 복사가 수행되기 전에 대상 안의 각 중첩 객체 중 원본에 대응하는 것들의 수명을 시작해요. 그렇지 않으면 아무것도 하지 않아요. 자명한 특수 함수를 통한 생성·대입 이후 두 공용체 객체는 대응하는 활성 멤버(있다면)가 같아요.

**익명 공용체(anonymous union)**란 변수를 동시에 정의하지 않는(공용체 타입의 객체, 참조, 포인터 포함) 이름 없는 공용체 정의를 말해요.

union { member-specification };

익명 공용체에는 추가 제약이 있어요. 멤버 함수를 가질 수 없고, 정적 데이터 멤버를 가질 수 없으며, 모든 데이터 멤버가 public이어야 해요. 허용되는 선언은 비정적 데이터 멤버와 static_assert 선언뿐이에요. (since C++11)

익명 공용체의 멤버들은 둘러싸는 스코프로 주입돼요(그리고 거기 선언된 다른 이름과 충돌하면 안 돼요).

int main()
{
    union
    {
        int a;
        const char* p;
    };
    a = 1;
    p = "Jennifer";
}

네임스페이스 스코프의 익명 공용체는 익명 네임스페이스에 있지 않다면 static으로 선언해야 해요.

union-like 클래스는 공용체이거나, 적어도 하나의 익명 공용체를 멤버로 가진 (비공용체) 클래스예요. union-like 클래스는 다음과 같은 variant 멤버 집합을 가져요.

  • 멤버인 익명 공용체들의 비정적 데이터 멤버
  • 추가로, union-like 클래스가 공용체라면, 익명 공용체가 아닌 비정적 데이터 멤버들

union-like 클래스는 **태그 붙은 공용체(tagged union)**를 구현하는 데 쓸 수 있어요.

#include <iostream>

// S has one non-static data member (tag), three enumerator members (CHAR, INT, DOUBLE), 
// and three variant members (c, i, d)
struct S
{
    enum{CHAR, INT, DOUBLE} tag;
    union
    {
        char c;
        int i;
        double d;
    };
};

void print_s(const S& s)
{
    switch(s.tag)
    {
        case S::CHAR: std::cout << s.c << '\n'; break;
        case S::INT: std::cout << s.i << '\n'; break;
        case S::DOUBLE: std::cout << s.d << '\n'; break;
    }
}

int main()
{
    S s = {S::CHAR, 'a'};
    print_s(s);
    s.tag = S::INT;
    s.i = 123;
    print_s(s);
}

출력:

a
123
C++ 표준 라이브러리에는 공용체와 union-like 클래스의 많은 용도를 대신할 수 있는 std::variant가 있어요. 위 예시는 다음과 같이 다시 쓸 수 있어요.
#include <iostream>
#include <variant>

int main()
{
    std::variant<char, int, double> s = 'a';
    std::visit([](auto x){ std::cout << x << '\n';}, s);
    s = 123;
    std::visit([](auto x){ std::cout << x << '\n';}, s);
}

출력:

a
123

이 예시는 (since C++17)부터 성립해요.

키워드

union

참고

기능 테스트 매크로 표준 기능
__cpp_trivial_union 202603L (C++26) 자명 공용체(trivial union)

더 알아보기

  • std::variant는 타입 안전한 구분 공용체(discriminated union)로, 공용체를 대신하는 현대적인 방법이에요.
  • 공용체의 멤버 함수·가상 함수 제약과 특수 멤버 함수의 deleted 정의 규칙은 관련 문서에서 이어서 다뤄요.
  • cppreference의 공용체 원문에서 결함 보고(defect report) 기록을 더 볼 수 있어요.