C言語でRAII

前置き:RAII って?

 C++ 言語には RAII; Resource Acquisition is Initialization という考え方があります。これはどういうことでしょうか。

#define ARRY_SZ (5)
int32_t p_arry = (int32_t *)malloc(sizeof(int32_t) * ARRY_SZ);
p_arry[0] = 1;
p_arry[1] = 2;
p_arry[2] = 3;
p_arry[3] = 4;
p_arry[4] = 5;

for (size_t idx=0; idx<ARRY_SZ; ++idx) {
    printf("p_arry[%zu]=%" PRId32 "\n", idx, p_arry[idx]);
}

よくある、Cでヒープ確保するコードですが、このコード片は free(p_arry) を呼び出し忘れているため、もしかしたらメモリーがリークするかもしれません。また、for 文で値を表示する前に p_arry[n] = n+1; の形式で確保した領域を初期化していますが、ここで初期化を忘れていたらどうでしょう。未初期化の領域を読み出そうとするのは Undefined Behavior なので、鼻から悪魔が出るかもしれません。

#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cinttypes>
#include <new>
#include <initializer_list>

class HeapArray {
private:
    std::int32_t *values = nullptr;
    std::size_t size = 0;
public:
    HeapArray() = delete;
    HeapArray(std::initializer_list<std::int32_t> init) {
        this->values = new(std::nothrow) std::int32_t[init.size()];
        if (this->values == nullptr) {
            return;
        }
        this->size = init.size();
        std::size_t idx=0;
        for (auto&& v : init) {
            this->values[idx] = v;
            ++idx;
        }
    }
    ~HeapArray() { delete[] this->values; };
    void debug() {
        for (std::size_t idx=0; idx<this->size; ++idx) {
            std::printf("values[%zu]=%" PRId32 "\n", idx, this->values[idx]);
        }
    }
};

int main() {
    HeapArray test {1, 2, 3, 4, 5};
    test.debug();
    return 0;
}

ではこちらの C++ のコードはどうでしょう。クラスにしておくことで、先程の C のコードとやってることは同じですが、変数宣言するだけでコンストラクタでヒープ確保をしますし、スコープから外れたら自動でデストラクタが呼ばれてヒープが開放され、また、初期化子リストなしで変数宣言するとデフォルトコンストラクタが delete してあるのでコンパイルが通らないので、プログラマに初期化を強要できます。

prog.cc: In function 'int main()':
prog.cc:36:15: error: use of deleted function 'HeapArray::HeapArray()'
   36 |     HeapArray test_2;
      |               ^~~~~~
prog.cc:13:5: note: declared here
   13 |     HeapArray() = delete;
      |     ^~~~~~~~~
このようなコンパイルエラーになるわけですね。

今回はやってませんが、ここで更に unique_ptr などスマートポインタ使うともっと良いかもしれませんね。

そもそも std::int32_t *values = new std::int32_t[5] {1, 2, 3, 4, 5}; とかやれば new だけで領域確保と初期化を同時にできるだろ、とかは目を瞑ってください……。

RAII によるリソース管理、他の言語にもあったりします。たとえば Pyton の with 構文。

with open("file.txt") as f:
    print(f.read())
この場合、open(2) で確保したリソースが変数 f に紐付いて管理されて、with 構文のインデントブロックのスコープから外れると、自動で close(2) が裏で呼ばれてリソースが開放されます。close 呼び忘れがなくてめっちゃ便利ですね。

C で RAII

C プログラマーはこのようなイディオムの恩恵に与れないのでしょうか。いえ、実はコンパイラ拡張によっては、このような書き方も可能になるのです。
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>

static inline void dtor(void *p) {
    free(*(void **)p);
    puts("freed!");
}

#define ARRY_SZ (5)

int main() {
    __attribute__((cleanup(dtor))) int32_t *p_arry = (int32_t *)malloc(sizeof(int32_t)*ARRY_SZ);
}

それがこの __attribute__((cleanup(func))) です。一見このコードは p_arry に格納したヒープがリークしてそうですが、実はちゃんと dtor 関数が呼ばれています。その証拠に、このコードをコンパイル・実行するとコンソールに freed! と表示されるはずです。

ところで、このコードだと確保した領域は未初期化のままです。そこで、次のようにしてみます。

#include <stddef.h>
#include <stdint.h>
#include <inttypes.h>
#include <starg.h>
#include <stdlib.h>
#include <stdio.h>

static inline void ctor(int32_t *p, size_t sz, ...) {
    va_list ap;
    va_start(ap, sz);
    for (size_t idx=0; idx<sz; ++idx) {
        p[idx] = va_arg(ap, int32_t);
    }
    va_end(ap);
}

static inline void dtor(void *p) {
    free(*(void **)p);
    puts("freed!");
}

#define CTOR_HEAP_ARRY(var, sz, ...) \
__attribute__((cleanup(dtor))) int32_t *var = (int32_t *)malloc(sizeof(int32_t)*sz); \
ctor(var, sz, __VA_ARGS__)

#define ARRY_SZ (5)

int main() {
    CTOR_HEAP_ARRY(p_arry, ARRY_SZ, 1, 2, 3, 4, 5);
    for (size_t idx=0; idx<ARRY_SZ; ++idx) {
        printf("p_arry[%zu]=%" PRId32 "\n", idx, p_arry[idx]);
    }
}

どうでしょうか!ヒープ確保と共に初期化もできましたよ!やりましたね!

ここで用意したマクロを使ってもらえなかったらどうするの、とか、そもそもこのマクロ結構危険じゃない?とか、いろいろ言いたいこともあると思います。initializer list でやるようなことを可変長引数で実現しようとしてエラーチェックもサボってるので、バッファオーバーランの危険性もあります。だめですね。

はい。みんな Rust とか使おうね。