공용체(union)는 구조체(struct)와 거의 동일하지만 모든 멤버 변수가 하나의 메모리 공간을 공유한다는 점이 다릅니다. 즉, union은 내부에 여러가지 타입의 멤버 변수를 선언하지만 실제 사용할 때에는 하나의 멤버 변수만 사용할 수 있습니다.
union과 struct의 차이를 아래의 예시를 보면서 살펴보겠습니다.
참고자료 및 이미지 출처
union은 struct와 유사하게 생겼으며, 선언하는 방식도 동일합니다.
하지만 union은 내부에서 여러 변수를 선언하여도 선언한 변수 중 하나의 변수만 저장할 수 있으며, 가장 크기가 큰 타입의 멤버 변수의 크기가 곧 union의 크기가 됩니다. 다음 예제의 경우 a, b, c 세 개의 멤버 변수를 선언하였지만 하나의 메모리 공간을 공유하기 때문에 a, b, c 중 하나만 사용해야 합니다.
union MyUnion { char a short b int c };
반면 구조체 내에서 a, b, c의 세 변수를 선언하면 메모리 공간은 다음과 같이 할당됩니다.
struct MyStruct { char a short b int c };
따라서 Union은 멤버 변수가 여러 가지 타입이 가능할 때 (즉, MyUnion이 int/short/char 중 어느 하나만 사용할 수 있을 때) 적용할 수 있으며, Union으로 하나의 메모리 공간에 선언함으로써 메모리를 효율적으로 사용할 수 있습니다.
제가 참여하고 있는 프로젝트에서는 아직 사용하지 않지만, 구글 크롬에서는 union을 사용하고 있습니다.
사실 union을 사용한 코드를 처음 본 것이 크롬 브라우저의 blink 레이아웃 엔진을 분석할 때이기도 했습니다.
blink 레이아웃 엔진의 코어 로직 중 NGConstraintSpace.h 라는 파일에서 union이 아래와 같이 쓰이고 있습니다. NGConstraintSpace는 blink가 레이아웃을 할 때 굉장히 중요하게 자주 사용되는 클래스이기 때문에, 핵심적인 코드에서 union이 사용되고 있는 예시로 볼 수 있겠습니다.
// The NGConstraintSpace represents a set of constraints and available space
// which a layout algorithm may produce a NGFragment within.
class CORE_EXPORT NGConstraintSpace final {
private:
struct RareData {
// |RareData| unions different types of data which are mutually exclusive.
// They fall into the following categories:
enum DataUnionType {
kNone,
kBlockData, // An inflow block which doesn't establish a new FC.
kTableCellData, // A table-cell (display: table-cell).
kCustomData // A custom layout (display: layout(foo)).
};
union {
BlockData block_data_;
TableCellData table_cell_data_;
CustomData custom_data_;
};
}
// To save a little space, we union these two fields. rare_data_ is valid if
// the |has_rare_data| bit is set, otherwise bfc_offset_ is valid.
union {
NGBfcOffset bfc_offset_;
RareData* rare_data_;
};
}
구글 개발자들이 남겨놓은 주석에서 볼 수 있듯이 "메모리를 아끼기 위해" 사용하는 것이 union 입니다. NGContraintSpace는 멤버 변수로 bfc_offset 또는 rare_data 중 하나만 사용하기 때문에 이렇게 union으로 선언되어 있습니다. 또한 흥미로운 점은 RareData 구조체 내부에서도 다시 union을 선언하여 이중으로 공간을 절약하고 있다는 것을 확인할 수 있습니다.
이를 적극적으로 활용한다면 상황에 따라 프로그램에 따라 꽤 많은 양의 메모리를 절약할 수 있겠네요.
위의 NGConstraintSpace.h 예제를 다시 확인해보면 union이 class 내부와 struct 내부에서 각각 선언되어 사용되고 있으며, 이 경우 union에 별도의 이름을 선언하지 않고 익명으로 사용하고 있음을 확인할 수 있습니다.
클래스 또는 구조체 내부에서는 일반적인 멤버 변수처럼 사용할 수 있기 때문에 오히려 익명으로 사용하는 것이 편리합니다.
이렇게 사용할 경우 union이 이름을 갖지 않기 때문에 union 내부 멤버 변수의 이름이 외부 변수 이름과 중복되지 않도록 주의해야 합니다.
익명 union에서 멤버 변수에 접근할 때는 일반적인 멤버 변수에 접근하는 것처럼 사용할 수 있습니다. 다시한번 NGConstraintSpace.h를 보겠습니다.
class CORE_EXPORT NGConstraintSpace final {
USING_FAST_MALLOC(NGConstraintSpace);
public:
// To ensure that the bfc_offset_, rare_data_ union doesn't get polluted,
// always initialize the bfc_offset_.
NGConstraintSpace() : bfc_offset_() {}
NGConstraintSpace(const NGConstraintSpace& other)
: available_size_(other.available_size_),
exclusion_space_(other.exclusion_space_),
bitfields_(other.bitfields_) {
if (HasRareData())
rare_data_ = new RareData(*other.rare_data_);
else
bfc_offset_ = other.bfc_offset_;
}
...
inline bool HasRareData() const { return bitfields_.has_rare_data; }
...
}
위 예제에서는 union 내부 변수에 접근할 때 other.rare_data_
또는 other.bfc_offset_
와 같이 사용하고 있습니다.
다만 rare_data와 bfc_offset 중 없는 쪽에 접근하면 안되므로 HasRareData()라는 별도의 함수와 1bit 변수를 사용하고 있네요. (여기서 등장하는 bit_field 에 대해서는 다른 포스팅에서 다루도록 하겠습니다.)
아까 다룬 예제에서 "To ensure that the bfc_offset_
, rare_data_
union doesn't get polluted, always initialize the bfc_offset_
" 라는 주석을 볼 수 있습니다.
union을 선언과 동시에 초기화하려면 첫 번째로 선언한 변수에 맞게 초기화해야 합니다. 이 경우 bfc_offset_
이 가장 먼저 선언되었으므로 NGConstraintSpace() 기본 생성자에서 bfc_offset()와 같이 멤버 초기화 리스트(member initializaion list) 방식으로 초기화해준 것입니다.
union은 class 또는 struct 내부에서 메모리 공간을 활용할 수 있는 좋은 문법입니다.
여러가지 타입의 변수 중 하나만 사용하는 경우에는 union을 사용하여 메모리를 절약하는 습관을 만드는 것이 좋겠습니다.