関数のオーバーロード

同じ名前の関数を複数定義することを、関数のオーバーロードという。これらの関数を区別するために、それらのパラメータは異なる必要がある。

次のコードには、info()関数の複数のオーバーロードがあり、それぞれが異なる型のパラメータを受け取る。

import std.stdio;

void info(double number) {
    writeln("Floating point: ", number);
}

void info(int number) {
    writeln("Integer       : ", number);
}

void info(string str) {
    writeln("String        : ", str);
}

void main() {
    info(1.2);
    info(3);
    info("hello");
}
D
function_overloading.1

すべての関数の名前はinfo()だが、コンパイラは呼び出し時に使用される引数と一致する関数を選択する。例えば、リテラル1.2は型doubleであるため、doubleを受け取る関数info()が呼び出される。

呼び出す関数の選択はコンパイル時に行われるため、必ずしも簡単または明確であるとは限らない。例えば、intdoublerealの両方に暗黙的に変換できるため、コンパイラは次のプログラムで呼び出す関数を決定できない。

real sevenTimes(real value) {
    return 7 * value;
}

double sevenTimes(double value) {
    return 7 * value;
}

void main() {
    int value = 5;
    auto result = sevenTimes(value);    // ← コンパイルエラー
}
D
function_overloading.2

注釈:関数本体がまったく同じ場合は、別々の関数を記述する必要は通常ない。テンプレートに関する章で、1つの定義を複数の型に使用する方法について後で説明する。

ただし、longパラメータを取る別の関数オーバーロードがある場合、long doublerealよりもintによりよく一致するため、曖昧さは解消される。

long sevenTimes(long value) {
    return 7 * value;
}

// ...

    auto result = sevenTimes(value);    // 現在コンパイルできる
D
オーバーロードの解決

コンパイラは、引数に最もよく一致するオーバーロードを選択する。これをオーバーロード解決と呼ぶ。

オーバーロード解決は、ほとんどのケースでは単純で直感的だが、時には複雑になることもある。以下は、オーバーロード解決のルールだ。この本では、簡略化した形で説明している。

一致の状態は、最悪から最良の順に次の4つがある。

コンパイラは、オーバーロードの解決中に、関数のすべてのオーバーロードを考慮する。まず、すべてのオーバーロードのすべてのパラメータのマッチ状態を決定する。各オーバーロードについて、パラメータの中で最も低いマッチ状態が、そのオーバーロードのマッチ状態となる。

すべてのオーバーロードの一致状態が決定された後、最も一致するオーバーロードが選択される。最も一致するオーバーロードが複数存在する場合は、より複雑な解決ルールが適用される。本書ではこれらのルールの詳細には触れない。プログラムが複雑なオーバーロード解決ルールに依存している場合、プログラムの設計を変更する時期が来ている可能性がある。もう1つの選択肢は、テンプレートなど、Dの他の機能を利用することだ。さらに単純だが、必ずしも望ましいとは限らないアプローチは、型ごとに関数に異なる名前を付けることで、関数のオーバーロードを完全に放棄することである。例えば、sevenTimes_real()sevenTimes_double()のようにする。

ユーザー定義型に対する関数のオーバーロード

関数のオーバーロードは、構造体やクラスでも有用だ。さらに、ユーザー定義型では、オーバーロードの解決における曖昧さがはるかに少なくなる。上記のinfo()関数を、構造体の章で定義したいくつかの型に対してオーバーロードしよう。

struct TimeOfDay {
    int hour;
    int minute;
}

void info(TimeOfDay time) {
    writef("%02s:%02s", time.hour, time.minute);
}
D

このオーバーロードにより、TimeOfDayオブジェクトをinfo()とともに使用できるようになる。その結果、ユーザー定義型の変数は、基本型とまったく同じ方法で出力できるようになる。

auto breakfastTime = TimeOfDay(7, 0);
info(breakfastTime);
D

TimeOfDayオブジェクトは、info()のオーバーロードと一致する:

07:00

以下は、Meeting型に対するinfo()のオーバーロードだ。

struct Meeting {
    string    topic;
    size_t    attendanceCount;
    TimeOfDay start;
    TimeOfDay end;
}

void info(Meeting meeting) {
    info(meeting.start);
    write('-');
    info(meeting.end);

    writef(" \"%s\" meeting with %s attendees",
           meeting.topic,
           meeting.attendanceCount);
}
D

このオーバーロードは、TimeOfDayの既定義のオーバーロードを利用していることに注意。これで、Meetingオブジェクトも基本型とまったく同じ方法で出力できるようになった。

auto bikeRideMeeting = Meeting("Bike Ride", 3,
                               TimeOfDay(9, 0),
                               TimeOfDay(9, 10));
info(bikeRideMeeting);
D

出力:

09:00-09:10"自転車ツアー"ミーティング(参加者3名)
制限

上記のinfo()関数のオーバーロードは大変便利だが、この方法にはいくつかの制限がある。

演習

次の構造体についても、info()関数をオーバーロードしよう。

struct Meal {
    TimeOfDay time;
    string    address;
}

struct DailyPlan {
    Meeting amMeeting;
    Meal    lunch;
    Meeting pmMeeting;
}
D

Mealには開始時刻しかないため、終了時刻を決定するために 1 時間半を追加する。構造体の章で先に定義したaddDuration()関数を使用できる。

TimeOfDay addDuration(TimeOfDay start,
                      TimeOfDay duration) {
    TimeOfDay result;

    result.minute = start.minute + duration.minute;
    result.hour = start.hour + duration.hour;
    result.hour += result.minute / 60;

    result.minute %= 60;
    result.hour %= 24;

    return result;
}
D

Meal構造体オブジェクトの終了時刻がaddDuration()で計算された後、DailyPlanオブジェクトは次の出力のように表示されるべきだ:

10:30-11:45"自転車ツアー"ミーティング(参加者4名)
12:30-14:00昼食、場所: イスタンブール
15:30-17:30"予算"ミーティング(参加者8名)