構造体の初期化パターン

C言語で構造体を初期化するパターンを考えます。呼び出し元のスタック上に配置する方法と、呼び出し先でヒープ上に確保する方法に大別できると理解しています。

以下の例では、`foo_*_t`は初期化時にメンバ変数`bar`を設定し、その値を2倍にして返す関数`foo_*_double`を備えます。ただし、`bar`の値は100以下に制限したいものとします。

https://paiza.io/projects/Ss-l3tcIHAPbvzcs8BYxCQ?locale=ja-jp

## 呼び出し元のスタック上に配置する

紳士協定によって必ず`foo_stack_init`関数で初期化するものと定めます。

```c:foo_stack.h
#ifndef FOO_STACK_H
#define FOO_STACK_H

#ifdef __cplusplus
extern "C"
{
#endif

#define __STDC_WANT_LIB_EXT1__ 1

#include <errno.h>
#ifndef __STDC_LIB_EXT1__
typedef int errno_t;
#endif
#include <stddef.h>

typedef struct foo_stack_t
{
    int bar;
} foo_stack_t;

errno_t foo_stack_init(foo_stack_t *foo, int bar);
errno_t foo_stack_double(foo_stack_t *foo, int *out);

#ifdef __cplusplus
}
#endif

#endif /* FOO_STACK_H */
```

```c:foo_stack.c
#include "foo_stack.h"

errno_t foo_stack_init(foo_stack_t *foo, int bar)
{
    if (foo == NULL ||
        100 < bar)
    {
        return EINVAL;
    }

    foo->bar = bar;
    return 0;
}

errno_t foo_stack_double(foo_stack_t *foo, int *out)
{
    if (foo == NULL ||
        out == NULL)
    {
        return EINVAL;
    }

    *out = foo->bar * 2;
    return 0;
}
```

```c:main.c
#include "foo_stack.h"

#include <assert.h>

int main_stack(void)
{
    foo_stack_t foo;
    errno_t err = foo_stack_init(&foo, 1);
    if (err != 0)
    {
        return 1;
    }

    int out = 0;
    err = foo_stack_double(&foo, &out);
    if (err != 0)
    {
        return 1;
    }
    assert(out == 2);

    return 0;
}
```

## 呼び出し先でヒープ上に確保する

Opaqueな型を利用してメンバ変数`bar`を隠蔽しています。`foo_heap_t`は不完全型になり、それ自体を宣言することはできません。

```c:foo_heap.h
#ifndef FOO_HEAP_H
#define FOO_HEAP_H

#ifdef __cplusplus
extern "C"
{
#endif

#define __STDC_WANT_LIB_EXT1__ 1

#include <errno.h>
#ifndef __STDC_LIB_EXT1__
typedef int errno_t;
#endif
#include <stddef.h>
#include <stdlib.h>

typedef struct foo_heap_t foo_heap_t;

errno_t foo_heap_new(foo_heap_t **foo, int bar);
errno_t foo_heap_double(foo_heap_t *foo, int *out);
void foo_heap_destroy(foo_heap_t *foo);

#ifdef __cplusplus
}
#endif

#endif /* FOO_HEAP_H */
```

```c:foo_heap.c
#include "foo_heap.h"

struct foo_heap_t
{
    int bar;
};

errno_t foo_heap_new(foo_heap_t **foo, int bar)
{
    if (foo == NULL ||
        100 < bar)
    {
        return EINVAL;
    }

    foo_heap_t *foo_ = (foo_heap_t *)malloc(sizeof(foo_heap_t));
    if (foo_ == NULL)
    {
        return ENOMEM;
    }

    foo_->bar = bar;
    *foo = foo_;
    return 0;
}

errno_t foo_heap_double(foo_heap_t *foo, int *out)
{
    if (foo == NULL ||
        out == NULL)
    {
        return EINVAL;
    }

    *out = foo->bar * 2;
    return 0;
}

void foo_heap_destroy(foo_heap_t *foo)
{
    free(foo);
}
```

```c:main.c
#include "foo_heap.h"

#include <assert.h>

int main_heap(void)
{
    foo_heap_t *foo = NULL;
    errno_t  err = foo_heap_new(&foo, 1);
    if (err != 0)
    {
        return 1;
    }

    int out = 0;
    err = foo_heap_double(foo, &out);
    if (err != 0)
    {
        return 1;
    }
    assert(out == 2);

    foo_heap_destroy(foo);
    foo = NULL;

    return 0;
}
```

## 所感

スタックに配置する場合、変数`foo`のメモリは関数の終了時に自動的に破棄されます。メモリアロケーションに失敗する可能性がなく、メモリリークの危険性が低いのがよいと思います。メモリ以外に明示的に破棄処理を書きたい場合には`foo_stack_deinit`のような関数を用意することになります。

スタックに配置すると、`foo_stack_init`を呼び出し忘れたことを後続の関数で検知し止める方法がありません。

スタックに配置するには構造体定義をヘッダーに記述する必要があり、メンバ変数`bar`は呼び出し元からも見えることになります。`bar`を直接利用した誤った操作をされる可能性があり、例のようなシンプルなものだと`foo_stack_init`で初期化するのは冗長に感じられます。

ヒープに配置する方法では、`foo_heap_t *`を`NULL`で初期化すれば、万が一`foo_heap_new`関数を経ないで利用しようとした場合に後続の関数で止まります。`bar`が見えないので`foo_heap_new`関数の利用を強制できているのはよいと思います。

`foo_heap_destroy`関数を忘れるとメモリリークが発生します。が、メモリの解放以外に明示的な破棄処理が必要なら`foo_stack_deinit`でも忘れるのは同じことのようにも思います。