LoginSignup
3
0

More than 1 year has passed since last update.

アジャイルソフトウェア開発の奥義のDelayedTyper.javaをC++で書く

Posted at

概要

ロバート・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関数の使用を強制する。
思ったより複雑な話だった。

command.hpp
    //省略
class command: public std::enable_shared_from_this<command>
{
public:
    //省略
    virtual void execute() = 0;
};
DelayedTyper.hpp
    //省略
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++に置き換えるとこうなる。

DelayedTyper.hpp
    //省略
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だって偉い人が言ってた。

DelayedTyper.hpp
    //省略
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)というやつ。
本当に使うことがあったんだ。

DelayedTyper.hpp
    //省略
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関数でオブジェクトを作るのが定石とのこと。

DelayedTyper.hpp
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を継承させることにした。

command.hpp
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

3
0
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
0