공용체

공용체 (Unions)

여러 타입을 같은 메모리 공간에 겹쳐 담아야 할 때가 있어요. 그럴 때 쓰는 게 러스트의 공용체(Union)예요. 자주 쓰이진 않지만, C와의 상호 운용이나 성능이 중요한 코드에서 유용하답니다. 이 페이지에서 공용체가 어떻게 동작하는지 살펴볼게요.

출처: Rust Reference

본문

선언

공용체 선언은 구조체 선언과 같은 문법을 쓰되, struct 대신 union을 써요.

Union → union IDENTIFIER GenericParams? WhereClause? { StructFields? }

공용체 선언은 그 위치한 모듈이나 블록의 타입 네임스페이스에 주어진 이름을 정의해요.

#[repr(C)]
union MyUnion {
    f1: u32,
    f2: f32,
}

공용체의 핵심 성질은 모든 필드가 공통 저장 공간을 공유한다는 거예요. 그 결과, 공용체의 한 필드에 쓰면 다른 필드를 덮어쓸 수 있고, 공용체의 크기는 가장 큰 필드의 크기로 결정돼요.

공용체 필드 타입은 다음과 같은 타입의 부분집합으로 제한돼요.

  • Copy 타입
  • 참조 형 (임의의 T에 대한 &T&mut T)
  • ManuallyDrop<T> (임의의 T에 대해)
  • 위에서 허용된 공용체 필드 타입만을 담은 튜플과 배열

이 제한 덕분에, 특히 공용체 필드는 결코 드롭될 필요가 없음이 보장돼요. 구조체와 열거형처럼, 공용체에 impl Drop을 해서 드롭될 때 무슨 일이 일어날지 수동으로 정의하는 것도 가능해요.

필드가 하나도 없는 공용체는 컴파일러가 받아들이지 않지만, 매크로는 받아들일 수 있어요.

공용체 초기화

공용체 타입의 값은 구조체 타입에 쓰는 것과 같은 문법으로 만들 수 있는데, 단 정확히 하나의 필드를 지정해야 해요.

union MyUnion { f1: u32, f2: f32 }

let u = MyUnion { f1: 1 };

위 표현식은 MyUnion 타입의 값을 만들고 f1 필드로 저장 공간을 초기화해요. 공용체는 구조체 필드와 같은 문법으로 접근할 수 있어요.

union MyUnion { f1: u32, f2: f32 }

let u = MyUnion { f1: 1 };
let f = unsafe { u.f1 };

공용체 필드 읽기와 쓰기

공용체에는 "활성 필드(active field)"라는 개념이 없어요. 대신 모든 공용체 접근은 단지 저장 공간을 그 접근에 사용된 필드의 타입으로 해석할 뿐이에요.

공용체 필드를 읽는 것은 공용체의 비트를 그 필드의 타입으로 읽는 것이에요. 필드는 0이 아닌 오프셋을 가질 수 있는데 (C 표현을 사용할 때를 제외하고), 그 경우 필드의 오프셋에서 시작하는 비트들이 읽혀요.

데이터가 필드의 타입에 맞게 유효함을 보장하는 것은 프로그래머의 책임이에요. 그렇게 하지 않으면 정의되지 않은 동작(undefined behavior)이 돼요. 예를 들어 부울 타입의 필드에서 값 3을 읽는 것은 정의되지 않은 동작이에요. 실질적으로, C 표현을 가진 공용체에 쓰고 나서 읽는 것은 쓰기에 사용된 타입에서 읽기에 사용된 타입으로의 transmute와 유사해요.

결과적으로, 공용체 필드의 모든 읽기는 unsafe 블록 안에 있어야 해요.

union MyUnion { f1: u32, f2: f32 }
let u = MyUnion { f1: 1 };

unsafe {
    let f = u.f1;
}

흔히 공용체를 사용하는 코드는 위험한 공용체 필드 접근 주위에 안전한 래퍼를 제공해요. 반대로, 공용체 필드에 대한 쓰기는 안전해요. 단지 임의의 데이터를 덮어쓸 뿐이고 정의되지 않은 동작을 일으킬 수 없기 때문이에요. (공용체 필드 타입은 드롭 글루(drop glue)를 가질 수 없으므로, 공용체 필드 쓰기가 어떤 것을 암묵적으로 드롭하는 일도 결코 없어요.)

공용체의 패턴 매칭

공용체 필드에 접근하는 또 다른 방법은 패턴 매칭이에요. 공용체 필드에 대한 패턴 매칭은 구조체 패턴과 같은 문법을 쓰되, 패턴이 정확히 하나의 필드를 지정해야 해요. 패턴 매칭은 특정 필드로 공용체를 읽는 것과 같으므로, 역시 unsafe 블록 안에 있어야 해요.

union MyUnion { f1: u32, f2: f32 }

fn f(u: MyUnion) {
    unsafe {
        match u {
            MyUnion { f1: 10 } => { println!("ten"); }
            MyUnion { f2 } => { println!("{}", f2); }
        }
    }
}

패턴 매칭은 더 큰 구조의 필드로 공용체를 매치할 수도 있어요. 특히 FFI를 통해 러스트 공용체로 C의 태그 공용체(tagged union)를 구현할 때, 이 기능으로 태그와 해당 필드를 동시에 매치할 수 있어요.

#[repr(u32)]
enum Tag { I, F }

#[repr(C)]
union U {
    i: i32,
    f: f32,
}

#[repr(C)]
struct Value {
    tag: Tag,
    u: U,
}

fn is_zero(v: Value) -> bool {
    unsafe {
        match v {
            Value { tag: Tag::I, u: U { i: 0 } } => true,
            Value { tag: Tag::F, u: U { f: num } } if num == 0.0 => true,
            _ => false,
        }
    }
}

공용체 필드에 대한 참조

공용체 필드들은 공통 저장 공간을 공유하므로, 공용체의 한 필드에 대한 쓰기 접근을 얻으면 나머지 모든 필드에 대한 쓰기 접근도 얻게 돼요. 빌림 검사(borrow checking) 규칙은 이 사실을 반영해 조정돼야 해요. 그 결과, 공용체의 한 필드가 빌려지면 나머지 모든 필드도 같은 수명 동안 함께 빌려져요.

union MyUnion { f1: u32, f2: f32 }
// ERROR: `u` (via `u.f2`)을 한 번에 하나 이상 가변 빌림할 수 없어요
fn test() {
    let mut u = MyUnion { f1: 1 };
    unsafe {
        let b1 = &mut u.f1;
//                    ---- 첫 번째 가변 빌림이 여기서 발생 (via `u.f1`)
        let b2 = &mut u.f2;
//                    ^^^^ 두 번째 가변 빌림이 여기서 발생 (via `u.f2`)
        *b1 = 5;
    }
//  - 첫 번째 빌림은 여기서 끝남
    assert_eq!(unsafe { u.f1 }, 5);
}

보시다시피, 많은 측면에서 (레이아웃, 안전성, 소유권을 제외하고) 공용체는 구조체와 정확히 똑같이 동작해요. 이는 대체로 구조체에서 문법적 형태를 상속받았기 때문이에요. 이는 언급하지 않은 다른 러스트 언어 측면 (프라이버시, 이름 분석, 타입 추론, 제네릭, 트레이트 구현, 고유 구현, 일관성, 패턴 검사 등)에도 마찬가지로 적용돼요.

더 알아보기 (Learn more)

  • Rust Reference의 공용체 문서에서 전체 문법과 예제를 확인할 수 있어요.
  • Rust Reference의 타입 레이아웃 문서에서 공용체의 메모리 표현을 더 자세히 다룬답니다.
  • Rust Reference의 구조체 문서에서 공용체와 문법이 닮은 구조체를 살펴볼 수 있어요.