공용체
공용체 (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) 기록을 더 볼 수 있어요.