重複コードを避けたい
2017年8月、画像の膨張・収縮アルゴリズムについて調べたときに、双方のコードの重複が気になりました。
オブジェクト指向のポリモーフィズムによる実装を考えてみます。
膨張処理も収縮処理も、Y座標とX座標によるラスタスキャン・ループの部分は同じコードで重複しています。実際、片方のコードはコピペして作成してしまいました。
簡単なコードなので、重複していることは気になりませんし、このコードのままで良いと考えています。しかし、もう少し複雑なコードだった場合はどうでしょうか?いつまでも、コピペを続けますか?
オブジェクト指向のポリモーフィズム
共通サブルーチン化
重複を避けるひとつの方法は、意味のある重複部分をサブルーチンにすることです。処理Aと処理Bの中に共通処理Xがある場合に、共通処理Xをサブルーチン化して処理Aと処理Bから呼出します。
共通メインルーチン化
今回の場合は、ある一部の処理が共通しているのではなく、ある一部の処理が異なっているのです。メインルーチンが共通処理Xで、その中の処理Aと処理Bが異なっています。
そこで、呼び出す側のロジックを共通化して、『共通メインルーチン』を作ります。
両者は、共通処理は何かの視点が異なるだけです。
- 共通点を抽出 : 共通サブルーチン化
- 相違点を抽出 : 共通メインルーチン化
この『共通メインルーチン化』という言葉は、「オブジェクト指向でなぜつくるのか」という書籍のポリモーフィズムの説明で使っていました。私は、この言葉でポリモーフィズムという概念がすっきりしました。
ポリモーフィズム
重複コードを改善するために、オブジェクト指向好きなC++言語の実践的プログラマーが考える方法はポリモーフィズムによる実装です。
オブジェクト指向プログラミング(Object Oriented Programming)の三大要素
- クラス (カプセル化)
- 継承
- ポリモーフィズム
ポリモーフィズムは日本語で多態性と言い、同じメソッドを呼び出しても実際のインスタンス毎にその挙動を変化させるものです。
C++では、継承と仮想関数を利用することでポリモーフィズムを実現します。
ポリモーフィズムによる実装
3つのクラスを定義します。
- 画像基底クラス : class cImageBase ...共通メインルーチンを定義
- 画像膨張クラス : class cDilation.... 画像基底クラスを継承し、膨張画像処理を定義
- 画像収縮クラス : class cErasion ....画像基底クラスを継承し、収縮画像処理を定義
画像基底クラスは、共通メインルーチンとなる ラスタスキャン・ループ loop_function() を定義します。画像処理をする func() は、純粋仮想関数として定義します。
画像膨張クラスと画像収縮クラスは、それぞれ画像基底クラスを継承して画像処理をする func() をオーバーライドします。画像基底クラスを継承していますので、共通メインルーチンとなる ラスタスキャン・ループ loop_function()は、双方のクラスで使用できます。
3つの関数のインターフェースを示します。
- cDilation::func() : 画像膨張
- cErasion::func() : 画像収縮
- cImageBase::loop_function() : 画像のラスタスキャン・ループ
C++言語によるアルゴリズムコードを示します。
class cImageBase { public: void loop_function( int iw ,int ih ,const unsigned char *pucSrc ,unsigned char *pucDst ); virtual unsigned char func( unsigned char uc ,unsigned char ua ,unsigned char ub ,unsigned char ul ,unsigned char ur) = 0; };
class cDilation : public cImageBase { public: unsigned char func( unsigned char uc ,unsigned char ua ,unsigned char ub ,unsigned char ul ,unsigned char ur ); };
class cErasion : public cImageBase { public: unsigned char func( unsigned char uc ,unsigned char ua ,unsigned char ub ,unsigned char ul ,unsigned char ur ); };
unsigned char cDilation::func( unsigned char uc ,unsigned char ua ,unsigned char ub ,unsigned char ul ,unsigned char ur ) { unsigned char ux = 0x00; if (uc != 0x00) { ux = 0xFF; } if (ua != 0x00) { ux = 0xFF; } if (ub != 0x00) { ux = 0xFF; } if (ul != 0x00) { ux = 0xFF; } if (ur != 0x00) { ux = 0xFF; } // return ux; }
unsigned char cErasion::func( unsigned char uc ,unsigned char ua ,unsigned char ub ,unsigned char ul ,unsigned char ur ) { unsigned char ux = 0xFF; if (uc != 0xFF) { ux = 0x00; } if (ua != 0xFF) { ux = 0x00; } if (ub != 0xFF) { ux = 0x00; } if (ul != 0xFF) { ux = 0x00; } if (ur != 0xFF) { ux = 0x00; } // return ux; }
void cImageBase::loop_function( int iw ,int ih ,const unsigned char *pucSrc ,unsigned char *pucDst ) { for(int iy=1; iy<ih-1; iy++) { for(int ix=1; ix<iw-1; ix++) { unsigned char uc = pucSrc[ (iy+0)*iw + (ix+0) ]; // 中央 unsigned char ua = pucSrc[ (iy-1)*iw + (ix+0) ]; // 上 unsigned char ub = pucSrc[ (iy+1)*iw + (ix+0) ]; // 下 unsigned char ul = pucSrc[ (iy+0)*iw + (ix-1) ]; // 左 unsigned char ur = pucSrc[ (iy+0)*iw + (ix+1) ]; // 右 // unsigned char ux = func( uc ,ua ,ub ,ul ,ur ); pucDst[ iy*iw + ix ] = ux; } } }
// 画像膨張 cDilation gDilation; gDilation.loop_function( iw ,ih ,pucSrc ,pucDst );
// 画像収縮 cErasion gErasion; gErasion.loop_function( iw ,ih ,pucSrc ,pucDst );
ポリモーフィズムによる実装の仕組み
画像基底クラスに共通メインルーチン loop_function() を定義して、画像膨張クラスと画像収縮クラスで画像基底クラスを継承することで膨張アルゴリズムと収縮アルゴリズムのメイン部分を共通のコードで実現することができました。
コンパイラが生成するx86アセンブラコードを確認します。
プログラムコードの中に、膨張クラス(cDilation)と収縮クラス(cErasion)の仮想関数テーブル(vtable)を作成します。仮想関数テーブルのfunc()ポインタは、それぞれ膨張関数と収縮関数です。クラスのインスタンスはデータ領域に生成します。インスタンスのthisポインタはクラスの仮想関数テーブル(vtable)ポインタを指します。
- 画像膨張処理は、インスタンス gDilation をthisポインタに設定し、共通メインルーチン loop_function() をコールします。
- 画像収縮処理は、インスタンス gErasion をthisポインタに設定し、共通メインルーチン loop_function() をコールします。
- loop_function()では、ラスタスキャン・ループしながら1画素ごとに func() をコールします。func()は、thisポインタが示す仮想関数テーブルの内容を間接コールしています。
このように同じメソッド func()をコールしても、膨張クラスのインスタンスは膨張関数、収縮クラスのインスタンスは収縮関数というように、インスタンス毎にその挙動を変えています。
ポリモーフィズムによる実装は、ラスタスキャン・ループの重複コードを除去できましたが、1画素ごとに仮想関数テーブルの内容を間接コールしているので、4K画像などの大画像を処理する場合はオーバーヘッドが気になります。
画像膨張・収縮のような簡単なコードの場合、無理して重複コードを除去する必要はないと考えます。もう少し複雑なコードだった場合、ポリモーフィズムによる実装を選択するかどうかは、ソースコードの保守とパフォーマンスのトレードオフかもしれません。
もう少し工夫することができないか、考えてみたいと思います。