잘못된 유니온 필드 접근
잘못된 유니온 필드 접근
유니온(union)은 여러 필드 중 한 번에 하나만 값을 담을 수 있는데요, 그중 활성화된(active) 필드가 아닌 다른 필드에 접근하면 컴파일 타임 또는 런타임에 안전 검사가 걸립니다. Zig는 이 경우를 Illegal Behavior로 처리해서, 잘못된 필드에 손대면 바로 오류를 알려줘요.
본문
먼저 컴파일 타임에 걸리는 경우를 볼게요. f에 int 필드를 42로 설정한 상태에서, 바로 다음 줄에 float 필드를 건드리면:
comptime {
var f = Foo{ .int = 42 };
f.float = 12.34;
}
const Foo = union {
float: f32,
int: u32,
};
$ zig test test_comptime_wrong_union_field_access.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_wrong_union_field_access.zig:3:6: error: access of union field 'float' while field 'int' is active
f.float = 12.34;
~^~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_wrong_union_field_access.zig:6:13: note: union declared here
const Foo = union {
^~~~~
int가 활성화돼 있는데 float 필드에 접근했다는 오류가 컴파일 타임에 바로 나오죠.
이번엔 런타임 상황을 볼게요. 값을 정적으로 알 수 없어서 컴파일 타임에 못 잡는 경우에도, 실행 중에 안전 검사가 발동합니다:
const std = @import("std");
const Foo = union {
float: f32,
int: u32,
};
pub fn main() void {
var f = Foo{ .int = 42 };
bar(&f);
}
fn bar(f: *Foo) void {
f.float = 12.34;
std.debug.print("value: {}\n", .{f.float});
}
$ zig build-exe runtime_wrong_union_field_access.zig
$ ./runtime_wrong_union_field_access
thread 972134 panic: access of union field 'float' while field 'int' is active
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_wrong_union_field_access.zig:14:6: 0x11e835e in bar (runtime_wrong_union_field_access.zig)
f.float = 12.34;
^
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_wrong_union_field_access.zig:10:8: 0x11e829e in main (runtime_wrong_union_field_access.zig)
bar(&f);
^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:788:64: 0x11e7bbb in callMain (std.zig)
if (fn_info.param_types.len == 0) return wrapMain(root.main());
^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:248:5: 0x11e75e1 in _start (std.zig)
asm volatile (switch (native_arch) {
^
(process terminated by signal)
주의할 점은, 이 안전 검사가 extern이나 packed 유니온에는 적용되지 않는다는 거예요. 활성 필드를 보장해 주는 이 검사는 일반 union에만 있는 기능입니다.
유니온의 활성 필드를 바꾸고 싶다면, 필드 하나를 골라 대입하는 대신 유니온 전체를 통째로 대입하면 됩니다:
const std = @import("std");
const Foo = union {
float: f32,
int: u32,
};
pub fn main() void {
var f = Foo{ .int = 42 };
bar(&f);
}
fn bar(f: *Foo) void {
f.* = Foo{ .float = 12.34 };
std.debug.print("value: {}\n", .{f.float});
}
$ zig build-exe change_active_union_field.zig
$ ./change_active_union_field
value: 12.34
f.* = Foo{ .float = 12.34 }처럼 유니온 전체를 새 값으로 덮어쓰면 활성 필드가 float로 바뀌고, 안전하게 접근할 수 있어요.
그런데 바꾸려는 필드에 아직 의미 있는 값을 모를 때도 있죠. 그럴 땐 undefined를 써서 활성 필드만 전환해 두면 됩니다:
const std = @import("std");
const Foo = union {
float: f32,
int: u32,
};
pub fn main() void {
var f = Foo{ .int = 42 };
f = Foo{ .float = undefined };
bar(&f);
std.debug.print("value: {}\n", .{f.float});
}
fn bar(f: *Foo) void {
f.float = 12.34;
}
$ zig build-exe undefined_active_union_field.zig
$ ./undefined_active_union_field
value: 12.34
f = Foo{ .float = undefined }로 활성 필드를 float로 바꿔 놓은 다음, 나중에 의미 있는 값을 채워 넣는 패턴이에요. 이렇게 하면 "활성 필드가 아직 int인데 float에 접근한다"는 오류 없이 필드 전환을 안전하게 시작할 수 있습니다.
더 알아보기
- union — 일반 유니온의 기본 개념
- extern union — 안전 검사가 없는
extern유니온