ページ 11

ポインタがズレて代入されてしまう

Posted: 2013年2月15日(金) 18:05
by adu
STGで頻繁にインスタンスの生成、削除を繰り返してもメモリの断片化が起きないように、クラスを決められた場所に作ったリスト構造で管理しようとしています。

TaskList , Task という2つのクラスを作り、TaskListのインスタンス作成時に必要になりそうな領域(sizeof(Task) * 予想されるタスク数n)をまとめて確保し、その領域をsizeof(Task)ごとに区切り、合わせてn個のTaskのインスタンスをメモリ上に等間隔に作りました。

使用中タスクのブロックと空きタスクのブロックに分けて、前者を双方向リストで、後者を単方向リストで管理します。前者はの先頭はbegin_used、後者の先頭はbegin_freeとしてTaskListクラスのstatic const なフィールドとして記憶されます。TaskからShip(自機)、Shot(自機ショット)クラスに派生させ、nextポインタをbegin_usedから始まってまたbegin_usedに戻ってくるまでmove() や draw() といった動作をさせます。

Taskクラスのnew演算子を呼ぶことで、空きタスクリストの先頭の次へのポインタを返し、そのポインタの指すインスタンスを空きタスクリストの連結でスキップされるようにオーバーライドしました。

次に、Taskのコンストラクタで使用中リストにthisを挿入したいのですが、ここが思い通りにできません。画像は動作をVisualStudioのデバッガで確認しているところなのですが、3枚目→4枚目の代入が思うようにいっていないようです。

なぜこのようなことが起こるのですか?

画像 → http://www1.axfc.net/uploader/so/2794200
画像は5枚あり、パスワード付きzipでまとめてあります。
DL時のキーワードは adu 、展開時のキーワードは aduaduadu です。

よろしくお願いします。

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月15日(金) 21:38
by softya(ソフト屋)
シンプルな再現コードを用意出来ませんか?
部分的なコードではよくわからないです。

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月15日(金) 22:42
by adu
始めから貼るべきでした。
Vector r は位置ベクトル、Vector v は速度ベクトル、 ~func は move() で呼ばれる関数ポインタです。null なら r+=v をします。
TaskクラスとShip, Shotクラスの間にMoverクラスがあります。

Ship.h

コード:

#pragma once
#include "mover.h"
#include <cassert>

class Shot;

extern TaskList ship_list;
extern TaskList shot_list;

class Ship : public Mover{
public:
	Ship(	int graphics_,
			Vector r_ = Vector(0,0),
			Vector v_ = Vector(0,0),
			double collision_radius_ = 5,
			double speed_fast_ = 1,
			double speed_slow_ = 0.5,
			unsigned int life_ = 2,
			unsigned int power_ = 1,
			unsigned int bomb_ = 2 ,
			void (*generate_shot_func_) (Mover*) = 0)
		: Mover(graphics_ , r_ , v_,Circle(0.2)) ,
		speed_fast(15.0) , speed_slow(speed_slow_) , life(life_) , power(power_) , bomb(bomb_), generate_shot_func(generate_shot_func_){
		//リストの末尾に挿入
		this->next = ship_list.begin_used;
		this->prev = ship_list.begin_used->prev;
		ship_list.begin_used->prev->next = this;
		ship_list.begin_used->prev = this;

		this->task_list = &ship_list;
		//基本的にコントローラー制御
		operate_move_func = 0;
	}
	void *operator new(size_t s){
		return ship_list.fetch();
	}
	void operator delete(void* p){
		ship_list.erase(p);
	}

	void move() override;
	virtual void shot();
	void (*generate_shot_func) (Mover*);

protected:
	double speed_fast;
	double speed_slow;
	unsigned int life;
	unsigned int power;
	unsigned int bomb;
};

class Shot : public Mover{
public:
	Shot(	int graphics_,
			Vector r_ = Vector(0,0),
			Vector v_ = Vector(0,80),
			double collision_radius_ = 5,
			unsigned int power_ = 1
		)
		: Mover(graphics_ , r_ , v_,Circle(collision_radius_)) ,
		power(power_) {
		//リストの末尾に挿入
		this->next = shot_list.begin_used;
		this->prev = shot_list.begin_used->prev;
		shot_list.begin_used->prev->next = this;
		shot_list.begin_used->prev = this;

		this->task_list = &shot_list;
		//TODO : 引数に取れるようにする
		operate_move_func = 0;
	}
	
	void* operator new(size_t s){
		return shot_list.fetch();
	}
	void operator delete(void* p){
		shot_list.erase(p);
	}
	virtual void move() override;

	double power;
};

Task.h

コード:

#pragma once

class Task;
class TaskList;
class TaskIter;

class Task{
	friend class TaskList;
	friend class TaskIter;
	friend class Ship;
	friend class Shot;

public:
	Task(){}
	Task(TaskList &tl);

protected:
	Task* next;
	Task* prev;
	TaskList* task_list;

	void* operator new(size_t t){};
	void  operator delete(void* p){};
};

class TaskList{
	friend class TaskIter;
	friend class Ship;
	friend class Shot;

public:
	//freeは単方向リスト usedは双方向リスト
	TaskList(size_t,unsigned int);

	Task* fetch();
	void erase(void*);

	unsigned int size();
	bool empty();
	bool full();

	void move();
	void draw();

protected:
	Task* at(unsigned int);
	Task* begin_free;
	Task* begin_used;
	
	unsigned int max_task_num;
	unsigned int task_num;
	size_t size_task;
};

class TaskIter{
public:
	TaskIter(const TaskList*);
	
	const TaskList* operating_task_list;

	Task* target();
	Task* next();
	Task* prev();
	void proceed();
	bool end();
protected:
	Task* target_task;
};
Task.cpp

コード:

#include "Task.h"
#include "Mover.h"
#include "DxLib.h"
#include <cassert>

TaskList::TaskList(size_t size_, unsigned int num_){
	max_task_num=num_;
	task_num=0;
	size_task=size_;

	//動的に確保 charは1バイトなのでそのまま乗算
	char* buffer = new char[size_*(num_+2)];
	
	//初期状態 : begin_used , begin_free , フリータスク , フリータスク , フリータスク ...
	begin_free=(Task*)(buffer+size_);
	begin_used=(Task*)buffer;

	//リスト構造を作る
	//freeは単方向リスト usedは双方向リスト

	//usedリストはこのような状態が空であることをあらわす
	begin_used->next = begin_used;
	begin_used->prev = begin_used;
	begin_used->task_list = (TaskList*)buffer;
	
	//freeリストは単方向なのでprevはさわらなくてよいはず(?)
	for(unsigned int i=1; i<num_+1; i++){
		Task* free_task = at(i);
		free_task->next = at(i+1);
		free_task->task_list = (TaskList*)buffer;
	}
	at(num_+1)->next = at(1);
	at(num_+1)->task_list = (TaskList*)buffer;
}

Task* TaskList::fetch(){
	//満杯ならエラー
	assert( !full() );
	
	//freeの先頭から抜き出し
	Task* rtn_task = begin_free->next;
	begin_free->next = begin_free->next->next;
	
	//used末尾への挿入はコンストラクタで行う

	rtn_task->task_list = this;
	
	task_num++;

	return rtn_task;
}

void TaskList::erase(void* useless_task){
	//空ならエラー
	assert(task_num>0);

	Task* p=static_cast<Task*>(useless_task);
	//usedから削除
	p->prev->next = p->next;
	p->next->prev = p->prev;

	//free先頭へ挿入
	p->next = begin_free->next;
	begin_free->next = p;

	task_num--;

	return;
}

Task* TaskList::at(unsigned int n){
	return (Task*)( (char*)begin_used + n*size_task );
}

unsigned int TaskList::size(){
	return task_num;
}

bool TaskList::full(){
	return begin_free->next == begin_free;
}

bool TaskList::empty(){
	return begin_used->next == begin_used;
}

void TaskList::move(){
	for(TaskIter ti(this); !ti.end(); ti.proceed() ){
		Mover* p = static_cast<Mover*>(ti.target());
		p->move();
	}
}

void TaskList::draw(){
	for(TaskIter ti(this); !ti.end(); ti.proceed() ){
		Mover* p = static_cast<Mover*>(ti.target());
		p->draw();
	}
}

TaskIter::TaskIter(const TaskList* tl){
	operating_task_list = tl;
	target_task = tl->begin_used->next;
}

Task* TaskIter::target(){
	return target_task;
}

Task* TaskIter::next(){
	return target_task->next;
}

void TaskIter::proceed(){
	target_task = next();
}

bool TaskIter::end(){
	return target_task == operating_task_list->begin_used;
}

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月15日(金) 22:45
by softya(ソフト屋)
このままだと動かないので再現コードとはいえませんので、バグが再現できるmain もつけて下さいね。
あと、もう少し余分なものは削ってあるとベストです。

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月15日(金) 23:29
by ISLe
adu さんが書きました:TaskList , Task という2つのクラスを作り、TaskListのインスタンス作成時に必要になりそうな領域(sizeof(Task) * 予想されるタスク数n)をまとめて確保し、その領域をsizeof(Task)ごとに区切り、合わせてn個のTaskのインスタンスをメモリ上に等間隔に作りました。
sizeof(Task)のサイズのメモリ領域に、ShipクラスやShotクラスを構築したら確実にオーバーランしますけど、そんな単純な話ではないですよね。

コンストラクタで初期化されていない領域をクラスオブジェクトとしてアクセスしていますし、期待通り動いたとしてもたまたまかと。

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月15日(金) 23:33
by softya(ソフト屋)
あぁ、ここで確保しているんですね。
char* buffer = new char[size_*(num_+2)];
コンストラクタも動作していないし動いているのが奇跡かもしれません。

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月15日(金) 23:55
by ISLe
adu さんが書きました:次に、Taskのコンストラクタで使用中リストにthisを挿入したいのですが、ここが思い通りにできません。画像は動作をVisualStudioのデバッガで確認しているところなのですが、3枚目→4枚目の代入が思うようにいっていないようです。
代入自体は間違っていないみたいですけど。

thisのアドレスと実際に代入されるアドレスが違うという話ですかね。
Task*へアップキャストされるためでしょうけど。

きちんとクラスオブジェクトの構築・解体していないのでnext,prevメンバのオフセットのズレを吸収できてないということですかね。

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月16日(土) 17:50
by ISLe
オーバーライドしたnewで返す値と、インスタンス化した際のnewの戻り値(あるいはthis)が全部一致する前提で書かれていますよね。
これらのポインタすべてがクラスオブジェクト構築のために確保されたメモリ領域の先頭を指すとは限らないので、このコードではnext,prevの内容が(全体を通して見た場合)保証されません。

コンテナを用意して、コンテナ内部に確保したメモリ領域にクラスオブジェクトを構築する必要があります。
next,prevはコンテナのメンバとし、クラスオブジェクトのライフサイクルの影響を受けないようにします。

わたしのブログに同じようなものを作ろうとした記事があります。
中途で放置されてますが、基本は押さえていると思うので参考にできるところは参考にしてください。

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月16日(土) 21:26
by adu
softya(ソフト屋) さんが書きました:このままだと動かないので再現コードとはいえませんので、バグが再現できるmain もつけて下さいね。
あと、もう少し余分なものは削ってあるとベストです。
すみません

thisポインタは配列のように単純に先頭を返すとは限らないのですか。
内部的に this は xxx+t 、xxx->next は *(xxx + n) 、 xxx->prev は *(xxx + p) のようになっているのでしょうか…(+はビット単位の移動)
そしてそのズレ(t,n,mなど)は継承によって変わってくるのでしょうか…
softya(ソフト屋) さんが書きました:コンストラクタも動作していないし動いているのが奇跡かもしれません。
ISLe さんが書きました:コンストラクタで初期化されていない領域をクラスオブジェクトとしてアクセスしていますし、期待通り動いたとしてもたまたまかと。
これらのご回答についてはnewで空きクラスが返された直後(?)にコンストラクタが呼ばれるはずだから大丈夫だと思っていたのですが…

紹介された方法などいろいろやってみます

Re: ポインタがズレて代入されてしまう

Posted: 2013年2月17日(日) 02:00
by ISLe
adu さんが書きました:内部的に this は xxx+t 、xxx->next は *(xxx + n) 、 xxx->prev は *(xxx + p) のようになっているのでしょうか…(+はビット単位の移動)
そしてそのズレ(t,n,mなど)は継承によって変わってくるのでしょうか…
クラスのメンバの並びやオフセットがどうなるかは分かりません。
PODと呼ばれる特定の条件を守った構造体でなければ、メンバの並びやオフセットは特定できません。
構築されたクラスオブジェクトへのポインタやthisがどこを指すかとか、どのメンバがどれだけのオフセットを持つかとか考えてはいけないのです。
adu さんが書きました:これらのご回答についてはnewで空きクラスが返された直後(?)にコンストラクタが呼ばれるはずだから大丈夫だと思っていたのですが…
それはShipクラスやShotクラスの内部の話ですね。

TaskListのメンバ関数で、char配列に対してTask*でアクセスしてます。
ここはクラスオブジェクトが構築される前の領域であり解体された後の領域です。
オフセットのズレと関係なしに内容は保証されません。


operator newで返すポインタと、operator deleteの引数で受け取るポインタは、オブジェクトを格納するのに十分な大きさのメモリ領域の先頭アドレスです。
基準として使えるのはこのアドレスしかなく、このアドレスからインスタンスにアクセスすることはできません。
めんどうくさいですが、一回書けば済むものなので頑張ってください。