多重定義(オーバーロード)とは
多重定義(たじゅうていぎ)、またはオーバーロード(英: overload)とは、
プログラミング言語において、同一の名前(シンボル)を持つ関数やメソッド、演算子を複数定義し、プログラムの文脈に応じて適切なものを選択して実行する仕組みです。
例えば、数値の絶対値を求める `abs` 関数を、
整数型、浮動小数点型、
複素数型など、異なる型に対してそれぞれ定義することができます。また、各型ごとに意味のある名前やIDを返す関数を定義することも可能です。
多重定義は、対象に応じて「関数の多重定義」「演算子の多重定義」「メソッドの多重定義」などと呼ばれます。メソッドの多重定義の特殊なケースとして、コンストラクタの多重定義があります。また、
Common Lispなどの言語では、多重定義可能な関数としてジェネリック関数(generic function)があります。
オーバーライドとの違い
多重定義と混同しやすい概念にオーバーライドがありますが、これらは全く異なるものです。オーバーライドは、継承関係にあるクラス間で、スーパークラスのメソッドをサブクラスで再定義することを指します。オーバーライドは動的な
ポリモーフィズム(多態性)を実現するために利用されます。
多重定義の概要
多重定義された関数や演算子を選択する際には、
引数の型情報が重要な役割を果たします。型付きの
プログラミング言語では、関数や演算子に与えられた実
引数(オペランド)の型に基づいて、どの定義を使用するかが決定されます。まれに、戻り値の型を利用できる言語も存在します。
関数の名称と
引数の型情報の組み合わせをシグネチャと呼びます。プログラム内でシグネチャがユニークであれば、関数名やメソッド名、演算子記号が重複していても、呼び出すべき対象を一意に決定できます。このような型付けによる多重定義は、暗黙の
型変換、継承、総称型などとともに、
プログラミング言語における多態性を実現するための重要な手段の一つです。
多重定義は、関数名や演算子記号が単なる記号であるという事実を反映し、定義の自由度が高いという特徴があります。ただし、特に演算子の場合、構文解析の都合上、優先順位などが制限されることがあります。また、関数名やメソッド、演算子の用法には、各分野や
プログラミング言語ごとに慣習があります。そのため、著名な関数や演算子に対して、慣習とかけ離れた定義を与えると、プログラムの可読性を著しく低下させる可能性があるため、注意が必要です。
デフォルト
引数(オプション
引数)をサポートしない言語では、多重定義によってデフォルト
引数と類似の機能を実現できます。
言語による多重定義のサポート
多重定義をサポートしない言語では、たとえ関数の
引数の型や数が異なっても、同じ処理を行う場合でも、
引数の型や数ごとに異なる名前の関数を定義する必要があります。
C言語の例
c
double vector_length_double_2d(double x, double y);
float vector_length_float_2d(float x, float y);
double vector_length_double_3d(double x, double y, double z);
float vector_length_float_3d(float x, float y, float z);
この例では、ベクトルの長さを計算する関数を、型と次元ごとに異なる名前で定義しています。
一方、多重定義をサポートする言語では、関数のシグネチャが異なれば同じ名前を使うことができます。関数には本質的な名前だけを付ければよく、呼び出すときも
引数によらず一様に記述できます。
C++の例
cpp
double vector_length(double x, double y);
float vector_length(float x, float y);
double vector_length(double x, double y, double z);
float vector_length(float x, float y, float z);
C++11規格では、2次元ベクトルの長さを求める標準関数として、多重定義された `std::hypot()` 関数が用意されています。
C++17では3次元ベクトルバージョンも追加されています。
ルックアップ
多重定義のルックアップ(探索)は、実
引数の型に応じて静的に解決されます。つまり、コンパイル時にどの関数を呼び出すかが決定されます。以下の
Javaの例では、 `testMethod()` は
引数の動的な型情報ではなく、コンパイル時に決定された静的な型情報に基づいて選択されます。
java
public class Test {
void testMethod(Object obj) {
System.out.println("Object version");
}
void testMethod(String str) {
System.out.println("String version");
}
public static void main(String[] args) {
Test test = new Test();
String str = "test";
test.testMethod(str); // String version
test.testMethod((Object)str); // Object version
}
}
多重定義された候補の中から、適切なものが選択できない場合はコンパイルエラーとなります。
曖昧なケースの例(C++)
cpp
void func(int a, double b);
void func(double a, int b);
func(1, 2); // コンパイルエラー
このような曖昧さを解決するためには、明示的な
型変換が必要となります。
cpp
func(static_cast
(1), 2);
一方、多重定義がなく、曖昧さがない場合は暗黙の型変換を利用することができます。
多重定義の欠点
多重定義された関数やメソッド、演算子は、その名前だけでは区別できないため、どのバージョンが使われているかをソースコード上で判断しにくいという欠点があります。そのため、可読性が低下する可能性があります。
統合開発環境 (IDE) の中には、構文解析によりどのバージョンがどこで使われているかを列挙してくれるものもありますが、そういったツールが使えない状況では、読み手に詳細なルックアップの知識がないと判別が困難なこともあります。
多重定義の例
C++による多重定義
C++では、以下の条件が異なれば、関数に同じ名前を付けることができます。
1. 引数の数
2. 修飾子(`const`など)
3. 型
さらに、メンバー関数では、以下の条件も加わります。
4. 修飾子の違い
5. 変換演算子を用いた時の戻り値の型の違い
C++では、省略子(`...`)とテンプレート関数も多重定義できます。
省略子を引数に取る関数は、あらゆる引数を受け付けることができますが、関数の内部で引数を参照することはできません。
テンプレート関数は、明示的に型が指定された関数よりも優先度が低く、省略子を用いた関数はさらに優先度が低くなります。この特性を利用して、同じ扱いができる型はテンプレート関数で処理し、特別な扱いが必要な型は明示的に型が指定された関数で処理し、引数の数が異なり、多重定義した関数群では対処できない引数は省略子を用いた関数で処理するなどの制御ができます。
FORTRANによる多重定義
FORTRANでは、関数の定義としては多重定義を認めませんが、呼び出し方法として多重定義を認めています。呼び出し時の名前と定義の名前が異なるため、混乱の原因になることがあります。
演算子の多重定義
演算子の多重定義は、ユーザー定義の演算子を作成する機能の一つです。オブジェクト指向言語においては、数値型とオブジェクトを同じ関数で処理するために必須の機能です。
テンプレートと多重定義
C++のように多重定義とテンプレートを使用可能な言語では、両方の機能を組み合わせることで、静的な多態を実現できます。
cpp
template
T Sign(T x){
return (x > 0) - (x < 0);
}
この例では、 `Sign` 関数内部の演算子と `abs` 関数が、引数の型によって変化します。上記のテンプレート関数は、複素数型 `std::complex` にも適用でき、その場合、結果は正規化された複素数(正規化ベクトル)となり、符号関数の複素数への拡張と一致します。
テンプレートと多重定義を備える言語では、多重定義でオーバーライドを代用することができます。
多重定義による多態は、コンパイル時にしか実現できないという問題がありますが、単純なオーバーライドでは実現しづらい柔軟性を備えています。
多重定義によって、大域スコープの関数をクラスのインターフェースの一部として見なすことができ、クラスだけでなく、`int`や`double`型などのメンバー関数を持たない型にもオブジェクト指向の恩恵を与えることができます。
また、大域スコープの関数はメンバー関数よりも柔軟な拡張性を持ちます。例えば、外部のライブラリにあるクラスにメンバー関数を追加することは困難ですが、大域スコープの関数を追加することは容易です。さらに、関数をテンプレートで実装すれば、複数のクラスを横断的に拡張できます。
単一ディスパッチでは不可能な多重ディスパッチを模倣できる点も重要です。これにより、例えば、矩形を描画する際に、描画先デバイスが矩形の描画に対応していれば、デバイスに直接矩形情報を送り、対応していなければ、パスや線分などを使って矩形を描画するといった処理が自然に記述できます。
大域スコープと記述していますが、名前空間の中にあっても、実引数依存の名前探索によって、多態性を実現できます。
多重定義濫用の弊害
多重定義は強力な機能ですが、濫用すると混乱を招く可能性があります。例えば、C++で有理数型を定義する際に、整数型を引数にとる `abs` 関数とは全く異なる意味で、有理数型を引数にとる `abs` 関数を定義すると、関数テンプレートなどで同じ処理を共有できなくなり、混乱を招きます。互換性のない多重定義は避けるべきです。
曖昧な型を持つ言語
PerlやPHPのような曖昧な型を持つ言語では、関数の多重定義ができない、あるいは制限されていることがあります。その場合は、関数の先頭で引数の型を判定する条件分岐で対応します。
PHPには「オーバーロード」という機能がありますが、これはプロパティやメソッドを動的に作成するための機能であり、他の多くのオブジェクト指向言語とは異なる意味で用いられています。
まとめ
多重定義は、プログラミングにおいて非常に便利な機能ですが、適切に使用しないと可読性を損なう可能性があります。そのため、多重定義を使用する際には、その意味をよく理解し、慣習に従って使用することが重要です。