多重定義

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動先: 案内検索

多重定義 (たじゅうていぎ) あるいは オーバーロード (テンプレート:Lang-en-short)とは、プログラミング言語において関数演算子メソッドの同一名や同一の演算子記号について複数定義し、利用時にプログラムの文脈に応じて選択することで複数の動作を行わせる仕組みである。 例えば整数型実数型複素数型の値について同じ「+」演算子を使って加算を行う、クラスごとに個々の意味で名前やIDを返すメソッドを定義するなどが挙げられる。多重定義する対象に応じてそれぞれ関数オーバーロード[1]、演算子オーバーロード[2]、メソッドのオーバーロード[3]と呼ばれる。

上書きを意味するオーバーライド[4]とはまったく異なる。

概要

動作を選択する際に用いられる代表的な文脈情報としては、型付けられたプログラミング言語においては関数や演算子に実引数(演算子ならばオペランド)として与えられた式や変数に関連付けられたの情報が用いられる(稀ではあるが戻り値を利用できるプログラミング言語も存在する)。関数の名称とそれらの型情報の組を合わせたものをシグネチャと呼ぶが、プログラム内でシグネチャが唯一に決まれば、関数名やメソッド名、演算子の記号が重複していても呼び出すべき対象を唯一に決定することが出来る。 このような型づけによる多重定義は、暗黙の型変換[5]あるいは型強制[6])、継承[7]あるいは包含[8]総称型[9]、あるいはパラメーター付型[10]と並んでプログラミング言語において多態性[11]を実現するための一つの手段である。

理論的には関数の名前や演算子記号は単なる記号であり、意味的必然があるわけではないので、これを反映して多重定義を許すプログラミング言語では多重定義された関数や演算子、メソッドの意味や動作の定義はかなり自由に行うことが出来る(演算子については構文解析の都合上、優先順位などが制限される場合も有る)。とはいえ関数名やメソッド、特に演算子の用法には各分野及びプログラミング言語毎に慣習が育っている場合があり、著名な関数(例えば数学関数のsinなど)やメソッド、演算子に対して慣習とあまりにかけ離れた意味、即ち動作の定義を与えるとプログラムの可読性の著しい低下をもたらす可能性があるので注意が必要である。

演算子の多重定義

例えば、C++ において何らかの数値型例えば有理数型のためのクラスを定義するとして、その際 + 演算子を加算以外の意味で定義すれば間違いなく混乱するであろう。 一方意味が大きく変わる場合でも比較的安全な事例もある。例えば、C++ において整数型に対する << は左ビットシフト演算を意味するが、C++ の標準入出力ライブラリで提供されるストリーム型(入出力列を抽象化した型)については右オペランドの値を左オペランドのストリームに書き込むことを意味する。これは一見混乱するようにも見えるが、整数シフトという意味づけはCから引き継いだ以外には特に数学的背景などの慣習もなく、プログラミングにおいて整数型とストリーム型を混同することも考えにくいため比較的無難な多重定義と言える。

boost の提供するライブラリに至っては、より前衛的な多重定義が行われている。各種演算子がことごとく異なる意味で用いられたり、本来配列の添え字を表現するための演算子であった [] を、数式上の括弧の一種であるかのように扱っていたりするケースがある。こうして、本来のプログラム言語の構造から離れた独自の文法をもたせて高度なプログラミングを行うことができるのも多重定義の特徴であるが、このようなスタイルには否定的な意見もある。

演算子の多重定義は、C++の他にもC#を始めとする.Net Framework系の言語、Delphi等でも使用できる。

演算子の多重定義とはやや異なるがSmalltalkRubyPythonのような多重定義不能な言語でも演算子を定義することができる。これらの言語では、値型と演算子を定義したオブジェクトの区別せず、値もオブジェクトも同列に扱うことが出来る。このため演算子の定義は可読性という意味合いよりも、値型に対する処理をオブジェクトにも適用して処理を再利用するという意味合いが強い。


この様に最初からオブジェクト指向を意識して作られた言語では演算子を定義できる言語が一般的であり、演算子を定義できない言語は少数派である。

メソッドの多重定義

Javaでの例

Javaの多重定義は、引数の数や型による違いだけでなく、異なる型を持つ引数の並べ替えによってもメソッドをオーバーロードすることができる。 たとえば、method()というシグネチャの引数無しメソッドがあるとする。

int method() {}

これは以下のようにオーバーロードできる。

int method(int x) {}

このとき、さらに異なる型のメソッドもオーバーロードできる。

int method(Sting name) {}

引数を増やしたオーバーロードも可能。

int method(int x, String name) {}

さらに、引数の数が同じであっても、順番が異なるためこの状況で以下のようなオーバーロードも可能。

int method(String name, int x) {}

このように同じ型の引数を複数持つメソッドとしてさらにオーバーロードすることも可能。

int method(int x, int y) {}

このように、一度に同じ名前のメソッドをいくつも定義できる。

オーバーロード濫用の弊害

便利ではあるが、無闇にメソッドをオーバーロードすることは、ソフトウェア工学エクストリームプログラミングの観点から推奨されていない。

  • 「後で使うかも知れないから」と無闇にオーバーロードしたものの、結局使わない[12]
  • リファクタリングの際、オーバーロードしたメソッドを手動で修正し忘れる。
  • メソッド名がすべて同じであるため、一見してどのような振る舞いを実装しているのかわかりづらくなる。機能を表す各々のメソッド名にすべきである(とくに、引数の数が同じで型が異なるメソッドの多重定義は混乱の元である)。

また、以下 (Javaで例示) のようなオーバーロードされたメソッドは、メソッドの利用者にとって意図しない結果を引き起こす。

static void method(String id) { System.out.print("String "); }
static void method(Object id) { System.out.print("Object "); }

このようなメソッドのオーバーロードは、問題なくコンパイル・ビルドできる。 ここで、以下のオブジェクトを作成して

Object[] o = new Object[]{ new String("name"), new Object("obj") };

このオブジェクトを上記のメソッド引数に入れる。

for(int i = 0; i < o.length; i++){
  method(o[i]);
}

メソッドの利用者は、出力結果に"String Object"と返されることを期待するが、実際には"Object Object " が返される。

曖昧な型を持つ言語

PerlPHPのような曖昧な型を持つ言語では、メソッドのオーバーロードができない、あるいは制限されていることがある。そのときはメソッドの先頭で引数の型を判定する条件分岐で対応する。

また、PHPには「オーバーロード」という機能が存在するが、これはプロパティやメソッドを動的に作成するための機能であり、他の多くのオブジェクト指向言語とは異なる意味で用いられている。[13]

テンプレートと多重定義

C++の様に多重定義とテンプレートを使用可能な言語では、両方の機能を組み合わせることにより静的な多態を実現することが出来る。また、PostgreSQLのストアドプロシージャーの様なテンプレートを備えていない言語でも同様の多態を実現できる場合がある。

以下に例を示す。:

// 引数valueの符号に合わせ1又は、-1を返す関数
template<class Type> Type Sign( const Type &value )
{
    return value / abs( value ); // 除算演算子及び、abs関数の実体はSignの引数によって変わる
}

int main(void)
{
    double value = -2;
    std::valarray<double> array( 2 );

    double value_sign = Sign( value ); // 1または-1のdoubleの値が返る
    std::valarray<double> array_sign = Sign( array ); // 1または-1を含むvalarrayの値が返る

    return 0;
}

この例ではSign関数内部の演算子とabs関数がSign関数の引数に指定した値によって変化する。 この様にテンプレートと多重定義を備える言語では、多重定義でオーバーライドを代用することができる。

多重定義による多態は、コンパイル時にしか実現できないという問題があるものの単純なオーバーライドでは実現しづらい各種柔軟性を備えている。

まず、メンバー関数だけでなく大域スコープの関数をクラスのインターフェースの一部として見做す事が出来るようになる。これにより、単に手続き型の要素でしか無かった大域スコープの関数をオブジェクト指向機能の一要素として組み入れる事が出来る。そして、大域スコープの関数がインターフェースとして機能し始める事によりクラスだけでなく、intやdouble型といったメンバー関数を持てない型にもオブジェクト指向の恩恵が得られる様になるのである。

次に、大域スコープの関数は直接クラスに所属しないという特性によりメンバー関数より柔軟な拡張性を得る事が出来る。例えば、外部のライブラリーのあるクラスにメンバー関数を追加することは、外部のライブラリーに手を加えなければいけないため事実上無理である。それに対し大域スコープの関数を追加する場合は、外部のライブラリーに手を加える必要がなく容易である。また、関数をテンプレートで実装すれば複数のクラスを横断的に拡張できる。先にテンプレート関数が存在する場合や、拡張対象のクラスの親クラスに対する関数が存在する場合、新たに、より具体的な型を引数に取る関数を追加することで静的なオーバーライドが可能となる。

次に、単一ディスパッチでは不可能な多重ディスパッチを模倣できるという点がある。これにより、例えば矩形を描画しようとする際、描画先デバイスが矩形の描画に対応していれば、デバイスに直接矩形情報を送り、描画先デバイスが矩形描画に対応していなければ、パスや線分等その他の機能を使って矩形を描画するといった処理が自然な形で記述可能となる。

脚注

  1. テンプレート:Lang-en-short
  2. テンプレート:Lang-en-short
  3. テンプレート:Lang-en-short
  4. テンプレート:Lang-en-short
  5. テンプレート:Lang-en-short
  6. テンプレート:Lang-en-short
  7. テンプレート:Lang-en-short
  8. テンプレート:Lang-en-short
  9. テンプレート:Lang-en-short
  10. テンプレート:Lang-en-short
  11. テンプレート:Lang-en-short
  12. YAGNI ("You ain't gonna need it") を参照
  13. テンプレート:Cite web

関連項目