C言語でインターフェースを実装する
Phantz
2021年5月16日に投稿、2021年6月3日に更新
[_注: このデモンストレーションの拡張版および更新版は、typeclass-interface-patternで見つかります。_](https://github.com/TotallyNotChase/typeclass-interface-pattern)
この記事では、標準C99(以上)で機能するポリモーフィズムを達成するために使用できる、拡張可能で実用的なデザインパターンについて説明しています。c-iteratorsでこのパターンの実装と利用法を、Rust風の遅延評価の型安全なイテレータとして実装しました。
もし、あなたがC言語でかなりの量のコードを書いたことがあるなら、高水準言語で見られるポリモーフィズムのような何かを持ちたいと思った経験があるかもしれません。
たとえば、一般的なコンテナ(例えば、一般的な要素のリンクリスト)を持ちたいかもしれませんし、もしくはポリモーフィックな型を引数に取る関数を持ちたいかもしれません。そのようなことが欲しくなったことがあるなら、おそらくC言語ではネイティブなポリモーフィズムのサポートが不足していることに少し欲求不満を感じているでしょう。
幸いにも、C言語でのポリモーフィズムは何も新しいことではありません。多くの記事、リポジトリ、プロジェクトが、バーチャルメソッドテーブル(VMT、つまりvtable)を使ったOOPポリモーフィズムパターンを説明し、使用しています。Linux、CPythonの実装、その他多くの大規模なCプロジェクトは、Cコミュニティの間でかなり一般的なパターンのひとつとして、vtableを使ったOOPを実装しています。
しかし、もしあなたがこのパターンを使用したことがあるなら、その欠点も知っているはずです。
- 型安全性に欠けています。なぜなら、このパターンの主要な関心事ではないからです。
- 内部的には安全でないキャストに依存しています。
- 論理的にみて、非常に不格好で、ごまかしの多いものです。
- かなりの罠を持っています。
- このパターンの_いくつか_の実装は標準への適合性を破り、実装定義された動作に依存しています。通常、彼らが破っているルールはstrict aliasingです。ただし、この問題を避けることは本当に難しくありません。CPythonは実際に一度この問題を抱えて後で修正しました。しかし、それが必ずしも破られている唯一のルールではありません。多くの実装は単に標準Cを使用せず、gnucなどC言語の拡張された実装に依存することもあります。
あぁ、実はもう一つの欠点があります - しかしこれは完全に主観的なものであり、実際の会話には何も貢献しません - もしあなたがOOPが好きでなくて、OOPの継承よりも機能的なポリモーフィズムを好むならどうでしょう?私はそのカテゴリーに分類されます :)
もしHaskellに精通していれば、あなたはすでにType Classesを知っています。それは特殊な多様性を表す方法です。Haskellは私がこのデザインパターンを考えるための主な言語だったりします。最終的な成果物はまさにtype classesのようにはなりません。なぜならtype classesは本質的に静的ディスパッチに基づいています(C言語では拡張可能な方法で実装することは不可能です)。でも、おそらくその道すがらで類似点を見つけ出すことでしょう。
もしRustに精通しているなら、あなたにはtype classesもすでにお馴染みです!ただ、異なる名前で少し力の足りないトレイト(Traits)です。また、このパターンの最終結果はトレイトと全く同じになるわけではありません - それよりも、トレイトオブジェクトに似たものになるでしょう。Dynamic traitsについて知っているかもしれませんね、Rustが動的ディスパッチを実行する方法です。
もしあなたが関数型言語ではなくOOP言語に馴染みがあれば、心配はいりません!OOPにもtypeclassesやtraitsと非常に似た概念があります - インターフェイスです。
もしあなたが上記で触れたどの概念にも精通していないとしても、それはまったく問題ありません!基本レベルで見ると、概念は実際そんなに複雑ではありません。Typeclasses、Traits、Interfacesは、_オブジェクト_ではなく_アクション_を中心にポリモーフィズムをモデリングする方法なのです。私がtypeclass(またはtraitやinterface)を実装する型を求めるとき、私は_そのtypeclassの下にある特定のことができる_型を求めています。それだけが全てです。オブジェクトやオブジェクトの階層ではなく、能力を中心にしたポリモーフィズムです。
- 単相化とシンプルな抽象化による型安全
- 動的ディスパッチを使用することで拡張性があり、ライブラリAPIで使用可能
- _完全に通常の型_として使うことができるため、CTL(Cテンプレートライブラリ)のような既存のコンテナライブラリと一緒に使用することができます。
- アクション/能力/関数を中心に制約されたポリモーフィズム
このパターンを議論し、実装するにあたって、なにができるかのちょっとした味見をここでしましょう。
void print(Showable showable)
{
char* s = showable.tc->show(showable.self);
puts(s);
free(s);
}
typedef enum
{
holy,
hand,
grenade
} Antioch;
これはShow
タイプクラスです。型を文字列表現に変換する能力を記述しています。
私は上記で挙げたAntioch
型に対してprep_antioch_show
を定義することによってShow
を実装しました。これでAntioch
の値はShowable
に変換することができ、Showable
の型で動作するジェネリック関数を使うことができます。
これでようやく自体のデザインパターンの話に進むことができます。ここでは3つのコアパーツがあります。私はこれらの3つのパーツを実装することによって上述のShow
タイプクラスを説明します。
typeclass
構造体の定義
これは、タイプクラスに関連する関数ポインタを含む構造体です。Show
の場合は、ここではshow
関数だけを使っていますが、Show
が実装されている型の値を受け取り(つまりself
)、印刷可能な文字列を返す必要があります。
typedef struct
{
char* (*const show)(void* self);
} Show;
関数のラッパーが初めて呼ばれた際(特定の型をその型クラスのインスタンスに変換するため)、その特定の型の関数ポインタで構造体をstatic
デュレーションで作成します(一種のvtableです)。すべての型クラスインスタンスでこの構造体ポインタが使用されます。これに関する詳しい情報はimpl_
マクロの部分で後述します。
typeclass_instance
構造体の定義
これは型制約として使用される具体的なインスタンスです。型クラスへのポインタと、関数ポインタを渡すためのself
メンバーを含むべきです。
typedef struct
{
void* self;
Show const* tc;
} Showable;
impl_
マクロで型クラスを実装するために使用される
このマクロは、型安全性に関わる実際の重点テーマです。
これは、型クラス用に実装する型の情報と、その型用の正確な関数実装を受け取り、以下を行う関数を定義します。
- 実装のための型の引数をとる
- 提供された関数実装をチェックする。これは期待される厳密な型の関数ポインタに与えられた関数実装を格納することで行われます。
- これらの関数ポインタを格納するために型クラス構造体を
static
デュレーションをもって初期化する - 上記の型クラス構造体へのポインタおよび
self
メンバに関数引数を格納する型クラスインスタンスを作成し返します。
これらのルールに従って、impl_show
は次のようになります。
#define impl_show(T, Name, show_f) \
Showable Name(T x) \
{ \
char* (*const show_)(T e) = (show_f); \
(void)show_; \
static Show const tc = {.show = (char* (*const)(void*))(show_f) }; \
return (Showable){.tc = &tc, .self = x}; \
}
第3引数としてshow
実装を取っています。関数定義では、実装を型char* (*const show_)(T e)
の変数に保存しています。実装するのはT
で、具体的な型です。それはポインタ型でなければなりません。それがvoid* self
に格納されるからです。
(void)show_;
のラインは、コンパイラが発する未使用の変数の警告を抑制するためのものです。show_
は実際には使用されていません。それはタイプチェックの目的のためにだけ存在します。これら2つのタイプチェックの行は、任意の優れたコンパイラによって完全に排除されます。
それから単純に静的な型クラスを定義し、その中に関数ポインタを格納します。その後、x
引数と型クラス構造体へのポインタを含むShowable
構造体を作成し、返します。
型クラスと型クラスインスタンス構造体が定義されると、ユーザーは自分の型と型クラスのための関数実装にimpl_
マクロを呼び出すだけで済みます。それにより定義される関数の宣言は、ヘッダーに含むことができます。
以下は以前に定義したShow
型クラスを非常に神聖なenumに実装する例です。
typedef enum
{
holy,
hand,
grenade
} Antioch;
static inline char* strdup_(char const* x)
{
char* s = malloc((strlen(x) + 1) * sizeof(*s));
strcpy(s, x);
return s;
}
/* `Antioch*`に対する`show`関数の実装 */
static char* antioch_show(Antioch* x)
{
/*
注意: `Showable`の`show`関数はmallocで確保された値を返すことが期待されている
`Showable`のジェネリックユーザーは、`show`関数から返されたポインタを`free`<br><br>こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。<br>[https://dev.to/totally_chase/implementing-functional-type-safe-polymorphism-in-c-10b9](https://dev.to/totally_chase/implementing-functional-type-safe-polymorphism-in-c-10b9)