0/2 はじめに void * ←ここ
1/2 クラス表現と継承
1/2 2018/07/01 再考しました 再考:クラス表現と継承
2/2 インターフェイス, オーバーライド
はじめに
前回投稿させていただいたC言語でインターフェイスクラスを実現 の中で、 C 言語によるオブジェクト記述法 の記事を張りました。
なるほどうまい事考えるもんだと思いましたが、正直かっちりオブジェクト指向言語の記述をしようぜ!ってのが僕が今やりたいことじゃないので、デザインパターントライにのめり込んで、初心を忘れる前にC言語でのオブジェクト指向について思うことをまとめようと思います。
こういった手法はみんな開発をよくするために考えられた素晴らしい道具達です。
でも、これだけ道具がそろっているのに、設計に関する議論は後を絶えないですよね。「C言語でのオブジェクト指向」も、結構賛否あるようで。
しかもそれらの根拠は大抵皆さん論理的な正論を言ってるんですよね。なのになぜ議論が絶えない?
私が思う答えが、「それがその人の好みだから」。
趣味趣向に優劣はつけにくいですからね。
当然守るべきポイントはあると思いますが。
なので今回は、あくまで自分の好みを中心に色んな観点で、何度かに分けて
C言語でのオブジェクト指向についてどういう風にすると便利なんだろうと考えてみようかなと思います。
完全に私の主観ですので悪しからず
オブジェクト指向の前に~ void *
今回は、本題の前にvoid *
について書きたいと思います。
こいつと関数ポインタがCらしい書き方でやればいいじゃん!に繋がってると思うので。
関数ポインタについてはインターフェイスクラスの時に記載します。
###どんなものか?
プログラム内部で扱う変数や関数はすべてメモリ上のどこかにデータが格納されており、その場所であるアドレスを示すのがポインタですね。
void *
はポインタ型なんですが、どんなデータ型のアドレスも格納することが出来ます。
なので、型が何か知らなくてもデータを受け渡せるのが特徴ですかね。
実際にデータを使う際は基本的に本来の型にキャストして使います。
オブジェクト指向でAPI設計が出来る。凄いぞvoid *
私の一番好きな使い方は、APIの識別子として利用する方法です。
先日作ったFlyweightライブラリを例に解説。
void * flyweight_define_class(size_t class_size, int is_threadsafe, struct flyweight_class_methods_s *methods);
void * flyweight_get(void * classHandle, void * constructor_parameter);
void flyweight_clear(void * classHandle);
-
flyweight_define_class
の戻り値で識別子として使うclassHandle
を取得 -
flyweight_get
やflyweight_clear
に紐づけた情報を識別子として渡し、操作を行う。
IDの代わりにvoid *
なんですね。利用者的にはなんでもいいけど。
この実装を見ると、実はこのvoid *
、ライブラリ内部で定義しているflyweight_class_factory
クラス構造体(あえてクラスと呼びます)の実体なんですよね。
これはその後のflyweight_get
, flyweight_clear
を利用する際に必要なデータが詰め込まれてます。
ライブラリの処理としては、flyweight_define_class
で構造体の実体を作成、使用者に渡します。
⇒その後flyweight_get
とflyweight_clear
で実体を引数で受け取って利用する。
なんかクラスのnew
とpublic method
みたいじゃないですか?
改めて、ライブラリ目線でのAPIの意味合いになります。
-
flyweight_define_class
⇒flyweight_class_factory
クラスのnew
メソッド。 -
flyweight_get
,flyweight_clear
⇒flyweight_class_factory
クラスのthis
付きstatic
メソッド
使用者にはflyweight_class_factory
のメンバー、メソッドはおろかクラスの存在すら意識せずに使えるというのが
凄いところだと思います。処理が実現できるならどんなクラスを定義しても差し替えてもいい。
オブジェクト指向言語で言うとインターフェイスクラスの実装クラスと同じ動作ですが、
'void *'が何者かを使用者が意識しなくてもいい分このケースではこちらの方が好きです。
好みの問題ですけどね。
インターフェイスクラスを使いたいケースも次の機会に記載します。
*void を利用したAPIは、使用者に存在も意識させずにクラス設計が出来る
何が実体かもうわからない! 荒れ放題の原因void *
void *
はとても便利なので、好きな人はそこかしこに使い出す傾向にあります。
私はそんなvoid *
にトラウマを持っています。
そのせいで、正直void *
は生成したコードの書いてあるファイル外で触ってくれるなとすら思います。
(これもvoid *
APIが好きな理由の一つです。使用者が絶対触れないから。)
そのトラウマの原因はこんな感じ。
まず最初に、リストを毎回実装するのが面倒なので、便利なpush, pop用のAPIが作られました。
struct list_s {
void * next;
void * prev;
void * data;
}
void list_push(struct list_s * list, void * data);
void * list_pop(struct list_s * list);
構造体をリスト化したい時は、構造体ポインタをメンバに持つlist構造体を定義して、最初にvoid * next
, void * prev
を定義してねというルールで使われてました。void *data
なのでどんなデータアドレスも入れることが出来ます。
で、とある人がこれを使ってメッセージをキューイングする実装をしました。
ここのメッセージ解析部分にバグがあり調査修正することに。これがトラウマ。
まずキューイングされたメッセージの一つでlist_popが使われていました。
なるほど、リストがあるのね。さて本体定義はどこだろう?xxx_msg.hみたいな定義はないな。
実装を見ながらgrepし、このリストの実体queueing_list_sを見つけました。
//2018/05/03追記 そもそもとしてlistのデータまで`void *`にするなよ! `struct queueing_list_s *`で定義しろよってのも
//わかりにくいポイントですね。`void *`は免罪符じゃないんだぞ!
struct queueing_list_s {
void * next;
void * prev;
struct yyy_info_s *info;
};
struct yyy_info_s {
struct list_s list;
};
…え~、なんかlist_s って文字が見えるんですけど、まじかよ。
grepでやけにlist_sが引っかかるからいやな予感はしてたんだよ。
…grepで引っかかる...? 他のメッセージの内容も確認します。
struct xxx_info_s {
struct list_s list;
...
};
....
struct __zzz_info_s {
struct list_s list;
...
};
....
struct __orz_info_s {
struct list_s Adata_list;
struct list_s Bdata_list;
struct list_s Cdata_list;
};
いたるところでメッセージデータの奥底にlist
が。これらのどこかで何か悪いデータが入り込んだ模様。
void *data
を介して色々な種類のlist_sが詰め込み放題。void *
を経由した先にまたvoid *
がどこまでも。本体はどこ?というかどうなれば正しいの?という状態
地道に構造を紐解き、やりたい構造を解析した結果、最終的には根本の実体が複数のqueueに積まれていて、free
した際にもう一方のqueueから抜いていないから不正アクセスであぼんとかだったと思います。
根本原因と改善
原因は不正アクセスだけど、もうあの構造でまともに動いてるのが奇跡だろと。なんであんな構造にしてしまったんでしょう。
私の思う理由はざっくりいうとただ一つ。
void *
の使い方に対するポリシーがどこにもないから
じゃあどんなポリシーを決めるとよかったんですかね。
例えばこんなポリシーを持ってデータ定義をしてあげるとか。
void *
を利用 ⇒ そのデータはすべて隠ぺいされてしまうようなもの。だから容易にアクセスさせない or アクセス方法を明確にするルールを設ける
上記ポリシーに従い、私の趣味で実装するなら以下とか。
- なんかAPIを作って、定義してある
void *
へのアクセスはすべてAPIを咬ませる。 -
void *
をヘッダーの構造体に書かないようにする。
後考えられるのは
- infoに対するparser APIを作ってあげる
- リストと対応するデータは近くに定義してコメントをつける。
- リストをtypedefで別名定義してあげる。
とかですかね。
使い方にポリシーが無いとトラウマを呼ぶvoid *
。自由すぎて
とんでもない野郎だな!なんて
逆にポリシーを決めて見にくさ、管理しにくさを解消できれば、便利なポインタなのかもしれませんね。
私はこの辺自信がないですが。
神にも悪魔にもなれるvoid *
。使い方は無限大?
void *
は使い方次第で神にも悪魔にもなれる、C言語開発者にとっての魔法のデータ。
自分はトラウマからAPIで隠ぺい出来なきゃほぼvoid *
は使わないですが、複雑怪奇に出来るほど色々なことが出来るんです。
関数ポインタも併せて上手に使えば、もうそれだけでオブジェクト指向でやりたいこともうまく表現出来るんじゃないかと。
上手にオブジェクト指向的コードを書くC言語開発者は沢山いる印象です。
そういったCの特徴を生かすのが好きな方に、「オブジェクト指向の記述で同じことが出来るよ!」と説いても、
そこは好みの話で平行線にになってしまうのかなと思ったので、まずvoid *
を紹介してみました。
ちなみに
void *
以外に、アドレスもデータ実体も使いますよって意図(多分)でunsinged int
辺りにアドレスもデータも突っ込むなんてことも見たことがあります。
ほんと何でもありですC言語。
この辺は好み的にも今回の趣旨的にも外れていくのでノータッチで
余談 私の好みの話
ここまでvoid *
について書いたのですが、正直な私の好みも書いておきます。
正直void *
は便利すぎて難しいです。全力で利用すると私のキャパを簡単に超えていきます。
なので、void *
の使いどころとしては、以下のように使い手が明確になっているケースに留めるようにしているつもりです。
- 関数ハンドル(識別子として使いだけ)
- 関数を跨いだデータ型のやり取り(proxyのようなAPIを挟んだデータのやり取り)
- その場限り
- 顧客都合
とりあえずこの4点以外でvoid *
を使った実装が出てきたら、「これはとても難しいプログラムだぞ!」とどっしり腰を据えてデバッグすることにしています。
正直コード量やテスト量が少なくなるというような定量的なメリットが見えない限りは、極力void *
は登場させるべきでないと思っています。
理由はvoid *
に対して少しでも頭を使う実装は、私レベルの脳みそではすぐ破綻するから。自由すぎて今は何?を覚えとかなきゃいけないので中々デバッグが辛いんですよね。
顧客要求なら色々工夫して凄い頑張りますけど、正直それ以外ならこの構成で頑張る前に別の表現方法が出来ないか検討するレベルで苦手です。
(逆に使う場所が決まってるなら好きにやっていいかと思ってしまうのが私の悪いところだなと思ったりして)
というわけで、例でいうところのlist_push
も、公開関数がvoid *
がメインのデータを絶対にlist_pushを使ってlistを詰めようなんて考えはしません。それでも管理できる人って本当凄いと思います。
この辺りの話に違和感なく**「void *
使おうよ!便利だよ?」と常に語れる方は極めて処理能力が優秀な開発者だと思いますし、そういった方が利用されるvoid *
の理論はとても理にかなっているものです。なので、そういったCの強みを完全に使いこなす開発者がいることを念頭に、C言語でのオブジェクト指向の使いどころを考える**ことが大事なことになるのかなと思います。
ただ、記事自体は私のスペックに合わせた、私にとってvoid *
が使いやすいパターンの例が基本となりますが。