GCC拡張のcleanup属性は大域脱出と共存できない【俺を信じろ】


GCC拡張のcleanup属性とは?

C言語でC++のRAII的なことができる。参照

大域脱出(nonlocal exit)とは?

C言語では、setjmp() / longjmp()、略してsjljのこと。参照

大域(non-local)とはどういう意味?

コールスタックのフレームひとつが「ローカル」と考える。フレームにはローカル変数が入ってるし。

普通はreturnで一つ根本方向のフレームに戻るだけだが、大域脱出では一つと言わず、いくつでも戻れる(戻らなくてもいい)。ゆえにnon-local。

コールスタックの根本方向に向かうということは、sjljは例外に似ている?

ある意味では似ている。致命的な違いは、スタックの巻き戻しを行わないこと。

行わせる方法はない。抜け道はない。俺を信じろ。

共存できないとは?

#include <stdio.h>
#include <setjmp.h>

#define _cleanup_(x) __attribute__((cleanup(x)))
#define _cleanup_free_ _cleanup_(freep)

static jmp_buf buf;

static void freep(void *p) {
    printf("free!\n");
    // free(*(void**) p);
}

static void foo(int jmp) {
    _cleanup_free_ char *c_auto_free = NULL;
    printf("Hello\n");
    if (jmp) {
        longjmp(buf, 1);
    }
}

int main() {
    foo(0);

    if (!setjmp(buf)) {
        foo(1);
    }

    printf("Out\n");
    return 0;
}

出力:

Hello
free!
Hello
Out

longjmp()でmain()に戻ったときには、freep()が実行されない。sjljはスタックの巻き戻しを行わないのでこうなる。

sjlj以外の方法は?

GCCのC言語には、sjlj以外の大域脱出の方法がない。ないったらない。俺を信じろ。

GCC拡張にはcleanup属性があるのに、なぜスタックの巻き戻しを行う大域脱出の方法がないのか? わからない。とにかく、ないものはない。俺を信じろ。

GCC以外なら?

WindowsのC言語には構造化例外(SEH)がある。もちろんMSVC限定、かと思ったら最近のMingwでも使えるらしい。

まとめ

これは、「プログラミング言語はみんな同じようなもの」と思ったら深刻に大間違いなケースのひとつ。大多数の言語でものすごく頻繁に使われている制御構造2つを同時には使えないのだ。「C言語(GCC)にはこういう制約がある」とよくよく体に染み込ませた上で設計しないと、大域脱出とスタックの巻き戻しの両方をうっかり使ってしまう。

そんな鍛え方をしたくない? 人がいない? じゃあC言語、もうやめたほうがいいな?