lctags を使って C/C++ ソースコードをリファクタリング(サブルーチン化編)
コーディングしていると、関数が肥大化していくことが良くあります。
関数の肥大化は、メンテナンス性や可読性が落ちる原因になるため、 リファクタリングで関数内の処理を分割するサブルーチン化作業が必要です。
サブルーチン化作業はほとんど機械的に出来ますが、 分割する関数の前後処理に気をつけないと、バグってしまうこともあります。
lctags は、そんなサブルーチン化作業をサポートします。
lctags 全般の紹介は次を参照してください。
- C/C++ ソースコードタグシステム lctags の紹介
事前準備
lctags のサブルーチン化処理は、ブロック文 {} を対象としています。 よってブロックでない処理をサブルーチン化する場合は、事前にブロック化しておく必要があります。 ブロック化は単に {} で括るだけです。
使用方法
emacs でソースファイルを開き、サブルーチン化したいブロックの先頭にカーソルを合せ、 次を実行します。
M-x lctags-split-it
あるいは C-c l でメニューを開いて、 r s でも可能です。
これにより、ブロック内の処理を解析してサブルーチンを生成し、 新しいバッファに生成したサブルーチンと、そのサブルーチンをコールするコードが 表示されます。
後は、この表示されたコードに置き換えればサブルーチン化作業が完了します。
デモ
次のリンクは、lctags を使って emacs から C 言語の関数をサブルーチン化する操作のデモ動画です。
チュートリアル
準備
lctags の DB を作成し、次の内容のソースコードを登録しておきます。
typedef struct {
int val;
} type_t;
int func6( int val )
{
type_t typ;
if ( val == 0 ) {
val = 1;
typ.val = val;
return 0;
}
return 1;
}
サブルーチン化
上記ソースコードを開き、 if ( val == 0 ) {
の "{" の箇所にカーソルを合せます。
ここで C-c l と入力し、lctags メニューを開きます。 lctags メニューでは、 refactoring の 'r' を押し、さらに subroutine の 's' を押します。
これにより、対象ブロックの解析とサブルーチンの生成が行なわれ、次の内容のバッファが開かれます。
/* please edit 'x' or 'o' and symbol and order of following items,
and push C-c C-c to update.
format:
:[xor]:argName:orgName
x is to pass value.
o is to pass address of value.
r is to pass value and to return directly.
----------------
:o:indirect-return
:o:pTyp:typ
:o:pVal:val
----------------
*/
//======= call ======
{
int funcRet__ = 0;
if ( func6__sub( &funcRet__, &typ, &val ) ) {
return funcRet__;
}
}
//======= sub routine ======
static int func6__sub( int* pFuncRet__, type_t* pTyp, int* pVal )
{
(*pVal) = 1;
pTyp->val = (*pVal);
return *pFuncRet__= 0, 1;
}
=== sub routin ===
以降は、サブルーチン化した処理です。
この処理をソースコードにコピーします。
=== call ===
以降は、サブルーチン化した関数を呼び出す処理です。
この処理を、 "if ( val == 0 ) {" の "{" の箇所に置き換えます。
置き換えると次のようになります。
static int func6__sub( int* pFuncRet__, type_t* pTyp, int* pVal )
{
(*pVal) = 1;
pTyp->val = (*pVal);
return *pFuncRet__= 0, 1;
}
int func6( int val )
{
type_t typ;
if ( val == 0 ) {
int funcRet__ = 0;
if ( func6__sub( &funcRet__, &typ, &val ) ) {
return funcRet__;
}
}
}
これでサブルーチン化は完成です。
生成するサブルーチンの説明
念のためサブルーチン化後の処理について説明しておくと、
func6__sub()
は戻り値が int 型の関数です。
戻り値が 0 以外の時は、サブルーチンの呼び出し側を return で終了させます。
その際、戻り値は funcRet__
に格納されています。
また、上記処理を良く見ると、サブルーチンに渡している val が気になるのではないかと思います。
サブルーチン化した処理では、 val を値渡しではなく、アドレス渡しにしています。
これは何故かというと、処理内で val に対して代入を行なっているためです。
例えば、 func6() の処理が、
次のように if
のブロック処理を抜けた後に val を return するような処理だった場合、
int func6( int val )
{
type_t typ;
if ( val == 0 ) {
val = 1;
typ.val = val;
}
return val;
}
if
のブロックをサブルーチン化する場合は、 val をアドレス渡しする必要があります。
ただ、今回の場合はブロックの後に val を参照していないので、 本来は val は値渡しでも問題ありません。
それにもかかわらず val をアドレス渡ししています。
これは、サブルーチン化の処理を安全方向に振るためです。
将来、この func6() 関数をさらに変更して、
if
ブロックの後に val を参照するかもしれません。
また、サブルーチン化した func6__sub()
処理を変更して、
戻り値が 0 になることもあるかもしれません。
そのような場合に備えて、アドレス渡しにしています。
値渡しの制御
もしも、このアドレス渡しが気になる場合は、次の方法で値渡しにすることが可能です。
生成されたバッファの上部に表示されている次の箇所を注目してください。
/* please edit 'x' or 'o' and symbol and order of following items,
and push C-c C-c to update.
format:
:[xor]:argName:orgName
x is to pass value.
o is to pass address of value.
r is to pass value and to return directly.
----------------
:o:indirect-return
:o:pTyp:typ
:o:pVal:val
----------------
*/
この :o:pVal:val
を :x:pVal:val
に編集し C-c C-c すると、
val が値渡しになったサブルーチンコードが生成されます。
{
int funcRet__ = 0;
if ( func6__sub( &funcRet__, &typ, val ) ) {
return funcRet__;
}
}
//======= sub routine ======
static int func6__sub( int* pFuncRet__, type_t* pTyp, int pVal )
{
pVal = 1;
pTyp->val = pVal;
pTyp->val = pVal;
return *pFuncRet__= 0, 1;
}
引数名の変更
サブルーチン化したブロックの引数名は、元の変数と同じ名前になります。 この変数名を違う名前に変更できます。
/* please edit 'x' or 'o' and symbol and order of following items,
and push C-c C-c to update.
format:
:[xor]:argName:orgName
x is to pass value.
o is to pass address of value.
r is to pass value and to return directly.
----------------
:o:indirect-return
:o:pTyp:typ
:o:pVal:val
----------------
*/
バッファ上部に出力されている :o:pVal:val
の pVal の部分を変更し C-c C-c すると、
引数が変更した名前になります。
アドレス渡しの変数を戻り値に
ブロック内で変更されている変数は、サブルーチン化の際にアドレス渡しの引数になります。 この変数を戻り値とすることで、引数は値渡しに出来ます。
アドレス渡しの変数を戻り値にするには、 :o:pVal:val
の o の部分を r とし、
C-c C-c することで更新されます。
return 文を持つブロック
reutrn 文を持つブロックをサブルーチン化すると、
その return 文は return *pFuncRet__= 0, 1;
のようになります。
この return 文が気になる場合は、元ブロックの return 文の形にすることができます。
元ブロックの return 文の形に変更するには、 :o:indirect-return
の o の部分を x とし、
C-c C-c することで更新されます。
この場合、サブルーチンの呼び出し側は、戻り値から return するかどうかを判別する必要があります。
カスタマイズ
static int func7( int val )
{
int index;
for ( index = 0; index < 10; index++ ) {
if ( val == 10 ) {
continue;
}
if ( val == 20 ) {
break;
}
if ( val == 30 ) {
return 0;
}
}
return 1;
}
上記ソースの for 文のブロックをサブルーチン化すると、 次のようになります。
/* please edit 'x' or 'o' of following items,
and push C-c C-c to update.
x: val
*/
//======= call ======
{
int funcRet__ = 0;
int result__ = func7__sub( &funcRet__, val );
if ( result__ == 1 ) { return funcRet__; }
else if ( result__ == 2 ) { break; }
else if ( result__ == 3 ) { continue; }
}
//======= sub routine ======
static int func7__sub( int* pFuncRet__, int val )
{
if ( val == 10 ) {
return 3;
}
if ( val == 20 ) {
return 2;
}
if ( val == 30 ) {
return *pFuncRet__= 0, 1;
}
return 0;
}
ここで、 func7__sub()
内の return 3 や return、
呼び出し側の result__ == 1
や result__ == 2
等の即値が気になると思います。
C では、即値は使わず define や enum 等を宣言して使用するのが定石とされています。
そこで、 lctags ではこの値をカスタマイズする方法を提供しています。
emacs では、次のように lctags-sub-ret-type を設定するだけです。
(setq lctags-sub-ret-type
"subMod_t/subModNone/subModReturn/subModBreak/subModContinue")
この設定をした際の上記処理のサブルーチン化結果は次の通りです。
/* please edit 'x' or 'o' of following items,
and push C-c C-c to update.
x: val
*/
//======= call ======
{
int funcRet__ = 0;
subMod_t result__ = func7__sub( &funcRet__, val );
if ( result__ == subModReturn ) { return funcRet__; }
else if ( result__ == subModBreak ) { break; }
else if ( result__ == subModContinue ) { continue; }
}
//======= sub routine ======
static subMod_t func7__sub( int* pFuncRet__, int val )
{
if ( val == 10 ) {
return subModContinue;
}
if ( val == 20 ) {
return subModBreak;
}
if ( val == 30 ) {
return *pFuncRet__= 0, subModReturn;
}
return subModNone;
}
lctags-sub-ret-type は、次の書式で定義します。
"type/val0/val1/val2/val3"
type は、サブルーチン化した関数の戻り値の型。
上記の例では func7_sub()
の int が該当します。
val0 〜 val3 は、戻り値の 0 〜 3 までの名前を指定します。
それぞれの値は、以下の通りです。
数値 | 意味 |
---|---|
0 | サブルーチン実行後、処理継続 |
1 | サブルーチン実行後、return で終了 |
2 | サブルーチン実行後、処理を break |
3 | サブルーチン実行後、処理を continue |
制限
サブルーチン化対象のブロックが次の条件に当て嵌る場合、サブルーチン化できません。
- マクロを利用し、そのマクロ内で return している。
- アドレス渡しする変数を、マクロ内で使用している。
- goto 文を使用している。
また、マクロ内で 2 項演算子を利用していると、 左にある変数はアドレスアクセスが必要なものだと判断します。 これは、 lctags の制限というよりは libclang の制限からくるものです。
何故ならば、libclang ではマクロ内で 2 項演算子が行なわれている場合に、 その演算子の種別を特定する手段がないためです。