エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

実装方法についての考察:mixinについて

今回のビット操作を実装していて気付いたことをメモ。おもに明日の自分のために。


今回、勢いで実装をはじめたことで、みごとにコードの重複が発生してしまいました。
古い実装では数値を表すSignedUnsignedという二つのクラステンプレートを用意していました。符号付きのSignedでは、値が更新されたときの符号拡張と、値を取り出すときにその拡張した符号を取り去る操作とが必要になります。一方符号なしのUnsignedの場合は原則として値が更新されたときにマスクして常に必要なビットだけが有効な状態になるようにしてやれば充分です。
このように符号のありなしで操作がかわるものの、ちがうのはそれぐらいでビット列として扱う場合は二つのテンプレートで差はなく、書き進めるうちに大量に重複コードを書くことになってしまいました。


こうなるのは早いうちから気がついていて、どうにかしないとと考えていたもののなかなかキレイな解決策が見つからず。


順当に。特殊化する必要のある操作を純粋仮想関数にして、それ以外の共通する操作を基底で定義すれば、一応目的は達せられるのですが。たとえば。

#include <iostream>

class Inc
{
public:
    void inc(int n = 1)
    {
        setValue(value() + n);
    }

    virtual void setValue(int value) = 0;
    virtual int value() const = 0;
};

class Foo : public Inc
{
public:
    Foo() : value_(0) {}
    int value() const { return value_; }
    void setValue(int value) { value_ = value; }

private:
    int value_;
};

int main(int, char* [])
{
    Foo foo;

    std::cout << foo.value() << std::endl;
    foo.inc();
    std::cout << foo.value() << std::endl;
    foo.inc(3);
    std::cout << foo.value() << std::endl;

    return 0;
}


しかしこれだとvtableを持つためオブジェクトが大きくなってしまいます。3ビットとか4ビットとか小さいものを扱いたいのに、それではもったいない。それにメンバにアクセスしたいだけなのに仮想関数を使うというのも大仰な感じがします。


どうにかするにあたって、別解として頭にあったのはRubyのmixinでした。こんな感じの。

module Inc
  def inc(n = 1)
    @value += n
  end
end

class Foo
  include Inc

  attr_reader :value

  def initialize
    @value = 0
  end
end

foo = Foo.new
print"#{foo.value}\n"
foo.inc
print"#{foo.value}\n"
foo.inc 3
print"#{foo.value}\n"


Inc内で、まだ定義されていない@valueを利用しています。これと同じことができないか?と考えてひねり出したのが次のようなコード。

#include <iostream>

template<typename T>
class Inc
{
public:
    void inc(int n = 1)
    {
        self().value_ += n;
    }

private:
    T& self() { return static_cast<T&>(*this); }
    const T& self() const { return static_cast<const T&>(*this); }
};

class Foo : public Inc<Foo>
{
public:
    Foo() : value_(0) {}
    int value() const { return value_; }

private:
    int value_;

    friend class Inc<Foo>;
};

int main(int, char* [])
{
    Foo foo;

    std::cout << foo.value() << std::endl;
    foo.inc();
    std::cout << foo.value() << std::endl;
    foo.inc(3);
    std::cout << foo.value() << std::endl;

    return 0;
}


IncTに継承されることを前提として、Tの持っている要素には自分自身をTにダウンキャストしてアクセスするというもの。自分でも乱暴な方法だとは思うのですが。それでも機能してくれました。
しかしながら。どうしても居心地の悪さがのこるのと、テンプレート引数にテンプレートを使おうとしたときに面倒が増えてしまうのとで、さらに書き直すことに。


結果的には次のような形に。ネットを検索してみれば、この形が一般的なようなのですが。

#include <iostream>

class Foo
{
public:
    Foo() : value_(0) {}
    int value() const { return value_; }

protected:
    int value_;
};

template<typename T>
class Inc : public T
{
public:
    void inc(int n = 1)
    {
        T::value_ += n;
    }
};

int main(int, char* [])
{
    Inc<Foo> foo;

    std::cout << foo.value() << std::endl;
    foo.inc();
    std::cout << foo.value() << std::endl;
    foo.inc(3);
    std::cout << foo.value() << std::endl;

    return 0;
}


ただ、これもこれで、個人的にはすっきりしないでいます。

上の(Rubyを含めた)三つの例では、使い回したい操作を実装したIncは実体化せず、Incを混ぜ込んだFooが実体化されますが、この例ではFooが実体化されずIncが実体化されます。
ということは。関連のないFooBarというクラスにIncを混ぜ込んだ場合、どちらもIncとして実体化されることになりますし、またFoo単体、Bar単体では(当たり前ですけれど)Incの操作は使えません。


ネットで探してみたところ。共通するコードをマクロにしておいてそれを展開する、という方法も提案されていました。結局は実装を再利用したいわけなので、継承である必要はないわけで。これもこれでC++っぽいのかな、と。ただやっぱり読みづらいとは思うところ。

#include <iostream>

#define INC             \
    void inc(int n = 1) \
    {                   \
        value_ += n;    \
    }

class Foo
{
public:
    Foo() : value_(0) {}
    int value() const { return value_; }

    INC // mixin

private:
    int value_;
};

int main(int, char* [])
{
    Foo foo;

    std::cout << foo.value() << std::endl;
    foo.inc();
    std::cout << foo.value() << std::endl;
    foo.inc(3);
    std::cout << foo.value() << std::endl;

    return 0;
}

バッドノウハウ

上で自分自身をダウンキャストする例を示しましたが。「テンプレート引数にテンプレートを使おうとしたときに面倒が増えてしまう」のはどんなときか、という話。


Fooをテンプレート化したとします。たとえばこんな感じ。

template<typename T, typename U>
class Inc
{
public:
    void inc(T n = 1)
    {
        self().value_ += n;
    }

private:
    U& self() { return static_cast<U&>(*this); }
    const U& self() const { return static_cast<const U&>(*this); }
};

template<typename T>
class Foo : public Inc<T, Foo<T> >
{
public:
    Foo() : value_(0) {}
    T value() const { return value_; }

private:
    T value_;

    friend class Inc<T, Foo<T> >;
};


このばあい、Inc >というコードが不格好です。なので、Incの第二引数はテンプレートをとるようにしてみます。

template<typename T, template<typename> class U>
class Inc
{
public:
    typedef U<T> derived_type;

    void inc(T n = 1)
    {
        self().value_ += n;
    }

private:
    derived_type& self() { return static_cast<derived_type&>(*this); }
    const derived_type& self() const { return static_cast<const derived_type&>(*this); }
};


これでこのように書けるようになりました。

template<typename T>
class Foo : public Inc<T, Foo>
{
    // ...
}


…のはずなのですが。friend宣言のところでエラーになります。

    friend class Inc<T, Foo>; // error!


宣言はFooの中でおこなわれていますが、この中ではFooFooというクラスとして扱われるため、クラステンプレートを期待する第二引数に合いません。


で。これを回避する方法。
Fooの中にFooが出てこなければいいので、Fooを別のテンプレートに押し込めます。

template<typename T>
class Foo;

template<typename T>
struct FooBase
{
    typedef Inc<T, Foo> base_type;
};


で、これをfriend宣言に使います。

    friend class FooBase<T>::base_type;


解決しました。



でもこれって、Incの引数でTが重複するのがいやだからとテンプレートに書き換えたのに、また重複を生んでいます。


結論。いらん努力…orz。