概要
ロバート・C・マーチンの「アジャイルソフトウェア開発の奥義」の13章。
DelayedTyper.javaをC++にしようとしたら嵌りました。
「Effective Modern C++」に答えがありました。
・発端:何度も使うnewでメモリーリーク。
・shared_ptrでいいんでしょ。
・あれ?thisはどうする?
・enable_shared_from_thisを使う。
前提
・「アジャイルソフトウェア開発の奥義 第2版」が手元にあり、13章を読んでいる。
・C++14
結論
newの代わりにshred_ptrを使い、thisのところはshared_from_this()を使う。
コンストラクタは隠してshared_ptrを戻すfactory関数の使用を強制する。
思ったより複雑な話だった。
//省略
class command: public std::enable_shared_from_this<command>
{
public:
//省略
virtual void execute() = 0;
};
//省略
class delayedTyper:public command
{
public:
static std::shared_ptr<delayedTyper> create(const long its_delay, const char its_char)
{
return std::shared_ptr<delayedTyper>(new delayedTyper(its_delay, its_char));
}
void execute() override
{
printf("%c", m_Char);
if(!ms_stop) delayAndRepeat();
}
static void dt_main();
private:
long m_Delay;
char m_Char;
static bool ms_stop;
static activeObjectEngine* ms_engine;
delayedTyper(const long its_delay, const char its_char)
: m_Delay(its_delay),
m_Char(its_char)
{
}
void delayAndRepeat()
{
ms_engine->addCommand(sleepCommand::create(m_Delay, ms_engine, shared_from_this()));
}
};
あとは素直に置き換え。
発端:何度も使うnewでメモリーリーク。
13章のcommandパターン→Active Object パターンの流れでの話。
p.207のDelayedTyper.javaのdelayAndRepeat()を形式的にC++に置き換えるとこうなる。
//省略
class delayedTyper:public command, public std::enable_shared_from_this<delayedTyper>
{
public:
//省略
private:
//省略
void delayAndRepeat()
{
ms_engine->addCommand(new sleepCommand(m_Delay, ms_engine, this));
}
};
C++にはガベージコレクションがない。
このコードをDr.Memoryに通すと案の定怒られる。
~~Dr.M~~ ERRORS FOUND:
~~Dr.M~~ 0 unique, 0 total unaddressable access(es)
~~Dr.M~~ 0 unique, 0 total uninitialized access(es)
~~Dr.M~~ 0 unique, 0 total invalid heap argument(s)
~~Dr.M~~ 0 unique, 0 total GDI usage error(s)
~~Dr.M~~ 0 unique, 0 total handle leak(s)
~~Dr.M~~ 0 unique, 0 total warning(s)
~~Dr.M~~ 2 unique, 337 total, 16228 byte(s) of leak(s)
~~Dr.M~~ 0 unique, 0 total, 0 byte(s) of possible leak(s)
shared_ptrでいいんでしょ
問題のsleepCommandは自らをactive object のキューに入れることがある。
deleteしていいタイミングが分かりにくい。
こういうときはshared_ptrだって偉い人が言ってた。
//省略
class delayedTyper:public command, public std::enable_shared_from_this<delayedTyper>
{
public:
//省略
private:
//省略
void delayAndRepeat()
{
ms_engine->addCommand(std::make_shared<sleepCommand>(m_Delay, ms_engine, this));
}
};
あれ?thisはどうする?
これでactiveObjectEngine::addCommand()には、command* ではなく、shared_ptrを入力することになってしまった。
(commandはsleepCommandとdelayedTyperの共通の基底クラス。)
上記の最後尾の引数のthisはdelayedTyperクラスで、この先のどこかでshared_ptrにしないといけない。困った。
thisをどこかで
std::shared_ptr<delayedTyper> dt(this);
のようにしてしまうと、同じthisを扱う別々のshared_ptrを量産してしまう。
→ 同じthisを2回deleteしてしまう。そこから先は動作未定義。
enable_shared_from_thisを使う
「Effective Modern C++」にピッタリのことが書いてあった。p129付近。
enable_shared_from_thisを継承して、shared_from_this()を使う。
奇妙に再帰したテンプレートパターン(Curiously Recurring Template Pattern, CRTP)というやつ。
本当に使うことがあったんだ。
//省略
class delayedTyper:public command, public std::enable_shared_from_this<delayedTyper>
{
public:
//省略
private:
//省略
void delayAndRepeat()
{
ms_engine->addCommand(std::make_shared<sleepCommand>(m_Delay, ms_engine, shared_from_this()));
}
};
怒られなくなった。
~~Dr.M~~ NO ERRORS FOUND:
~~Dr.M~~ 0 unique, 0 total unaddressable access(es)
~~Dr.M~~ 0 unique, 0 total uninitialized access(es)
~~Dr.M~~ 0 unique, 0 total invalid heap argument(s)
~~Dr.M~~ 0 unique, 0 total GDI usage error(s)
~~Dr.M~~ 0 unique, 0 total handle leak(s)
~~Dr.M~~ 0 unique, 0 total warning(s)
~~Dr.M~~ 0 unique, 0 total, 0 byte(s) of leak(s)
~~Dr.M~~ 0 unique, 0 total, 0 byte(s) of possible leak(s)
shared_from_this()の仕組み
動作の仕組みについてはこちらを参考にさせていただいた。感謝。
公開するのはコンストラクタではなくfactory
上の仕組みからshared_from_this()が呼び出されるより前にshared_ptrをつくることが必須。
「Effective Modern C++」によるとコンストラクタはprivateにして、shared_ptrを返すfactory関数でオブジェクトを作るのが定石とのこと。
class delayedTyper:public command, public std::enable_shared_from_this<delayedTyper>
{
public:
static std::shared_ptr<delayedTyper> create(const long its_delay, const char its_char)
{
return std::shared_ptr<delayedTyper>(new delayedTyper(its_delay, its_char));
}
//省略
private:
delayedTyper(const long its_delay, const char its_char)
: m_Delay(its_delay), m_Char(its_char)
{
}
void delayAndRepeat()
{
ms_engine->addCommand(sleepCommand::create(m_Delay, ms_engine, shared_from_this()));
}
//省略
};
多重継承を避ける
今回の場合は実害は無いが、気分的に多重継承を避けたい。
あとsleepCommandにもこの継承は必要だったので、commandにこの機能がついているべきなのだろう。
delayedTyperの基底クラスのcommandにenable_shared_from_thisを継承させることにした。
class command: public std::enable_shared_from_this<command>
{
public:
command() = default;
command(const command& other) = default;
command(command&& other) noexcept = default;
command& operator=(const command& other) = default;
command& operator=(command&& other) noexcept = default;
virtual void execute() = 0;
virtual ~command()= default;
};
なんでstd::enable_shared_from_this<delayedTyper>ではなく、基底クラスでのstd::enable_shared_from_this<command>の継承でもいいか。
enable_shared_from_thisの型仮引数Tは、以下2点で使わるのでcommandならOK。
・shared_ptrのコンストラクタでenable_shared_from_thisの関数を呼んでいいかどうか判断するためのパターンマッチング
→ Tは本当になんでもいい。
・shared_from_this()が作るshared_ptrの型を決める
→ TはdelayedTyperがpublic継承しているクラスであればいい。
おわり
問題としては典型的なものだった。おかげでC++の標準の機能で解決できた。
余計にdelayedTyperをC++にした記事というのをみつけられなかったのが不思議。
参考
ロバート・C・マーチン「アジャイルソフトウェア開発の奥義 第2版」
スコット・メイヤーズ「Effective Modern C++」
コラム2. enable_shared_from_this
std::shared_ptrでthisを使いたい時に注意すること
cpprefjp - C++日本語リファレンス std::enable_shared_from_this