3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ぼくのかんがえるさいきょうのvoid *の使い方

Last updated at Posted at 2018-05-11

はじめに

記事への異論は認めます。

私はこれまでにC言語のポインタについて振り返った際や、オブジェクト指向を阻害する要素で、Cの**魔法の言葉void ***の説明をしてきました。
例えばガチャピンのようにスキージャンプや宇宙遊泳にもいけるような、なんでもできるスーパーマンです。でも*ガチャピンってなんだろう?*って考えると、彼は宇宙にいく為に生み出されたキャラクターではなかったはず。子供向け番組のマスコットキャラクターで、ただ色々優秀すぎてこんなことになってしまっただけです。

というわけで、ガチャピン=マスコットキャラクターが主軸であるように、私の中のvoid *の使いどころの主軸を整理します。
私の中では以下になります。
void * = APIを経由して、特定の誰かと誰かがデータをやり取りする場合に一番活躍する道具
例: void * pthread_create(void *arg)

後はその理由と完全に個人的趣向でさらしていく記事となります。
この趣向はvoid *は便利すぎる道具であることの話とかぶっている部分もありますが、その後色々なやり取りやデザインを通してブラシアップされたので記事にしました。

さいきょうのけつろんのだしかた

最強を決める手順は以下のようにいきたいと思います。

  1. ざっくりメリットデメリットの整理
  2. デメリットをつぶす用途のブラシアップ
  3. 上げた条件の上位互換を差し替える
    ⇒ぼくのかんがえるさいきょうのvoid *の使い方

ざっくりメリットデメリットの整理

メリット

一言でいうとこれです。とにかくなんにでも置き換えられる。
全てのメモリ領域アドレスはvoid *に代入でき、かつプログラムの扱うデータはメモリ領域に含まれるので、キャストさえすればなんでも表現できるデータ型となります。

位置さえあっていればそのままアドレスを受け渡すことも出来るし、データ側を意識させずに関数を経由させることも出来るし、実質知ってる人しかそのメモリ領域の中身がわからないんだから、データの隠ぺいにも使えます。

全てを表現できるデータ型といっても過言じゃないんでしょうか

デメリット

冒頭で紹介した以前の記事でも書いたように、なんでもできる⇒なんにでも使った結果制御できなくなる
ということが、ただvoid *の便利さだけに目がくらんで利用しまくる人に多いです。
というわけでデメリット。とにかくわかりにくくなる

なんにでも出来る⇒なんに使われているかぱっとわからない
何か知らないけどデータがあります。これなんとかして。
という依頼を受けて、即座にハイ喜んでと対処できる人は皆無でしょう。

デメリットをつぶすブラシアップ

シンプルなメリットデメリットが出てきましたので、デメリットをつぶす使い方を考えましょう。

わかりにくい

  1. 何に使われているか、目的を明確にする
  2. 不必要なところでは使わない

対処はこのくらいですかね。1は言わずもがな。
2の不必要なケースについては、**データの元ネタを利用者、提供者が知っているなら、互いに情報共有しておけばvoid *なんていらなくね?**ということで、直接void *を使うというケースは必要性がなくなります。

上記を踏まえて私がvoid *を使うケースは以下。

  1. **APIを経由して、特定の誰かと誰かがデータをやり取りする場合。**ただし、使い方はどこかに明記すること。void *は受け渡しのみで使うこと
  2. **特定のクラス(ファイル)のAPI内で使うことが確定している場合。**これはかなり自由度・好みが分かれるところだと思います。

以下詳細

APIを経由して、特定の誰かと誰かがデータをやり取りする場合。

例えばAPIを介してデータが受け渡されるケース。

void register(void (*func)(void *arg));
void transfer(void * data);

みたいな感じのAPIがあって、registerでとあるモジュールが関数を登録。
transferで登録関数を実行。transferを呼び出す側と関数登録をする側のデータ側に対する意識があっていれば、register/transferの実装としては型を知らなくても出来ます。
transferがもっと大事な機能を持つAPIになるならこの形は汎用的でですね。
(機能が特定されているならvoid *じゃなくていいとは思いますが。)

void * pthread_create(void *)がまさしくこれですね。好きなインプットでthreadが作れますよ!という。

これは使いすぎると怖い所もありますが、APIの設計自体が、インプットを与える利用者と、アウトプットを作成・登録する実行者が分かれている場合は、このAPIを利用する=データの認識を合わせるという意思表示になるので、情報が発散することはまずないはずです。

受け渡しのデータ内でvoid *を使いたいケースはあるか?

あるかもしれませんが、その場合は2.特定のクラス(ファイル)のAPI内で使うことが確定している場合の使い方と併用するし、
そのvoid *管理用の処理を別途API化することをお勧めします。

例えば、使いたいのはこのようなケースだと思います。

まずapplicationレイヤー的には扱うモジュールが複数あって、データの内容が違います。

application.h
struct application_connection {
	struct app_information info;
	void * middle_layer_info:
};
middle_layer_module.h
struct middle_layer_module_A {
	struct info_for_A info;
	void * device_info;
};

struct middle_layer_module_B {
	struct info_for_B info;
	void * device_info;
};

さらにこのdeviceも、いくつかの種類があって、扱うハード構成によって変わります。

device.h
struct device__A {
	struct device_for_A info;
};

struct device__B {
	struct device_for_B info;
};

こうなるので、void *が乱立してしまう。という話。用途としてはありそうですね。

乱立させる前にAPIで整理

これはやるなら各void *要素のAPIを用意してあげるだけで格段に印象が変わります。(たとえ中身がmallocだけでも)
middle_layer_infoに対する操作、device_infoに対する操作というように、void *の為の操作が表現できるなら表現してあげるだけで、遥かに頭を使わなくてよくなり、
影響範囲もバグの量も激減すること間違いなしです。
(定義順が変とか、もっとスマートなやり方があるとかはスルーでお願いします。)

application.h
struct application_connection {
	struct app_information info;
	void * middle_layer_info;
};

構造体実体は隠ぺいして、APIで利用します。

middle_layer_module.h
//void *用のAPIを用意
void * middle_layer_info_new(int middle_type);
void middle_layer_info_xxx(void * this);
...
void middle_layer_info_free(void *this);

deviceも同様。

device.h
void * device__A_new(void);
void device__A_free(void *);

void * device__B_new(void);
void device__B_free(void *);

中身のイメージはこんな感じ。

middle_layer_module.c
void * middle_layer_info_new(int middle_type); {
	if(type == module_A) {
		return middle_layer_module_A_new();
	} else {
		return middle_layer_module_B_new();
	}
}

struct middle_layer_module_A {
	struct info_for_A info;
	void * device_info;
};

void * middle_layer_module_A_new(void) {
	struct middle_layer_module_A * instance = malloc(sizeof(struct middle_layer_module_A));
	...
	if(device_type==device_A) {
		instance->device_info = device__A_new();
	} else {
		instance->device_info = device__B_new();
	}
}
...
void middle_layer_module_A_free(void *this);

struct middle_layer_module_B {
	struct info_for_B info;
	void * device_info;
};

void * middle_layer_module_B_new(void) {
	struct middle_layer_module_B * instance = malloc(sizeof(struct middle_layer_module_B));
	...
	if(device_type==device_A) {
		instance->device_info = device__A_new();
	} else {
		instance->device_info = device__B_new();
	}
	return instance;
}
...
void middle_layer_module_B_free(void *this);
device.c
struct device__A {
	struct device_for_A info;
};
void * device__A_new(void) {...}
void device__A_free(void *){...}


struct device__B {
	struct device_for_B info;
};
void * device__B_new(void) {...}
void device__B_free(void *) {...}

大分頭の使いどころが減りましたが、わかりにくさは否めないですね。
まだ改善の余地はありそう。

上げた条件の上位互換を差し替える

よし、デメリットであるわかりにくさは解消されたぞ!もういいだろう。
…まあCに理解のある人、運用をちゃんとしてくれる人ならこれで成り立ちますね。
ただ、なんでも入れられるvoid *を使うがゆえに、例えば似たようなクラス名を使っていたらキャストミスに気付かない等、気になる点はまだあるんですよね。

Update: APIを経由して、特定の誰かと誰かがデータをやり取りする場合。

こちらについての検討材料は1点。仲介役のAPI、本当に汎用性いるの?
pthread_create等はもうしょうがないですよね。
ただ自作のAPIで同じ構造体に対してしか使わないのにvoid *で定義しているなんてこと、ありませんか?

関数定義の面で考えるとvoid *とそうでないものの違いはそこまでありません、名前だけです。
正直この場合、void *にしない強い理由もする強い理由もないケースなんですよね。
なら踏み込んで考えましょう。敢えてvoid *を使うメリット、ありますか?

私は安易にvoid *を使うことは反対派なので(トラウマがあるので)、使うには強いメリットが欲しくなります。
ここに好みが出る気がしますが、私はメリットが弱いと思います。
いい面⇒データの種類が増えたとしても、そのまま対処可能
悪い面⇒キャストが面倒、何がやり取りされるかわかりにくい

ここで一つ質問。そのまま対処可能というけど、本当に追加データが加わるユースケースは確定していますか?
先が見えないならそのメリットって不明瞭ですよね。
もう一点。**追加データが出た場合に直す箇所って大規模ですか?**せいぜい関数定義の2行と最初のキャストじゃないかなと。

未来の可能性とボリュームを天秤にかけた結果でどうするかを決めるべきだとは思いますが、今回は大抵のケースではメリットが弱そうに感じてしまいます。
(未来があった場合の修正規模がすごいなら、最初から後者にしますけど。)

拡張性を持たせる場合、それが利用される可能性の高さ, その場合の規模を考える必要があるのではないかと思います。その結果、大体いらなくね?ってなる気がします。
どちらでもいいレベルですが、私に決定権があるならこんな感じですかね。

Update: 特定のクラス(ファイル)のAPI内で使うことが確定している場合。

今回記事を書いた最大の理由です。この用途は上位互換があるんです。

ちょうど例に出したvoid *乱立ケースを書き直しましょう。
前方宣言というテクニックでvoid *なしでかけちゃいます。
(定義順が変とか、もっとスマートなやり方があるとかはスルーでお願いします。)

まずは例のapplication_connectionを手直し。

application.h
struct application_connection {
	struct app_information info;
	MiddleLayer middle_layer_info;
};

MiddleLayerってなんでしょう。定義はこちら。
こんな風に構造体の中身は置いておいて、名前だけ定義することで関数に利用できます。
*中身は.cで書くので隠ぺいしたまま、void を使わずに表現が出来ます。

middle_layer_module.h
struct middle_layer;
typedef struct middle_layer * MiddleLayer;

//void *用のAPIを用意
MiddleLayer middle_layer_info_new(int middle_type);
void middle_layer_info_xxx(MiddleLayer this);
...
void middle_layer_info_free(MiddleLayer this);
device.h
struct device_A;
typedef struct device_A *DeviceA;
DeviceA device__A_new(void);
void device__A_free(DeviceA this);

struct device_B;
typedef struct device_B *DeviceB;
DeviceB device__B_new(void);
void device__B_free(DeviceB this);

後は.c内で中身を定義

middle_layer_module.c
struct middle_layer_module_A;
typedef struct middle_layer_module_A* MiddleModuleA;

struct middle_layer_module_B;
typedef struct middle_layer_module_B* MiddleModuleB;

struct middle_layer {
	union {
		MiddleModuleA moduleA;
		MiddleModuleB moduleB;
	} module;
};

MiddleLayer middle_layer_info_new(int middle_type); {
	MiddleLayer instance = malloc(sizeof(*instance));
	if(type == module_A) {
		instance->module.moduleA = middle_layer_module_A_new();
	} else {
		instance->module.moduleB = middle_layer_module_B_new();
	}
	return instance;
}

struct middle_layer_module_A {
	struct info_for_A info;
	union {
		DeviceA deviceA;
		DeviceB deviceB;
	} device_info;
};

inline MiddleModuleA middle_layer_module_A_new(void) {
	MiddleModuleA instance = malloc(sizeof(struct middle_layer_module_A));
	...
	if(device_type==device_A) {
		instance->device_info.deviceA = device__A_new();
	} else {
		instance->device_info.deviceB = device__B_new();
	}
	return instance;
}
...
void middle_layer_module_A_free(MiddleModuleA this);

struct middle_layer_module_B {
	struct info_for_B info;
	union {
		DeviceA deviceA;
		DeviceB deviceB;
	} device_info;
};

MiddleModuleB middle_layer_module_B_new(void) {
	MiddleModuleB instance = malloc(sizeof(struct middle_layer_module_B));
	...
	if(device_type==device_A) {
		instance->device_info.deviceA = device__A_new();
	} else {
		instance->device_info.deviceB = device__B_new();
	}
	return instance;
}
...
void middle_layer_module_B_free(MiddleModuleB this);
device.c
struct device__A {
	struct device_for_A info;
};
void * device__A_new(void) {...}
void device__A_free(DeviceA this){...}


struct device__B {
	struct device_for_B info;
};
DeviceB device__B_new(void) {...}
void device__B_free(DeviceB this){...}

この例だとunionが好きか嫌いかとかはあると思いますが、何よりこの使い方は、型が不定義な状態で関数が作れないからvoid *にした悩みを解決してくれるんですよね。
というわけで、void *が苦手な私としては、これが上位互換となるわけです。

APIを経由して… updateの副作用

この表現を上位互換と説明した理由がもう1つあります。

以前の記事で、私はこのvoid *表現をオブジェクト指向でAPI設計が出来る使い方と説明しました。
じゃあどちらがオブジェクト指向としてわかりやすいか、見比べてみましょう。

前方宣言
struct middle_layer;
typedef struct middle_layer * MiddleLayer;

//void *用のAPIを用意
MiddleLayer middle_layer_info_new(int middle_type);
void middle_layer_info_xxx(MiddleLayer this);
...
void middle_layer_info_free(MiddleLayer this);
void_*
//void *用のAPIを用意
void * middle_layer_info_new(int middle_type);
void middle_layer_info_xxx(void * this);
...
void middle_layer_info_free(void *this);

さて、オブジェクト指向の表現としてよりわかりやすいのはどっち?
というわけで、私の中で前方宣言が完全上位互換となりました。

実現方法としてはどちらもオブジェクト指向してますが、前方宣言を使った例はこれがなんのクラスか明記しているんですよね。
もうこれだけで格段に理解度、設計との紐づけやすさ、見やすさが上がると思いませんか?
格段に理解度が高まるコードにすると、バグ発生率・メンテナンス工数といった後工程の工数が格段に下がる。経験上では断言できます。統計とりたいくらい。
(注意:見やすさを重視しすぎて実装コストが数倍になったら本末転倒です。綺麗なコードは後工数削減のため)

私的な上位互換が出た時点で、もうこのケースでvoid *を使う理由がなくなってしまいました。
というわけで理由のない使い方は廃止します。

*結論:ぼくのかんがえるさいきょうのvoid の使い方

この使い方が一番で唯一です。

APIを経由して、特定の誰かと誰かがデータをやり取りする場合
例: pthread_create

他の表現については上位互換があるため、私なら別表現にすることをお勧めします。
といいつつその理由は簡潔に言うと見にくいからなので、強いこだわりのある方を優先いたします。
ただ自分の中のポリシーを定めるのは大事です。

なので、私のポリシーはこれです。(いい用途が見つかったら即変更しますけど)

余談:

多分理論にうるさい人は、こんな感じで「さいきょうのせっけい」「さいきょうのてすと」みたいに色々な最強装備を持っていると思います。
なので、そういった方と平行線の議論をするよりは、「ああ、これがこの人の最強装備なんだな」と思って、どちらでもいい所は譲ってあげてください。
きっと無駄な議論が減るし、肝心な時の自分の主張も通りやすくなりますよ。
(私は折れる派でありたいと思っています。実行できてるかは不明)

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?