今回のビット操作を実装していて気付いたことをメモ。おもに明日の自分のために。
今回、勢いで実装をはじめたことで、みごとにコードの重複が発生してしまいました。
古い実装では数値を表すSigned
とUnsigned
という二つのクラステンプレートを用意していました。符号付きの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; }
Inc
はT
に継承されることを前提として、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
が実体化されます。
ということは。関連のないFoo
とBar
というクラスに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
の中でおこなわれていますが、この中ではFoo
はFoo
というクラスとして扱われるため、クラステンプレートを期待する第二引数に合いません。
で。これを回避する方法。
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。