多重定義
多重定義 (たじゅうていぎ) あるいは オーバーロード (英: overload) とは、プログラミング言語において同一の名前(シンボル)を持つ関数あるいはメソッドおよび同一の演算子記号について複数定義し、利用時にプログラムの文脈に応じて選択することで複数の動作を行わせる仕組みである。 例えば整数型や浮動小数点型、複素数型の値について同じ「abs」という関数を定義して絶対値を求める、型ごとに個々の意味で名前やIDを返す関数を定義するなどが挙げられる。多重定義する対象に応じてそれぞれ関数の多重定義[注釈 1]、演算子の多重定義[注釈 2]、メソッドの多重定義[注釈 3]と呼ばれる。メソッドの多重定義の特殊なケースとして、コンストラクタの多重定義がある。また、Common Lispなどでは、多重定義可能な関数としてgeneric function(en:Generic function)がある(このgenericはジェネリックプログラミングのジェネリックである)。
動作の「上書き」を意味するオーバーライド[注釈 4]とはまったく異なる概念である。オーバーライドは動的なポリモーフィズム(多態)に利用される。
概要
[編集]動作を選択する際に用いられる代表的な文脈情報としては、型付けられたプログラミング言語においては関数や演算子に実引数(演算子ならばオペランド)として与えられた式や変数に関連付けられた型の情報が用いられる(稀ではあるが戻り値の型を利用できるプログラミング言語も存在する)。関数の名称とそれらの型情報の組を合わせたものをシグネチャと呼ぶが、プログラム内でシグネチャが唯一に決まれば、関数名やメソッド名、演算子の記号が重複していても呼び出すべき対象を唯一に決定することができる。このような型付けによる多重定義は、暗黙の型変換[注釈 5]あるいは型強制[注釈 6])、継承[注釈 7]あるいは包含[注釈 8]、総称型[注釈 9]、あるいはパラメーター付型[注釈 10]と並んでプログラミング言語において多態性[注釈 11]を実現するための一つの手段である。
理論的には関数の名前や演算子記号は単なる記号であり、意味的必然があるわけではないので、これを反映して多重定義を許すプログラミング言語では多重定義された関数や演算子、メソッドの意味や動作の定義はかなり自由に行うことができる(演算子については構文解析の都合上、優先順位などが制限される場合も有る)。とはいえ関数名やメソッド、特に演算子の用法には各分野及びプログラミング言語毎に慣習が育っている場合があり、著名な関数(例えば数学関数のsin
など)やメソッド、演算子に対して慣習とあまりにかけ離れた意味、即ち動作の定義を与えるとプログラムの可読性の著しい低下をもたらす可能性があるので注意が必要である。
デフォルト引数(オプション引数)をサポートしない言語(Javaや、バージョン4.0よりも前のC#など)では、多重定義によってデフォルト引数と類似の機能を実現することができる。
言語による多重定義のサポート
[編集]関数の多重定義をサポートしない言語では、たとえ関数の引数の型や数によらずアルゴリズムすなわち本質的な内容がまったく同じでも、引数の型や数ごとに関数をそれぞれ定義する場合は同じ名前が使えず、引数に応じた名前をそれぞれ付ける必要があり、呼び出すときも引数に応じて使い分ける必要がある。
C言語での例を以下に示す。
#include <stdio.h> #include <math.h> float vector2f_length(float x, float y) { return sqrtf(x * x + y * y); } double vector2d_length(double x, double y) { return sqrt(x * x + y * y); } float vector3f_length(float x, float y, float z) { return sqrtf(x * x + y * y + z * z); } double vector3d_length(double x, double y, float z) { return sqrt(x * x + y * y + z * z); } int main(void) { printf("%f\n", vector2f_length(1.0f, -1.0f)); printf("%f\n", vector2d_length(1.0, 2.0)); printf("%f\n", vector3f_length(1.0f, -1.0f, 1.0f)); printf("%f\n", vector3d_length(1.0, 2.0, -1.0)); }
ベクトルの長さを計算する関数を、型および次元ごとに命名している。
一方、関数の多重定義をサポートする言語では、関数のシグネチャが異なれば同じ名前を使うことができる。関数には本質的な名前だけを付ければよく、呼び出すときも引数によらず一様に記述できる。
C++での例を以下に示す。
#include <cstdio> #include <cmath> float vector_length(float x, float y) { return std::sqrt(x * x + y * y); } double vector_length(double x, double y) { return std::sqrt(x * x + y * y); } float vector_length(float x, float y, float z) { return std::sqrt(x * x + y * y + z * z); } double vector_length(double x, double y, float z) { return std::sqrt(x * x + y * y + z * z); } int main(void) { printf("%f\n", vector_length(1.0f, -1.0f)); // (float, float) バージョンが呼ばれる。 printf("%f\n", vector_length(1.0, 2.0)); // (double, double) バージョンが呼ばれる。 printf("%f\n", vector_length(1.0f, -1.0f, 1.0f)); // (float, float, float) バージョンが呼ばれる。 printf("%f\n", vector_length(1.0, 2.0, -1.0)); // (double, double, double) バージョンが呼ばれる。 }
なお、C++11規格では、2次元ベクトルの長さを求める標準関数として、多重定義されたstd::hypot()
関数が用意されている[1]。C++17では3次元ベクトルバージョンも追加されている。
ルックアップ
[編集]多重定義のルックアップ(英: look-up、探索)は実引数の型に応じて静的に解決される。以下のJavaの例では、java.lang.String
クラスはjava.lang.Object
クラスから派生しているものの、testMethod()
は引数の動的な型情報(実行時型情報)によって選択されることはなく、あくまでコンパイル時に解決される静的な型情報に基づいて選択される。
public class Main { static void testMethod(String str) { System.out.println("String version is called."); } static void testMethod(Object obj) { System.out.println("Object version is called."); } public static void main(String[] args) { String str = "test"; Object obj = str; testMethod(str); // String バージョンが呼ばれる。 testMethod(obj); // Object バージョンが呼ばれる。 } }
なお、曖昧さを解決できず、多重定義された候補の中から1つを選択することができない場合はコンパイルエラーとなる。前述のC++の例において、曖昧さが解決できないケースを以下に示す。
printf("%f\n", vector_length(1, -1)); // コンパイルエラー。 printf("%f\n", vector_length(1.0f, -1.0)); // コンパイルエラー。 printf("%f\n", vector_length(1.0, -1.0f)); // コンパイルエラー。
曖昧さの解決のためには明示的な型変換が必要となる。
printf("%f\n", vector_length(double(1), double(-1))); // コンパイル可能。(double, double) バージョンが呼ばれる。
一方、前述のC言語の例のように、多重定義を持たず、曖昧さがない場合は暗黙の型変換を利用することができる。
printf("%f\n", vector2d_length(1, -1)); /* コンパイル可能。 */ printf("%f\n", vector2d_length(1.0f, -1.0)); /* コンパイル可能。 */ printf("%f\n", vector2d_length(1.0, -1.0f)); /* コンパイル可能。 */
欠点
[編集]関数/メソッドおよび演算子が多重定義された場合、その名前だけで区別することができないので、多重定義の候補のうち、どのバージョンが使われるのかがソースコード上で一見して分かりづらく、可読性が下がる。統合開発環境 (IDE) の中には、構文解析によりどのバージョンがどこで使われているかを列挙してくれるものも存在するが、そういったツールが使えない状況では読み手に詳細なルックアップの知識がないと判別が困難なこともある。
多重定義の例
[編集]// (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)戻り値の型の違い」による多重定義が可能になっている。JavaやC#などC++以外の言語では(1)と(3)の範囲にとどまっている事が多い。C++で特に特徴的なのは※1の省略子と※2のテンプレート関数を多重定義できる点である。省略子を引数にとる関数はあらゆる引数を受け付ける関数である。引数の型や数を無視する反面、関数の内部では一切引数を参照することができない。テンプレート関数はint等明示的に型を書いた関数より選択される優先度が低く、省略子を用いた関数は更に低い。この特性を利用して同じ扱いで処理できる型はテンプレート関数で処理、特別扱いが必要な型であれば明示的に型を書いた関数で処理、引数の数が異り多重定義した関数群では対処しようがない引数は省略子を用いた関数を使って何もしない等既定の処理をさせるようにすることができる。
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のストアドプロシージャーの様なテンプレートを備えていない言語でも同様の多態を実現できる場合がある。
以下に例を示す。
#include <cmath> #include <cstdlib> // 引数valueの符号に応じて、-1, 1または0を返す関数。 // ※absを使用しているため、引数に指定できる値の値の範囲はこの関数の引数と同じ型(反変な型)をとるabsの仕様に依存する。 template<class Type> Type Sign( Type const &value ) { return Type() == value ? Type() : abs( value ) / value; // 除算演算子及び、abs関数の実体はSignの引数によって変わる } int main(void) { double value = -2; std::valarray<double> array( 2 ); double value_sign = Sign( value ); // double型の-1, 1または0が返る std::valarray<double> array_sign = Sign( array ); // -1, 1または0を含むvalarrayの値が返る return EXIT_SUCCESS; }
この例ではSign関数内部の演算子とabs関数がSign関数の引数に指定した値によって変化する。なお上記関数テンプレートは複素数型 std::complex
に対して適用することもできる。その場合、結果は正規化された複素数(正規化ベクトル)となり、符号関数の複素数への拡張に一致する。
この様にテンプレートと多重定義を備える言語では、多重定義でオーバーライドを代用することができる。
多重定義による多態は、コンパイル時にしか実現できないという問題があるものの単純なオーバーライドでは実現しづらい各種柔軟性を備えている。
まず、メンバー関数だけでなく大域スコープの関数をクラスのインターフェースの一部として見做す事が出来るようになる。これにより、単に手続き型の要素でしか無かった大域スコープの関数をオブジェクト指向機能の一要素として組み入れる事が出来る。そして、大域スコープの関数がインターフェースとして機能し始める事によりクラスだけでなく、intやdouble型といったメンバー関数を持てない型にもオブジェクト指向の恩恵が得られる様になるのである。
次に、大域スコープの関数は直接クラスに所属しないという特性によりメンバー関数より柔軟な拡張性を得る事が出来る。例えば、外部のライブラリーのあるクラスにメンバー関数を追加することは、外部のライブラリーに手を加えなければいけないため事実上無理である。それに対し大域スコープの関数を追加する場合は、外部のライブラリーに手を加える必要がなく容易である。また、関数をテンプレートで実装すれば複数のクラスを横断的に拡張できる。先にテンプレート関数が存在する場合や、拡張対象のクラスの親クラスに対する関数が存在する場合、新たに、より具体的な型を引数に取る関数を追加することで静的なオーバーライドが可能となる。
次に、単一ディスパッチでは不可能な多重ディスパッチを模倣できるという点がある。これにより、例えば矩形を描画しようとする際、描画先デバイスが矩形の描画に対応していれば、デバイスに直接矩形情報を送り、描画先デバイスが矩形描画に対応していなければ、パスや線分等その他の機能を使って矩形を描画するといった処理が自然な形で記述可能となる。
なお大域スコープと記述しているが名前空間の中にあっても次の例のように多態性の実現は可能である[2]。この仕組みを実引数依存の名前探索という。
#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 RasterDevice { struct Device { /* 省略 */ }; // Raster形式用に線分を描画する void Draw( Device &device, Graphics::Line const &shape ); } namespace VectorDevice { struct Device { /* 省略 */ }; // Vector形式用に線分を描画する void Draw( Device &device, Graphics::Line const &shape ); } namespace DisplayDevice { struct Device { /* 省略 */ }; // 表示装置用に線分を描画する void Draw( Device &device, Graphics::Line const &shape ); } int main() { RasterDevice::Device device0; VectorDevice::Device device1; DisplayDevice::Device device2; Draw( device0, Graphics::Square( 0, 0, 1, 1 ) ); // 内部でRasterDevice::Draw( Device &, Graphics::Line const & )を呼び出す Draw( device1, Graphics::Square( 0, 0, 1, 1 ) ); // 内部でVectorDevice::Draw( Device &, Graphics::Line const & )を呼び出す Draw( device1, Graphics::Square( 0, 0, 1, 1 ) ); // 内部でDisplayDevice::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( ... )を呼び出す Draw( device2, Graphics::Ellipse( 0, 0, 1, 1 ) ); // Graphics::Draw( ... )を呼び出す return EXIT_SUCCESS; }
多重定義濫用の弊害
[編集]例えば、C++ において何らかの数値型例えば有理数型のためのクラスを定義するとして、整数型を引数にとるabs関数が絶対値を返すにもかかわらず有理数型をとるabs関数を全く違う意味で定義すると関数テンプレートなどで同じ処理を共有できないばかりでなく混乱を招く。互換性の無い多重定義は避けるべきである。
曖昧な型を持つ言語
[編集]PerlやPHPのような曖昧な型を持つ言語では、関数の多重定義ができない、あるいは制限されていることがある。そのときは関数の先頭で引数の型を判定する条件分岐で対応する。
また、PHPには「オーバーロード」という機能が存在するが、これはプロパティやメソッドを動的に作成するための機能であり、他の多くのオブジェクト指向言語とは異なる意味で用いられている。[4]
脚注
[編集]注釈
[編集]出典
[編集]- ^ std::hypot - cppreference.com
- ^ a b http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf
- ^ http://www.j3-fortran.org/doc/year/10/10-007.pdf
- ^ “オーバーロード”. 言語リファレンス. The PHP Group. 2014年4月16日閲覧。