せんちゃの修行日記~C++での設計方法について考える~

アバター
せんちゃ
記事: 50
登録日時: 15年前
住所: 江別市東野幌町
連絡を取る:

せんちゃの修行日記~C++での設計方法について考える~

投稿記事 by せんちゃ » 12年前

コンシューマー開発に参加してからそろそろ4ヶ月経とうとしています。
3回目の給料もはいったので5ヶ月めも近いか…


会社の先輩に考え方などを学び、
ネットで様々なゲーム業界のプログラマーさんのブログなども参考にしつつ…

この頃どういう設計が良い設計と言えるのかわからなくなってきました。
人によっては「C++を使いこなそうとしないプログラマーが多いのは嘆かわしい」と言う一方、
「Cだけでも十分ゲームは作れる」という主張もあります。

私はC++で書いていますが、何をもってして「C++で書いてる」というのかはわかりません。
私がプロジェクトに参加した段階で土台は出来上がっていたので、上の部分を作るのにテンプレートは必要ないのでテンプレートは使っていません。
STLも今回は未使用です。iostreamとかsstreamとかもmallocが禁止されているので使えません。


となるとクラス設計くらいしかC++の機能は利用しないのですが、
まぁこれを「C++を駆使して書いている」と言えるのかは甚だ疑問です。
今回は様々な意見や主張をまとめて、それを自分なりに考えていこうという日記。



・「C++を駆使して作っていこう」という思想について
つまりはSTLを駆使して作る、という話なのだと思います。
あとは継承でしょうかね、オーバーライドすれば下を変えるだけで上のクラスはすべて治せるので、しっかり設計すれば大変効率的です。
テンプレートもスクリプトエンジンなどを作るなら動的確保が常に行われる代物ですので必要不可欠です
しかしここで一点の問題が生まれるのは

・STLはちょっと敷居が高い
という点
単にコンテナとイテレータとアルゴリズムと関数オブジェクトを備えたライブラリ、といっても
やはり仕様がやや複雑なのでプログラマーでも新人とかは知らない人も多いと思います。

更に準じて
・テンプレートはコードサイズが爆発的に増える危険性がある
というものと
・エラーメッセージが読めるかどうか

などの問題が生まれます。

コードサイズが増えるのは致命的です。
ゲームをつくる場合、数十個もの個別のオブジェクトをつくる場合があります。
マリオで例えるならクリボーやノコノコです。


これら一個一個を作るにあたって作成したクラスが定義されている.oファイルをmapファイルから確認したとして、
クリボー.oファイル、ノコノコ.oファイルがそれぞれコード領域だけで50KBくらい食っていたらどうしましょうか。


実は今の仕事で近いことが起きています。
何層にもわたるテンプレートとマクロによるコードの出力により、実際のステップは200行くらいしかなくても
コンパイルすると展開され、膨大なコードサイズになって出力されます。


クラス階層も非常に深く、下の仕様がわかっていないと上が作れない作りになっていると尚更です。
そういったコードのメンテナンスは引き継いだときの対応は困難の極みです。



動的確保もゲームシーンの中でバンバンやるようなものではないです。
ダイナミックにリソースを読み込んでは破棄して...ということをやらなければいけないゲームも当然ありますが、
やらなくても作れるゲームならやるべきではないです。

メモリーリークの原因になり得るし確保できなかったときどうするのか、という話にもなってきます。
ダイナミックロードを採用するならあらかじめメモリブロックを確保してそこからやりくりしていくのが理想です。




では
・「ベタにCでコーディングしていく」
というのはどうでしょうか

インヘリタンスやポリモフィズム、カプセル化はできませんので
グローバル変数があちこちに散りばめられてどこでデータが変わるかわからなくなるかもしれないし、
似たようなロジックを何回も書く事になるのかもしれない。
テンプレートはなくてもマクロはテンプレートよりなんでもできるので、もしかしたらテンプレート以上に危険なのかもしれない。



これまた様々な問題がでます。



しかしC言語でしばしば話題になるグローバル変数ですが、
グローバル変数の何がいけないかは「どこでも値が変わるかもしれない」という点にあります。
でもこれはクラスにも設計次第では起こりえます。

set~メソッドがそれにあたります。
get~メソッドは参照するだけなので問題ありません。参照やポインタで渡すならちょっとまてよ!となりますが
しかしsetというメソッドを作るということは「どこからでも値を書き換えられる可能性が生まれる」ということにもなります。
つまりこれはカプセル化できていない、ということになります。
setメソッドは不正な値が入った時にASSERTで止めるなどの例外処理を入れることができるので直接代入するよりはよっぽどマシですが、
結果的にこういう窓口をバンバン作ると気にしなければいけない部分が増えるのであまり良いとは言えません。


オブジェクトとオブジェクトが関連付けされ、そのネストが深くなるほどグローバル変数以上にややこしくなる可能性もありそうです。
しかしながらどこかで初期値は入れなければいけませんので、どこかしらでデータの初期値を入れる必要があります。
だからsetを作るな!とは言い難いのです。


グローバル変数のようにどこでも書き変わる可能性があるものはどこでも書き換えられるようにしないように
窓口のみ提供して、そこからしか参照はしない。というルールを作れば管理は非常にしやすくなります。
別のファイルから値が変わるような作りも極力避ければ問題は出ません。



以上のことから私は
「オブジェクト指向設計でもメタプログラミングでも、ベタCな設計でも開発者の作り方次第」という至ってシンプルな結論に至りました。
オブジェクト指向でクラス設計をするのであれば継承のネストは深くしないように設計する必要がありますし、多重継承も避けるべきです。
下のクラスが何をやっているのか上のクラスはわからないように作るべきで、下をわからないと上がいじれない作りもこれまた良くない。

テンプレートはアプリケーションの骨組みを作るときに利用するなら効果を発揮できますが
コードサイズを考えるならどこでも使うべきではありません。

グローバル変数がどこからでも変わりうる値なら決まった窓口しか用意せず、
そこからしかデータを受け取れないようにすれば良いのではないか。



個々のオブジェクトがあるデータの状態の変化を見て各自で挙動をとるように設計するのであれば
公開されたデータがどうしても必要になるので静的なデータは避けて通れません。
そこで無理やりローカルにしようとしたり、変に参照を渡して~とかやるくらいならば
いっそグローバルにしてしまったほうがメンテはしやすそうです。



C++の機能を上手に使うから正しい~とかベタCはダメ~とかは
好みでどうぞ、で良いような気がしてきました。
メンテナンスしやすいように作ってるならどっちでも良くて、しづらいならどんな方法でも良い作りとは言えないのではないのかなぁと


そうなると「言語って関係ないよね」という言葉に戻ってくるのかもしれませんが、
まだまだ私は答えを見つけるために試行錯誤していく予定です。


まだまだ私はC++の落とし穴に引っかかっていないので、いろいろと得をしたり怖い思いをしながら
学習して自分なりの設計に対する考え方を作っていきたいものです



しかし長い文章になってしまった…
ではでは(o・・o)/
最後に編集したユーザー せんちゃ on 2013年2月28日(木) 01:20 [ 編集 1 回目 ]

ISLe
記事: 2650
登録日時: 15年前

Re: せんちゃの修行日記~C++での設計方法について考える~

投稿記事 by ISLe » 12年前

制約の多い環境でC++の何がいちばん便利かと言ったら演算子のオーバーロードですかね。
なかなか他の言語にはない機能でもありますし。

C言語でカプセル化は可能だということは声を大にして言いたい。
C言語でグローバル変数をまったく使わずにプログラムを作れますし、C++でもグローバル変数は問題になります。
クラスを使って「カプセル化できるようになった」のではなく「カプセル化をスマートに記述できるようになった」のです。

アバター
h2so5
副管理人
記事: 2212
登録日時: 14年前

Re: せんちゃの修行日記~C++での設計方法について考える~

投稿記事 by h2so5 » 12年前

ISLeさんの補足で、C言語でよく見るカプセル化の実装をひとつ。

CODE:

/* foo.c */
typedef struct {
	int bar;
} Foo_;
typedef struct Foo_ Foo;

void createFoo(Foo** ptr)
{
	*ptr = (Foo*)malloc(sizeof(Foo));
}

void deleteFoo(Foo* ptr)
{
	free(ptr);
	ptr = NULL;
}

int getBar(const Foo* ptr)
{
	return ptr->bar;
}


/* foo.h */
struct Foo_;
typedef struct Foo_ Foo;

void createFoo(Foo** ptr);
void deleteFoo(Foo* ptr);
int getBar(const Foo* ptr);


/* main.c */
#include "foo.h"

int main()
{
	Foo* instance;
	createFoo(&instance);
	
	//instance->bar;    /* 直接のアクセスはできない */
	getBar(instance);   /* "メンバ関数"経由で取得できる */
	
	deleteFoo(instance);
}

アバター
せんちゃ
記事: 50
登録日時: 15年前
住所: 江別市東野幌町
連絡を取る:

Re: せんちゃの修行日記~C++での設計方法について考える~

投稿記事 by せんちゃ » 12年前

>ISLeさん
>h2so5 さん

C++に近いことはCでもできる、ということですね。
実際のところ

CODE:

typedef struct tag_A{
	int m1;
	int m2;
	int m3;
	int m4;
	void ( *initialize )( struct tag_A* _this );
}A;
というような関数ポインタをメンバに入れてあげれば

CODE:

void A_initialize_01( A* _this )
{
	_this->m1 = 0;
	_this->m2 = 0;
	_this->m3 = 0;
	_this->m4 = 0;

	printf( "[A]初期化01が呼ばれた \n" );
}


void A_initialize_02( A* _this )
{
	_this->m1 = 0;
	_this->m2 = 0;
	_this->m3 = 0;
	_this->m4 = 0;

	printf( "[A]初期化02が呼ばれた \n" );
}
といったようなvirtualっぽい実装もできますし、

CODE:

typedef struct tag_B_SuperA{
	A a;

	int m5;
	int m6;
	int m7;
}B;
とすれば継承っぽくもなるので、
Cでも十分オブジェクト指向的な作り方が可能だという認識はあります。
(privateのようなアクセス制限をつけることはできませんが。。。)

上記のプログラムでのA* _this のようなものを暗黙的に持ってくれてたりするのが
C++のクラスなのかなと考えると、C++のクラスはC以上にプログラマーにわかりやすく一連のことをやってくれているのかなぁと感じます。


日記にはCではカプセル化ができないと書いたものの、
作り方次第で同じことを実現する方法はあるんだなという認識は今のところ持っていると思います。

YuO
記事: 947
登録日時: 14年前

Re: せんちゃの修行日記~C++での設計方法について考える~

投稿記事 by YuO » 12年前

オブジェクト指向は物の考え方なので,Cでオブジェクト指向プログラミングをすることも,(面倒ですが)可能です。
ADT (Abstract Data Type) などの基本的な考え方は,構造化プログラミングの世界でも同じことなので。

で,CでもC++でもクラス指向のオブジェクト指向ができるような作りにしてあるのがWindowsのCOMです。
CではlpVtbl経由で,this相当を最初の引数に渡しますが,C++ではそのあたりが隠蔽されます。
# メッセージパッシングによるオブジェクト指向をやろうとしたのがWindowsのGDIですが……。


STLは,同じものを作るのであれば使った方がよいかもしれません。
べたで書いてもstd::sortを越える速度を出すのは難しいこともあります。