Invalid Enum Cast — 존재하지 않는 태그 값으로 enum을 만들면 어떻게 될까요?

Invalid Enum Cast — 존재하지 않는 태그 값으로 enum을 만들면 어떻게 될까요?

enum은 자기 태그 값에 없는 정수로는 만들 수 없어요. 그런데 만약에 그런 값을 억지로 넣으려 한다면요? Zig는 그 상황이 컴파일 타임에 드러나는지, 런타임에만 벌어지는지에 따라 다르게 반응합니다. 두 경우를 나란히 보면서 어떻게 막아주는지 확인해볼게요.

출처: Zig Documentation

본문

컴파일 타임 — 값을 알 수 있어서 아예 걸러집니다

값이 컴파일 타임에 정해져 있어서 어떤 결과가 나올지 미리 알 수 있다면, Zig는 컴파일 에러로 잡아줘요. 아래 코드는 u23Foo로 바꾸려고 하는데, Foo에는 태그 값이 a=0, b=1, c=2뿐이라 3에 해당하는 태그가 없죠.

const Foo = enum(u2) {
    a,
    b,
    c,
};
comptime {
    const a: u2 = 3;
    const b: Foo = @fromBackingInt(a);
    _ = b;
}

comptime 블록 안이라서 a의 값 3이 컴파일 타임에 확정되어 있어요. 그래서 실행되기도 전에 컴파일러가 문제를 찾아내요.

$ zig test test_comptime_invalid_enum_cast.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_invalid_enum_cast.zig:8:20: error: enum 'test_comptime_invalid_enum_cast.Foo' has no tag with value '3'
    const b: Foo = @fromBackingInt(a);
                   ^~~~~~~~~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_invalid_enum_cast.zig:1:13: note: enum declared here
const Foo = enum(u2) {
            ^~~~

핵심 메시지는 enum '...Foo' has no tag with value '3'예요. 말 그대로 값 3에 해당하는 태그가 enum에 없다는 뜻이죠.

런타임 — 값을 미리 알 수 없으면 패닉으로 막습니다

값이 런타임에 들어와서 컴파일 타임에는 알 수 없는 경우, 에러 대신 실행 중에 패닉(panic)이 발생해요. 아래 코드는 함수 인자로 받은 aFoo로 바꾸는데, main에서 3을 넘기고 있어요.

const std = @import("std");

const Foo = enum(u2) {
    a,
    b,
    c,
};

fn foo(a: u2) void {
    const b: Foo = @fromBackingInt(a);
    std.debug.print("value: {s}\n", .{@tagName(b)});
}

pub fn main() void {
    foo(3);
}

foo(3)이 호출되면 유효하지 않은 enum 값을 만드는 순간 런타임에서 막아버려요.

$ zig build-exe runtime_invalid_enum_cast.zig
$ ./runtime_invalid_enum_cast
thread 972816 panic: invalid enum value
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_invalid_enum_cast.zig:10:20: 0x11e839b in foo (runtime_invalid_enum_cast.zig)
    const b: Foo = @fromBackingInt(a);
                   ^
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_invalid_enum_cast.zig:15:8: 0x11e828e in main (runtime_invalid_enum_cast.zig)
    foo(3);
       ^
/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)

panic: invalid enum value라는 메시지와 함께 프로그램이 종료돼요. @tagName(b)를 호출하기 전에, 바로 @fromBackingInt(a)를 실행하는 시점에서 패닉이 나는 걸 스택 트레이스에서 확인할 수 있어요.

정리하면, 존재하지 않는 태그 값으로 enum을 만드는 건 두 갈래로 막혀요. 컴파일 타임에 값이 확정되면 컴파일 에러, 런타임에만 알 수 있으면 패닉으로요. 어느 쪽이든 유효하지 않은 enum 값은 만들어질 수 없도록 Zig가 스스로 지켜주는 거예요.

더 알아보기

  • @fromBackingInt는 정수 값을 enum으로 바꾸는 내장 함수예요. 기본 값(@enumFromInt)과 달리 명시적으로 안전 검사를 우회하고 싶을 때 쓰죠.
  • 유효하지 않은 값을 건네받았을 때 패닉 대신 직접 처리하고 싶다면, 값을 만들기 전에 태그 존재 여부를 먼저 확인하는 방식을 고려해볼 수 있어요.