多重定義 (たじゅうていぎ) あるいは オーバーロード (: overload)とは、プログラミング言語において関数演算子メソッドの同一名や同一の演算子記号について複数定義し、利用時にプログラムの文脈に応じて選択することで複数の動作を行わせる仕組みである。 例えば整数型浮動小数点型複素数型の値について同じ「abs」という関数を定義して絶対値を求める、ごとに個々の意味で名前やIDを返す関数を定義するなどが挙げられる。多重定義する対象に応じてそれぞれ関数の多重定義[1]、演算子の多重定義[2]、メソッドの多重定義[3]と呼ばれる。また、Common Lispなどでは、多重定義可能な関数としてgeneric function(en:Generic function)がある(このgenericはジェネリックプログラミングのジェネリックである)。

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

目次

概要編集

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

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

多重定義の例編集

C++による多重定義[12]

// (1-1): 引数の数の違いによる多重定義
int Function(void);
int Function( int value );
int Function( int value0, int value1 );

// (2): 引数の修飾子の違いによる多重定義
int Function( int *value );
int Function( int const *value );
int Function( int *const *value );
int Function( int *const *const *value );

// (3): 引数の型の違いによる多重定義
int Function( char value );
int Function( std::complex< double > const &value );
int Function( ... ); // ※1
template< class Type > int Function( Type const &value ); // ※2

struct Example
{
    // (1-2): 引数の型の違いによる多重定義(構築子版)
    Example(void);
    Example( int value );
    
    // (4): メンバー関数の修飾子の違いによる多重定義
    int Function(void);
    int Function(void) const;
    
    // (1-3): 引数の型の違いによる多重定義(メンバー関数版)
    int Function( int value );

    // (5): 戻り値の型の違いによる多重定義
    operator bool (void) const;
    operator int (void) const;
};

基本的には「(1)引数の数」と「(2)修飾子」「(3)型」が異なっていれば関数に同じ名前を付けられるようになっている。また、大域関数で可能な多重定義はメンバー関数で全て可能である。メンバー関数は更に「(4)修飾子の違い」による多重定義と変換演算子を用いた時に限り可能な「(5)戻り値の型の違い」による多重定義が可能になっている。JavaC#などC++以外の言語では(1)と(3)の範囲にとどまっている事が多い。C++で特に特徴的なのは※1の省略子と※2のテンプレート関数を多重定義できる点である。省略子を引数にとる関数はあらゆる引数を受け付ける関数である。引数の型や数を無視する反面、関数の内部では一切引数を参照することができない。テンプレート関数はint等明示的に型を書いた関数より選択される優先度が低く、省略子を用いた関数は更に低い。この特性を利用して同じ扱いで処理できる型はテンプレート関数で処理、特別扱いが必要な型であれば明示的に型を書いた関数で処理、引数の数が異り多重定義した関数群では対処しようがない引数は省略子を用いた関数を使って何もしない等既定の処理をさせるようにすることができる。


FORTRANによる多重定義[13]:

module Example

    implicit none
    
    ! Function0, Function1をFunctionとして定義。FORTRANに予約語はなくFunctionは予約語ではない。
    interface Function
        module procedure Function0, Function1
    end interface Function

contains

    function Function0( value1 ) result( value0 )
        !省略
    end function Function0
    
    function Function1( value1, value2 ) result( value0 )
        !省略
    end function Function1

end module Example

特徴的なのは関数の定義としては多重定義を認めないものの呼び出し方法として多重定義を認めている点である。呼び出し時の名前と定義の名前は別物であるため混乱の原因となるだけではあるが全く別の名前をつける事も可能になっている。

演算子の多重定義編集

多重定義を使った利用者定義演算子の一種である。詳細は当該記事を参照のこと。

オブジェクト指向言語においては数値型とオブジェクトを同じ関数で処理するために必須の機能である(後述のテンプレートと多重定義参照)。

テンプレートと多重定義編集

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

以下に例を示す。:

// 引数valueの符号に合わせ1又は、-1を返す関数
template<class Type> Type Sign( Type const &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 EXIT_SUCCESS;
}

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

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

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

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

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

なお大域スコープと記述しているが名前空間の中にあっても次の例のように多態性の実現は可能である[12]。この仕組みを実引数依存の名前探索という。

#include<cstdlib>

namespace Graphics
{
        class Line{ /* 省略 */ };
        class Ellipse{  /* 省略 */  };

        class Square
        {
                /* 省略 */
                Line At( size_t index ) const; // 四角形の辺を返す関数
                /* 省略 */
        };

        void Draw( ... )
        {
        }

        // 四角形を描画する
        template<class Type> void Draw( Type &device, Square const &shape )
        {
                Draw( device, shape.At( 0 ) );
                Draw( device, shape.At( 1 ) );
                Draw( device, shape.At( 2 ) );
                Draw( device, shape.At( 3 ) );
        }

}

namespace PNGDevice
{
        struct Device{ /* 省略 */ };
        // PNG用に線分を描画する
        void Draw( Device &device, Graphics::Line const &shape );
}

namespace JpegDevice
{
        struct Device{ /* 省略 */ };
        // Jpeg用に線分を描画する
        void Draw( Device &device, Graphics::Line const &shape );
}

int main()
{
        PNGDevice::Device device0;
        JpegDevice::Device device1;

        Draw( device0, Graphics::Square( 0, 0, 1, 1 ) ); // 内部でPNGDevice::Draw( Device &, Graphics::Line const & )を呼び出す
        Draw( device1, Graphics::Square( 0, 0, 1, 1 ) ); // 内部でJpegDevice::Draw( Device &, Graphics::Line const & )を呼び出す

        Draw( device0, Graphics::Ellipse( 0, 0, 1, 1 ) ); // Graphics::Draw( ... )を呼び出す
        Draw( device1, Graphics::Ellipse( 0, 0, 1, 1 ) ); // Graphics::Draw( ... )を呼び出す

        return EXIT_SUCCESS;
}

多重定義濫用の弊害編集

例えば、C++ において何らかの数値型例えば有理数型のためのクラスを定義するとして、整数型を引数にとるabs関数が絶対値を返すにもかかわらず有理数型をとるabs関数を全く違う意味で定義するとテンプレート関数などで同じ処理を共有できないばかりでなく混乱を招く。互換性の無い多重定義は避けるべきである。


また無闇に多重定義を使うことは、ソフトウェア工学エクストリームプログラミングの観点から推奨されていない。[要出典]

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

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


脚注編集

  1. ^ : function overloading
  2. ^ : operator overloading
  3. ^ : method overloding
  4. ^ : method overriding
  5. ^ : implicit type conversion
  6. ^ : type coercion
  7. ^ : inheritance
  8. ^ : inclusion
  9. ^ : generic type
  10. ^ : parametric type
  11. ^ : polymorphism
  12. ^ a b http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf
  13. ^ http://www.j3-fortran.org/doc/year/10/10-007.pdf
  14. ^ YAGNI ("You ain't gonna need it") を参照
  15. ^ オーバーロード”. 言語リファレンス. The PHP Group. 2014年4月16日閲覧。

関連項目編集