ジェネリックラッパー
CoddyのCジャーニー「Object Oriented Programming」セクションの一部 — レッスン 50/61。
void* 単独であらゆるデータを格納できますが、重大な欠点があります。それは、保持している型が何であるかを見失ってしまうことです。真に有用なジェネリックコンテナを構築するには、データポインタと型情報をペアにする必要があります。そこで登場するのが、ジェネリックラッパーパターンです。
考え方は単純です。void* と、格納されている型を識別する enum をまとめた struct を作成します。これにより、ラッパーは常にその内容をどのように解釈すべきかを把握できるようになります。
typedef enum {
TYPE_INT,
TYPE_STRING
} DataType;
typedef struct {
void* data;
DataType type;
} Wrapper;データを保存する際、実際の値のためにメモリを割り当て、その型を記録します。取得する際は、キャストする前に型を確認します:
Wrapper wrap_int(int value) {
int* p = malloc(sizeof(int));
*p = value;
return (Wrapper){ .data = p, .type = TYPE_INT };
}
void print_wrapper(Wrapper* w) {
if (w->type == TYPE_INT) {
printf("%d\n", *(int*)w->data);
} else if (w->type == TYPE_STRING) {
printf("%s\n", (char*)w->data);
}
}このパターンは、C言語で型安全なジェネリックコンテナを構築するための基礎となります。enumは実行時の型タグとして機能し、格納されたデータをどのように扱うかについて、コードが安全な判断を下せるようにします。
チャレンジ
簡単整数または文字列のいずれかを格納でき、保持している型を追跡することで、後で安全にデータを取得して表示できる、型安全な汎用ラッパーシステムを構築しましょう。
コードは以下の3つのファイルに分けて構成します:
wrapper.h:TYPE_INTとTYPE_STRINGという値を持つDataType列挙型を定義します。次に、void*データポインタとDataTypeフィールドをまとめたWrapper構造体を定義します。ラッパーの作成と内容の表示を行うための関数プロトタイプを宣言します。wrapper.c: ラッパーの機能を実装します:wrap_int— 整数を受け取り、そのためのメモリを割り当て、値を格納し、適切な型タグを持つ Wrapper を返します。wrap_string— 文字列 (char*) を受け取り、メモリを割り当てて文字列をコピーし、文字列型タグを持つ Wrapper を返します。print_wrapper— 型タグを確認し、データを適切に表示します(整数はそのまま、文字列はそのまま)。free_wrapper— ラッパー内の動的に割り当てられたメモリを解放します。
main.c: すべてをまとめます。型インジケータ(整数ならi、文字列ならs)に続いて値を読み取ります。適切なラッパーを作成し、その内容を表示して、メモリを解放します。
プログラムは2つの入力を受け取ります:
- 型インジケータ:整数なら
i、文字列ならs - ラップする値
入力が i と 42 の場合の出力例:
42入力が s と Hello の場合の出力例:
Hello入力が i と -100 の場合の出力例:
-100入力が s と Generic Programming の場合の出力例:
Generic Programmingヘッダーファイルではインクルードガードを使用することを忘れないでください。文字列のコピーには、strlen と strcpy を使用して、文字列とヌル終端文字のための十分なスペースを割り当ててください。
チートシート
ジェネリック・ラッパー(Generic Wrapper)パターンは、void*ポインタと型情報を組み合わせて、型安全なジェネリック・コンテナを作成します。これにより、格納されている型を把握したまま、任意のデータ型を保存できます。
格納される型を識別するための列挙型(enum)を定義します:
typedef enum {
TYPE_INT,
TYPE_STRING
} DataType;データポインタと型タグをまとめる構造体を作成します:
typedef struct {
void* data;
DataType type;
} Wrapper;データを保存する際、メモリを割り当てて型を記録します:
Wrapper wrap_int(int value) {
int* p = malloc(sizeof(int));
*p = value;
return (Wrapper){ .data = p, .type = TYPE_INT };
}データを取り出す際、キャストする前に型タグを確認します:
void print_wrapper(Wrapper* w) {
if (w->type == TYPE_INT) {
printf("%d\n", *(int*)w->data);
} else if (w->type == TYPE_STRING) {
printf("%s\n", (char*)w->data);
}
}列挙型は実行時の型タグとして機能し、安全な型チェックと格納されたデータの適切な処理を可能にします。
自分で試してみよう
#include <stdio.h>
#include <string.h>
#include "wrapper.h"
int main() {
char type;
scanf("%c", &type);
getchar(); // 改行を読み飛ばす
Wrapper w;
if (type == 'i') {
int value;
scanf("%d", &value);
// TODO: `wrap_int` を使用して整数ラッパーを作成する
} else if (type == 's') {
char str[256];
fgets(str, sizeof(str), stdin);
// 末尾に改行があれば削除する
str[strcspn(str, "\n")] = '\0';
// TODO: `wrap_string` を使用して文字列ラッパーを作成する
}
// TODO: `print_wrapper` を使用してラッパーの内容を表示する
// TODO: `free_wrapper` を使用してラッパーのメモリを解放する
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。