Modern C++編【言語解説】 第10章 関数オーバーロードとデフォルト引数

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

関数オーバーロード

C++ では、引数の個数や型に違いがあれば、同じ名前の関数を複数定義できます。 この機能を、関数オーバーロード(あるいは単にオーバーロード多重定義とも)といいます。
また、関数の名前、引数の型の並びの組み合わせをシグネチャと呼びます。

他にも、演算子をオーバーロードする機能もあります(第20章参照

C言語では、同じことをする関数であっても、型ごとに異なる名前の関数を用意しなければなりませんでした。 例えば、平方根を求める標準関数sqrt は、 以下のように、使う型ごとに名前の違うバージョンが定義されています。

float sqrtf(float x);
double sqrt(double x);
long double sqrtl(long double x);

C99以降では、tgmath.h をインクルードすることで、 3つのバージョンを「sqrt」という統一された名前で使えるようになります。

これらの関数を使う側からすると、型に応じて使い分ける必要があるので不便です。 関数オーバーロードの機能を使うと、これらの関数は1つの共通の名前に統一できます。

float sqrt(float x);
double sqrt(double x);
long double sqrt(long double x);

実際、C++ の標準ライブラリでは、オーバーロードによって、このように統一された名前になっています。 (互換性のため、C言語時代の名前でも使えます)。

なお、プログラムの分かりやすさのために、 オーバーロードを行う際には、それぞれの関数が同じ目的の処理を行っているようにして下さい。


実際にオーバーロードされた関数を呼び出す際には、 実引数に指定した値の型に応じて、どのタイプの関数を呼ぶべきなのか判断されます。

#include <cmath>
#include <iostream>

int main()
{
    float f1 = std::sqrt(2.0f);  // float sqrt(float) が使われる
    double f2 = std::sqrt(2.0);   // double sqrt(double) が使われる
    long double f3 = std::sqrt(2.0L);  // long double sqrt(long double) が使われる
    
    std::cout << f1 << "\n"
              << f2 << "\n"
              << f3 << std::endl;
}

実行結果:

1.41421
1.41421
1.41421

この例だと実行結果からでは挙動が分からないので、自分でも関数オーバーロードを試してみましょう。

#include <cstdio>

namespace {

    void write(int n)
    {
        std::printf("%d\n", n);
    }

    void write(double n)
    {
        std::printf("%f\n", n);
    }

    void write(const char* s)
    {
        std::printf("%s\n", s);
    }
    
}

int main()
{
    write(100);
    write(3.5);
    write("xyz");
}

実行結果:

100
3.500000
xyz

あえて、printf関数を使って出力しています。 printf関数は、型に応じてフォーマット指定を変えないと正しく出力できませんが、実行結果を見る限りうまく動作しているようです。 つまり、write関数に渡した引数の型に応じて、呼び出される関数が変化していることが分かります。

なお、関数オーバーロードは、引数に違いがあることが必要で、戻り値が違うだけでは不十分です。 なぜなら、戻り値は受け取らないことがあるので、戻り値の違いだけでは、どの関数を呼び出したいのか見分けられないからです。

#include <iostream>

namespace {

    int getValue()
    {
        return 100;
    }
    double getValue()
    {
        return 3.5;
    }
    
}

int main()
{
    int a = getValue();
    double b = getValue();

    std::cout << a << std::endl;
    std::cout << b << std::endl;
}

getValue関数は、戻り値の違いだけでオーバーロードしようとしているので、 このプログラムはコンパイルできません。
このプログラムのように、getValue関数を呼び出す際に戻り値を受け取っていれば、 受け取る変数の型によって区別できそうに見えますが、認められません。

曖昧な呼び出し

関数オーバーロードによって、同じ名前の関数が複数定義できるので、 呼び出しの際に曖昧さが生まれることがあり、 その場合にはコンパイルエラーになってしまいます。
次のプログラムで確認してみます。

#include <iostream>

namespace {

    void func(float f)
    {
        std::cout << "float: " << f << std::endl;
    }
    void func(double f)
    {
        std::cout << "double: " << f << std::endl;
    }

}

int main()
{
    func(1.0f);  // float版
    func(1.0);   // double版
    func(1);     // エラー
}

func(1); という呼び出しが問題です。 実引数は 1 なので、これは int型ですが、引数が int型の func() は存在しません。 float型か double型を引数に取る func() がありますが、 int型から float型、int型から double型へは、いずれも暗黙的に変換できるため、 どちらを優先すべきか判断できず、コンパイルエラーになります。
この場合、キャストによって型を明確にすればコンパイル可能です。

func(static_cast<float>(1));      // float版
func(static_cast<double>(1));     // double版

関数を削除する

まず、次のプログラムを見て下さい。

#include <iostream>

namespace {

    void func(int n)
    {
        std::cout << n << std::endl;
    }

}

int main()
{
    func(1);
    func(1.5);
}

実行結果:

1
1

func() の仮引数は int型ですが、実引数に「1.5」を指定してもコンパイルエラーとはなりません。自動的に、縮小変換されますが、当然、小数点以下が失われてしまいます。

double型の値を使えるようにしたければ、double型版をオーバーロードすれば良いですが、そうではなくて、double型の値の利用を禁止したいのであれば、それを実現する手段があります。

#include <iostream>

namespace {

    void func(int n)
    {
        std::cout << n << std::endl;
    }
    void func(double) = delete;

}

int main()
{
    func(1);
    func(1.5);  // コンパイルエラー
}

関数オーバーロードのような形で、double型版の func() の宣言を書き、その末尾部分に「= delete」と記述しています。宣言時に「= delete」が記述された関数は削除され、その関数を使おうとするとコンパイルエラーになります。

func() の実引数に double型の値を指定すると、コンパイラは、仮引数が double型の func() の宣言を発見しますが、それは削除されているため使用できず、エラーとして報告されるという流れです。

もし、実引数の値が「1.5f」のような float型の値ならどうでしょう。 この場合、2つの func(int版と double版)のどちらにより適合するかを判断し、情報を失わなくて済む double型の方が選択されます。しかし、この関数は削除されていて使用できないため、やはりエラーとなります。
「削除されていて使用できないので、他の関数を探す」という動作にはなりません。この挙動が、単に double型版を用意しない場合との違いです。もし、double型版がそもそも用意されておらず、int型版だけが存在するのであれば、実引数を float型の値にしたときには、int型版が(縮小変換を伴って)呼び出されます。

constメンバ関数と非constメンバ関数のオーバーロード

constメンバ関数と、非constメンバ関数とは、オーバーロードできます
もし、constオブジェクトから呼び出そうとすれば、constメンバ関数の方が選択されますし、 非constオブジェクトから呼び出そうとすれば、非constメンバ関数の方が選択されます。

constオブジェクト、非constオブジェクトを問わずに呼び出せるようなメンバ関数が必要であれば、 constメンバ関数と、非constメンバ関数とでオーバーロードしなければなりません。 このような場合、constメンバ関数と、非constメンバ関数の実装が同じになってしまうことがあります。 まったく同じコードを2か所に書くのは保守面から望ましくないので、次のように書くと良いです。

class MyClass {
public:
    inline int* Get()
    {
        return const_cast<int*>(static_cast<const MyClass*>(this)->Get());
    }

    const int* Get() const;
};

const int* MyClass::Get() const
{
    // 実装を書く
}

実装は、constメンバ関数の方だけに書き、非constメンバ関数は constメンバ関数の方を呼び出す形にします。 constメンバ関数の方に書くのは、制約が強い側で書いた方が、コンパイラのチェックが入り安全だからです。 逆にしてしまうと、const による強制力が台無しになります。

非constメンバ関数から、constメンバ関数を呼び出すには工夫が必要です。 同じ名前、同じ引数の関数ですから、単純に書くと、自分自身を呼び出して無限再帰してしまいます。
そこでまず、thisポインタに明示的に const を付けます。 そのためには、「static_cast<const MyClass*>(this)」というように、static_cast を使います
const付きの thisポインタが手に入れば、これを経由して関数を呼び出せば、 constポインタ経由なら constメンバ関数の方が選択されるルールによって、 意図通り、constメンバ関数版の Get関数を呼び出すことができます。

最後に、constメンバ関数が返した戻り値を、const_cast で const を取り除いて返します。 const_cast は使わないことが望ましいですが、この場面では、constメンバ関数内で何をしているか、 プログラマ自身で分かっている訳ですし、呼出し元はそもそも非const版なのだから、 書き換えられるようにすることにも問題はありません。

デフォルト引数

オーバーロードに関連して、デフォルト引数という機能があります。 関数の実引数を指定しないことを許し、関数の宣言側で規定したデフォルトの値を指定したことにします。

まずはサンプルプログラムを見て下さい。

#include <cassert>
#include <iostream>

namespace {

    int divide(int n, int d, int* s = nullptr)
    {
        assert(d != 0);
        
        if (s != nullptr) {
            *s = n % d;
        }
        return n / d;
    }
    
}

int main()
{
    int s;
    int a = divide(10, 3, &s);
    std::cout << a << "…" << s << std::endl;

    a = divide(10, 4);
    std::cout << a << std::endl;
}

実行結果:

3…1
2

divide関数は、引数n を 引数d で除算し、その商を戻り値として返し、剰余を引数s に指定したアドレスに格納します。

デフォルト引数を使うための指示は、関数宣言のところに書きます。

int divide(int n, int d, int* s = nullptr);

この場合、引数s にデフォルト値として nullptr が与えられています。 この記述は、関数定義の側には書くことはできません
また、手前側の引数にデフォルト値を与えないのなら、それより後ろの引数にも与えられません

divide関数を呼び出す際に、次のように書いた場合、引数s の部分には、デフォルト値である nullptr が補われます。

a = divide(10, 4);  // 3つ目の実引数を指定していないので、デフォルト値が使われる


デフォルト引数は、関数オーバーロードによって代替できます。 今回のサンプルプログラムであれば、次のように2つの divide関数をオーバーロードすることでも同じ結果を得られます。

#include <cassert>
#include <iostream>

namespace {

    int divide(int n, int d, int* s)
    {
        assert(d != 0);
        
        if (s != nullptr) {
            *s = n % d;
        }
        return n / d;
    }

    int divide(int n, int d)
    {
        return divide(n, d, nullptr);
    }
    
}

int main()
{
    int s;
    int a = divide(10, 3, &s);
    std::cout << a << "…" << s << std::endl;

    a = divide(10, 4);
    std::cout << a << std::endl;
}

実行結果:

3…1
2

結果としては同じなのですが、本質的な違いは理解しておくべきです。

関数オーバーロードの場合、引数s が無いタイプを呼び出したときに、 nullptr が補われていることは、関数を呼び出す側からは分かりません。 もちろん、devide関数の実装を見れば分かりますが、他人が作ったライブラリ等であれば、 ソースコードが見られるとは限りません。
一方、デフォルト引数は、関数宣言のところにデフォルト値が書かれているので、 関数を呼び出す側からでも、何が補われるか分かります。 他人が作ったライブラリであっても、関数を呼び出すにはヘッダファイルが必要になり、そこに関数宣言が書かれています。

そのため、関数オーバーロードは、関数作成者の意志で補うべき値を変えることができます。 関数を使う側にすれば、そもそも、何かの値が補われているということすら分からないので、 関数作成者にある程度の自由があります。
デフォルト引数の場合でも、関数作成者の意志でデフォルト値を変更することは可能ですが、 関数を使う側は、現時点でのデフォルト値をアテにしてプログラムを書いてしまうので、安易に変更すると、 使用者側のプログラムを破壊しかねません

また、オーバーロードされた関数1つ1つが別のメモリアドレスに配置されるのに対し、 デフォルト引数を使った場合は、関数は1つだけしか存在しないことになります。 この違いは、関数ポインタ(C言語編第37章)を使う場合に影響します。

賛否はあるものの、個人的にはオーバーロードで表現できるのならば、 オーバーロードを優先して使う方が良いかと思います。


練習問題

問題① 2つの int型の値が同じかどうかは ==演算子で調べられますが、 const char*型で表現された2つの文字列の文字の並びが同じかどうかを調べるには ==演算子が使えません。 どちらでも使えるような equal関数を、関数オーバーロードを利用して作成して下さい。

問題② 次のプログラムの問題点を指摘して下さい(複数あります)

#include <iostream>

namespace {

    void func();
    void func(int a = 0);
    void func(int a = 0, int b);
    
    void func()
    {
        std::cout << "func()" << std::endl;
    }

    void func(int a = 0)
    {
        std::cout << "func(int)" << std::endl;
    }

    void func(int a = 0, int b)
    {
        std::cout << "func(int, int)" << std::endl;
    }
}

int main()
{
    func();
    func(10);
    func(10, 20);
}


解答ページはこちら

参考リンク

更新履歴

'2017/8/5 「関数を削除する」の項を追加。

'2017/7/28 新規作成。



前の章へ

次の章へ

Modern C++編のトップページへ

Programming Place Plus のトップページへ