世界の隅の開発室

 

◎  スポンサーサイト 

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

◎  CallFuncNでキャプチャ付きラムダ式を使う上での寿命の問題【cocos2d-x】 



runActionメソッドで扱うことのできるアクションの一つに、関数の実行があります。


auto func = CallFunc::create(CC_CALLBACK_0(NAMESPACE::METHODSYMBOL,OBJECT));
OBJECT->runAction(func);


こんな感じで扱うのが一般的ですかね。

CC_CALLBACKでインスタンスとメソッドのシンボルをバインドして、アクションを生成。

それをNodeインスタンスのrunActionにぶん投げる、って感じでしょうか。

もちろん、これだけでは何の意味もないので、大抵は遅延をかけたり、アニメーションと組み合わせたりします。

//遅延をかける例

auto func = CallFunc::create(CC_CALLBACK_0(NAMESPACE::METHOD_SYMBOL,OBJECT));
auto delay = DelayTine::create(DELAY_TIME);
OBJECT->runAction(Sequence::create(delay,func,nullptr));


ま、これなら基本的に普通に動くんですが、仮にキャプチャ付きのラムダ式を用いて関数アクションを生成する場合は、変数のキャプチャに関して注意しなくてはならない点があります。

まずはこのコードを見てみて下さい。


int n = 10;
auto func = CallFuncN::create([&](Ref* obj){OBJECT->setValue(n);});
auto delay = DelayTine::create(DELAY_TIME);
OBJECT->runAction(Sequence::create(delay,func,nullptr));


なんとなく動きそうな感じのプログラムです。

キャプチャしたthisとnを使って、遅延後thisにnを格納するプログラムな訳ですが

このコード、かなり高い可能性でBAD_ACCESSを起こします。

なぜかといいますと、キャプチャは参照受け取りを意味しますので

まあOBJECTはともかくとしてローカル変数のnも参照受け取りしているわけですね。

となると、nの寿命はもちろんrunActionが行われているメソッド内に限られますので、このラムダ式の実行時...つまりDELAY_TIME後にはnはスコープを出て解放されている可能性がかなり高いです。

となると、nを参照しようとした時点でBADACCESSって訳です。

というわけで、基本的にrunActionに渡すラムダ式ではキャプチャを使わないようにした方がいいと思います。

上記のコードを修正しますと


int n = 10; auto func = CallFuncN::create([n,OBJECT](Ref* obj){OBJECT->setValue(n);});
auto delay = DelayTine::create(DELAY_TIME);
OBJECT->runAction(Sequence::create(delay,func,nullptr));


となります。

runActionは別スレッドで動きますので、くれぐれも寿命や競合の問題には気をつけたいところです。

◎  タッチイベントをコード側からキャンセル【cocos2d-x】 

cocos2d-xではEventDispatcherを使ってこんな風にタッチイベントを取得します


//GameLayer.cpp

auto touchListner = EventListenerTouchOneByOne::create();
touchListner->setSwallowTouches(true);

touchListner->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan,this);
touchListner->onTouchMoved = CC_CALLBACK_2(GameLayer::onTouchMoved,this);
touchListner->onTouchEnded = CC_CALLBACK_2(GameLayer::onTouchEnded,this);
touchListner->onTouchCancelled = CC_CALLBACK_2(GameLayer::onTouchCancelled,this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListner, this);


すると、指が離れた、もしくは着信などによってタッチが中断された場合、

onTouchMoved から onTouchEnded or Cancelledにイベントが推移します。

これらはEventDispatcherによって制御されていますが

ここで、タッチイベントの発生とは関係なく、直接タッチをキャンセルさせたい場合
つまりコード側でタッチキャンセルイベントの実行をしたい場合はどうするんでしょうか。

もちろんonTouchCancelld();と呼び出したところで意味はありません。
このメソッド自体は単なるイベントを紐付けたNodeを継承したクラスのメンバ関数であり
タッチイベントのキャンセル自体とは関連がないからです。

ここで、ではさくっとeventDispatcherの実装を見てみます。

すると、こんなパブリックメソッドが見つかります。


//CCEventDispatcher.h

public:
////////
void dispatchEvent(Event* event);
////////



イベントを発生させてそうなメソッドです。さて、ここに適したイベントを渡せばキャンセルできそうな感じです。

さて、ここでeventDispatcherの実装を他にも色々と見ると、タッチイベント中断の為にここに渡すべきイベントは、Eventクラスを継承したEventTouchクラスであることが分かります。



//CCEventTouch.h

public:
////////
enum class EventCode
{
BEGAN,
MOVED,
ENDED,
CANCELLED
};

EventTouch();

inline EventCode getEventCode() const { return _eventCode; };
inline const std::vector<Touch*>& getTouches() const { return _touches; };

#if TOUCH_PERF_DEBUG
void setEventCode(EventCode eventCode) { _eventCode = eventCode; };
void setTouches(const std::vector<Touch*>& touches) { _touches = touches; };
#endif

////////



かなり見えてきました。
タッチのイベントの種類を示す列挙体と、アクセッサたちです。

これらを上手く構成してeventDispatcherに渡せば、上手く動きそうです。

というわけで、onTouchMoved中に何らかのイベントによってタッチをキャンセルしたい場合、コードはこうなります。


//GameLayer.cpp

void GameLayer::onTouchMoved(cocos2d::Touch* touch,cocos2d::Event* event){
////////
EventTouch cancellEvent; //インスタンス作成
cancellEvent.setEventCode(EventTouch::EventCode::CANCELLED); //イベント種類設定
cancellEvent.setTouches(std::vector<Touch*>{touch}); //タッチ情報セット(std::initializer:c++11)

_eventDispatcher->dispatchEvent(dynamic_cast<Event*>(&cancellEvent)); //イベントdispatch
////////
}


これで動くと思います。

◎  std::remove_ifでメモリリークした話 

どうも。

ちょっと、std::remove_ifでメモリリークを起こす件について軽く。

std::remove_ifは、STLアルゴリズムの一つで、コンテナを受け取り、3つ目の引数にとった関数オブジェクトを使って、「有効な要素」を前に詰めて、無効な要素の先頭イテレータを返すような仕様になっています。

使い方はこんな感じです。


std::vector<int> arr = {1,5,6,4,3,8};

std::remove_if(arr.begin(),arr.end(),[](int n){return n > 3;});


このケースですと、ラムダ式の返り値がfalse、つまり有効な要素の数が2つですので
1と3が前に詰められ、それ以降は無効な要素であるため、begin+2...つまり3つ目を指すイテレータが返ってきます。

そのため


std::vector<int> arr = {1,5,6,4,3,8};

arr.erase(std::remove_if(arr.begin(),arr.end(),[](int n){return n > 3;}),arr.end());


と書くと、無効イテレータからendイテレータまでが削除されて、晴れて1と3のみの要素を持つコンテナになります。


まあ、ここまではいいんですが、ではこれを動的要素にするとどうなるでしょうか。


std::vector<int*> arr = {new int(1),new int(5),new int(6),new int(4),new int(3),new int(8)};

auto invalidIt = std::remove_if(arr.begin(),arr.end(),[](int* n){return *n > 3;});
for(auto it = invalidIt; it<arr.end(); it++){
delete *it;
}

arr.erase(invalidIt,arr.end());



当然動的要素ですので、eraseする前にヒープから解放してあげないといけません。
なので、forでdeleteしています。
ちゃんと動くように見えるんですが
このコード、メモリリーク起こします。

その原因は、remove_ifの動作の厳密な正体にありまして。

この関数、無効な要素の先頭イテレータを返す、という動作をすると聞いたので
てっきり有効な要素を前に、無効な要素を後ろに並べ替える関数かと思っていたんですね。

しかし、実際には、たんなる有効要素の前進代入でして。

つまり、先ほどのint*ベクターの関数実行前と実行後の中身は

実行前
1,5,6,4,3,8

期待していた動作 1,3,5,6,4,8
実際の動作 1,3,6,4,3,8


というわけで、実際の動作を見て頂ければ分かる通り、スワップやソートでもなんでもなく、代入だったんですね。
コスト的に見れば当然なんですが、すっかりハマってしまいまして。

つまり、この関数実行後のコンテナの3番目〜6番目に対してdeleteをかけると、本来有効な要素である3に対しても解放処理を行ってしまい、結果的に2番目に位置する要素が無効なポインタになり、メモリリークを起こす、といった次第です。

そのため、deleteないし解放に関するクラスメソッドを実行する際は十分に注意する必要があります。

修正案としては、無効な要素にdeleteをして、nullptrを突っ込んだ後、remove_ifでそのnullptrを検知する、という感じですね。

コードも載ってけておきます。



for(int *n: arr){
if(*n > 3){
delete n;
n=nullptr;
}
}

arr.erase(remove_if( arr.begin(), arr.end(), [](int* n){return n==nullptr;} ),arr.end());





ではでは。

◎  C++11のTemplate Aliasesで、TTPやTMPがちょこっとだけわかりやすくなった気がする 

TTP = Template Template Parameters
TMP = Template Meta Programming
です。特に前者は分かり辛くてすいません


C++11から、Template Ailiasesというものが導入されました。

テンプレート機能が使えるtypedefと言った方が最も分かりやすいでしょうが、typedefよりも色々できることが増えています。
using構文というものです。

まずtypedefの置換としての扱い方から。
例えば以下のコードは同じ意味を持ちます。


//typedef int Type; 古来から伝わるシンタックス

using Type = int; //新たなシンタックス(こっちの方が直感的)



typedefはC言語からずっとお世話になっている構文ですが...
まず以下のようなコードを考えます。



typedef std::vector<int> vec_int;
vec_int a;



このコードは合法です。vec_intはstd::vector<int>のエイリアスとなります。
では次のコードはどうか?


typedef std::vector aliasVec;
aliasVec<int> vec;


これはコンパイルエラーとなります。
typedefはテンプレートをサポートしていません。
単なる置換じゃなくてエイリアスですから。

この問題も、usingを使えば解決します。


template<typename T>
using aliasVec = std::vector<T>;
////////////////
aliasVec<int> vec;


これでテンプレートクラスのエイリアスもつくれます。
これが可能になると、定数や型のテンプレート引数の埋め込みが可能になります。


template<typename T,typename U>
class myclass{
//なんか凄い処理
}

template<typename T>
using BoolMyClass = myclass<T,bool>; //引数二つ目はbool使うことが多い

///////

BoolMyClass<int> a; //myclass<int,bool>
BoolMyClass<double> b;
myclass<int,int> c; //引数二つ目を使う時は元のクラス名を使えば良い


この技法で、コンテナに独自のアロケーターを仕込んでも普段と同じ感覚で使うことができたりします。


で、こっからが本題なのですが
今まで少しばかりややこしい構文であった
Template Template Paramaters
Template Meta Programming
が、少しだけ分かり易く書けるようになります。


まず、Template Template Paramatersの基本から。


template<class T>
struct TemplateContainer{
T<int> container;
}

template<typename T>
class myclass{
};

////////////

TemplateContainer<myclass> a; //error!



あれ、typedefでも似たようなことをやったような。
テンプレートクラスのシンボル名だけを引数にとるようなテンプレートの使い方は違法なんですね。


このコードを動くようにするには
Template Template Paramatersという、テンプレート引数に「型」を取るのではなく、「テンプレートクラス」を取るような工夫を施す必要があります。


template <template <typename> class Container>
struct TemplateContainer{
T<int> container;
}



こうすることで、TemplateContainerという宣言が合法になります。

しかし、このTemplate Template Paramaters、クラスを引数に取るということで、当然その条件として引数の型が一致していなければなりません。

template <template <typename> class Container>

この宣言はテンプレート引数1つを取るクラスのテンプレート引数の宣言ですので(ややこしい)
デフォルトテンプレート引数が設定されている
std::vectorなどを渡すと、見かけ上同じ型に見えても、引数不一致でエラーを吐いてしまいます。

template <class T, class Allocator=allocator<T> > 
//std::vectorのテンプレート宣言、一方はデフォルト引数があるけども、実際の型としては引数を二つとる


これを解決する為に、引数側でAllocatorのデフォルト引数を設定する、という方法があります

template <template<typename T, class Allocator=std::allocator<T> > class TemplateContainer>

しかしこれだとAllocatorを使い分けたい時に困ります。
std::vectorではmyallocatorを、std::listではstd::allocatorを...といった状況ですかね。
あと私のような雑魚C++erには非常に分かり辛い構文になってしまっています。


ここで、先ほどのTemplate Aliasesを使うと
どうしてもコンテナの型はテンプレートクラス側で決めたいのに、アロケータだけは呼び出しもとで決定したい....!!
という超変態さんの欲求を満たすことができます。


template <template<typename> class T>
struct TemplateContainer{
T<int> container;
};

template <typename T>
using vector = std::vector<T, MyAllocator<T>>;
/////////////////////////
TemplateContainer<vector> a;




これで解決ですね!


また、Template Meta Programmingでよく見かける、ローカルエイリアス宣言された型
あるいは、ローカル定義されたクラスをスコープ参照する構文、これに不便を感じたことはないでしょうか。

もっとも身近なのは、コンテナのイテレータ型の取り出しでしょうか?

std::vector::iterator

ってやつですね。(型推論が追加されて使う機会も減りましたが)
今までも、typedefして簡単に使う工夫はされてきましたが、前述の通りtypedefはテンプレートに対応していないので、

typedef std::vector<int>::iterator vecIntIt;

のように、せっかくのテンプレートクラスに型を限定してしかシンタックスを宣言できませんでした。

しかし、Template Aliasesを使うことで、


template<typename T>
using vecIt = typename std::vector<T>::iterator;
/////////////
vecIt<int> = vec.begin(); //vec宣言済み



このように、複雑なイディオムを簡略化することができます。
あ、このときはtypenameで型であることを明示してくださいね。メタプログラミングに慣れている方なら忘れることはないと思いますが。


これらを更に応用して、より美しいTMPコードが書けるようになります。
以下は、引数に取った二つの型のうち、小さい方を選択するTMPコードになります。




template<bool b,class T,class U>
struct smaller_class{
typedef U type;
};

template<class T,class U>
struct smaller_class<false,T,U>{
typedef T type;
};

class myclass01{
public:
void operator()()const{
std::cout << \"実体化されたのは01でした\" << std::endl;
}
};

class myclass02{
public:
double a;
void operator()()const{
std::cout << \"実体化されたのは02でした\" << std::endl;
}

};

int main(){
smaller_class<sizeof(myclass01)>=sizeof(myclass02),myclass01,myclass02>::type()();
}



このコードを実行すると当然doubleの要素を持つmyclass02の方が大きいので

実体化されたのは01でした

と表示されます。


ですが、大変気持ち悪いと思います。TMPは嫌いです。
ただ、usingを使うと、少しだけ使い方が分かり易くなります。


template<typename T,typename U>
using smallClass = typename smaller_class<sizeof(T)>=sizeof(U),T,U>::type;

int main(){
smallClass<myclass01,myclass02>()();
}




こうしてあげることで、この呼び出しは先ほどのものと全く等価になります。

他にも色々メタプログラミングにおけるTemplate Aliasesの用途はたくさんあります。

どんどん気持ち悪くもどこか美しい、そんなTMPコードを書いていきましょう。


今回は長くなりましたがこの辺で。



back to TOP

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。