ざっくりZig - 関数の終了処理(defer, errdefer, unreachable, @panic)

2024/05/13に公開

Zigでは関数の実行が終了する際に行う処理を事前に設定できます。deferは関数の終了が正常か否かに拘わらず必ず行う処理を設定します。一方errdeferは終了時にエラーが返される際に行う処理を設定します。ただし、unreachableでプログラムが終了するときはそれらで設定した処理が実行されませんので注意が必要です。

defer

deferは関数の終了後に行う処理を事前に設定するものです。1つの関数の中に複数存在できますが、その場合は後に設定された処理から先に実行されます。

deferの実行順
const std = @import("std");
const print = std.debug.print;

pub fn main() void {
    defer print("main: defer1\n", .{});
    defer print("main: defer2\n", .{});
    defer print("main: defer3\n", .{});
}
結果(ソースコードとは逆順で実行)
main: defer3
main: defer2
main: defer1

設定は{...}によるブロックでも可能できます。この場合、ブロック内部の実行順はソースコードの順と同じです。

ブロックによるdeferの設定
const std = @import("std");
const print = std.debug.print;

pub fn main() void {
    defer {
        print("main: defer1-1\n", .{});
        print("main: defer1-2\n", .{});
    }
    defer {
        print("main: defer2-1\n", .{});
        print("main: defer2-2\n", .{});
    }
}
結果(ブロック内部の実行順は変更しない)
main: defer2-1
main: defer2-2
main: defer1-1
main: defer1-2

errdefer

errdeferは関数がエラーを返すときに実行する処理を設定する点がdeferと異なります。その他はdeferと同じです。ただし、関数の呼び出しがtryで行われたときは呼び出し元のerrdeferで設定された処理も実行されますが、catchでエラー処理が行われる場合は呼び出し元のerrdeferで設定した処理は実行されません。

errdeferの実行
const std = @import("std");
const print = std.debug.print;

pub fn f() !void {
    defer print("f: defer\n", .{});
    errdefer print("f: errdefer\n", .{});
    return error.Something;
}

pub fn main() !void {
    defer print("main: defer\n", .{});
    errdefer print("main: errdefer\n", .{});

    // 以下の違いでerrdeferの処理が実行されるかが変わる
    // try f();
    // f() catch |err| print("catch: {any}\n", .{err});
}
結果 (try f()を実行したとき)
f: errdefer
f: defer
main: errdefer  (実行された)
main: defer
error: Something
// Debugモードで実行すると以下にエラーメッセージを表示
結果 (f() catch ... を実行したとき)
f: errdefer
f: defer
catch: error.Something  (mainのerrdeferは実行されていない)
main: defer

unreachableとdefer, errdefer

unreachableは「この後は何も実行しない」という意味で、コンパイルオプション(-O)がDebugReleaseSafeのときはpanicとなり、ReleaseFastReleaseSmallのときはここでプログラムが終了します。

このとき、unreachableの前にdefererrdeferがあっても、これらで設定した処理は実行されません。

unreachableによりdefer, errdeferが実行されない
const std = @import("std");
const print = std.debug.print;

pub fn g(flag: bool) !void {
    defer print("g: defer\n", .{});
    errdefer print("g: errdefer\n", .{});
    if (flag) return error.Something;
}

pub fn f() !void {
    defer print("f: defer\n", .{});
    errdefer print("f: errdefer\n", .{});
    g(true) catch unreachable;  // この後はdefer, errdefer含め何も実行されない
    return error.Something;
}

pub fn main() !void {
    defer print("main: defer\n", .{});
    errdefer print("main: errdefer\n", .{});
    try f();
}
結果(fとmainのdefer, errdeferは実行さない)
g: errdefer
g: defer

@panic

@panicは引数の文字列を出力したあと強制的にpanic処理(コアダンプ)が行わわれてプログラムが終了します。このとき、unreachableと同様defer, errdeferの処理は実行されません。なお、コンパイルオプションによって引数の文字列以外に出力される情報が以下のように異なります。

{ コンパイルオプション(-O) 引数の文字列以外の出力
Debug トレース情報(@panicの位置から呼び出し元まで)
ReleaseSmall トレース情報なし
RelaseFast, ReleaseSafe @panicの位置もしくはmain関数の実行位置
@panicによる終了
const std = @import("std");
const print = std.debug.print;

pub fn g(flag: bool) !void {
    defer print("g: defer\n", .{});
    errdefer print("g: errdefer\n", .{});
    if (flag) @panic("Panic!!!");   // 強制的にpanic(コアダンプ)
}

pub fn f() !void {
    defer print("f: defer\n", .{});
    errdefer print("f: errdefer\n", .{});
    g(true) catch unreachable;
    return error.Something;
}

pub fn main() !void {
    defer print("main: defer\n", .{});
    errdefer print("main: errdefer\n", .{});
    try f();
}
結果(Linuxの場合)
// Debug
thread 2651 panic: Panic!!!
/path/to/source.zig:8:15: 0x1033c02 in g (source)
    if (flag) @panic("Panic!!!");
              ^
/path/to/source.zig:14:6: 0x1033e75 in f (source)
    g(true) catch unreachable;
     ^
/path/to/source.zig:21:10: 0x1034350 in main (source)
    try f();
         ^
/path/to/zig/lib/std/start.zig:511:37: 0x1033b05 in posixCallMainAndExit (source)
            const result = root.main() catch |err| {
                                    ^
/path/to/zig/lib/std/start.zig:253:5: 0x1033621 in _start (source)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
中止 (コアダンプ)

// ReleaseFast
thread 2597 panic: Panic!!!
/path/to/source.zig:8:15: 0x1008f24 in main (source)
    if (flag) @panic("Panic!!!");
              ^
/path/to/zig/lib/std/start.zig:511:37: 0x1008f04 in posixCallMainAndExit (source)
            const result = root.main() catch |err| {
                                    ^
/path/to/zig/lib/std/start.zig:253:5: 0x1008e31 in _start (source)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
中止 (コアダンプ)

// ReleaseSafe
thread 2626 panic: Panic!!!
/path/to/source.zig:8:15: 0x100a1a4 in main (source)
    if (flag) @panic("Panic!!!");
              ^
/path/to/zig/lib/std/start.zig:511:37: 0x100a171 in posixCallMainAndExit (source)
            const result = root.main() catch |err| {
                                    ^
/path/to/zig/lib/std/start.zig:253:5: 0x100a021 in _start (source)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
中止 (コアダンプ)

// ReleaseSmall
thread 2545 panic: Panic!!!
Unable to dump stack trace: debug info stripped
中止 (コアダンプ)

まとめ

  • deferは関数の終了後に行う処理を設定する
    • 後に設定された処理から先に実行する
    • 一文もしくはブロックで設定できる
    • unreachableでプログラムが終了すると実行されない
  • errdeferは関数からエラーが返されるときの処理を設定する以外はdeferと同じ
  • @panicはプログラム内で強制的にpanic処理を行い、設定したメッセージを出力する
    • コンパイルオプションによって出力される情報が異なる

< エラー(error, try, catch, if...else, ビルトイン関数)
ポインタとスライス(*T, *[N]T, [*]T, []T, &変数, &関数) >
ざっくりZig 一覧

Discussion