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

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


関数オーバーロード 🔗

C++ では、仮引数の型や個数に違いがあれば、同じ名前の関数を複数定義できます。この機能を、関数オーバーロード(あるいは単にオーバーロード多重定義とも)といいます。

C言語では、同じ目的の関数であっても、仮引数に違いがあるのなら、別々の名前の異なる関数を定義しなければなりませんでした。たとえば、平方根を求める標準ライブラリ関数 sqrt には、型に応じて、次のように亜種があります。

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

使う側からしてみると、型に応じて使い分ける必要があるので不便です。そもそもの目的は同じなのですから、名前は1つである方がシンプルであるともいえます。

関数オーバーロードの機能を使うと、これらの関数に同一の名前を付けられます。実際、C++ の標準ライブラリでは、以下のように宣言されています。

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

互換性維持のため、C言語時代の名前も使えるようになっています。

関数オーバーロードは特別な構文があるわけではなく、いつもどおりの関数宣言・定義を書けばよいです。

オーバーロードされた関数を呼び出す際には、関数名による区別がつかないため、実引数による判断がなされます。実引数の型と個数が、仮引数の型と個数に一致しているものが選択されます

このように、呼び出すべき関数を判断する処理は、コンパイル時に行われます。また、これをオーバーロードの解決と呼びます。

なお、戻り値の型にしか違いがないような関数オーバーロードは行えません。これは、オーバーロードの解決は、引数の一致によって判断されるからです。

戻り値は受け取られないこともあるので、解決のための判断材料にできません。

実際に試してみましょう。

#include <cstdio>

void write(int n);
void write(double n);
void write(char c);
void write(const char* s);

int main()
{
    write(100);    // 実引数は int なので、write(int) が呼び出される
    write(3.5);    // 実引数は double なので、write(double) が呼び出される
    write('x');    // 実引数は char なので、write(char) が呼び出される
    write("xyz");  // 実引数は const char[] なので、write(const char*) が呼び出される
}

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

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

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

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

実行結果:

100
3.500000
x
xyz

「write」という名前の4つの関数を定義しています。それぞれの仮引数が異なっているので、関数オーバーロードが成立します。実引数に応じて呼び分けが行われていることも分かります。

ちなみに、C++ の文字リテラルは char型なので(第2章)、仮引数が char型の関数が呼び出されています。

関数の宣言がなく、定義だけであっても、オーバーロードに影響しません。

#include <cstdio>

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

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

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

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

int main()
{
    write(100);    // 実引数は int なので、write(int) が呼び出される
    write(3.5);    // 実引数は double なので、write(double) が呼び出される
    write('x');    // 実引数は char なので、write(char) が呼び出される
    write("xyz");  // 実引数は const char[] なので、write(const char*) が呼び出される
}

実行結果:

100
3.500000
x
xyz

なお、関数オーバーロードを行う場合には、それぞれの関数が同じ目的を持っているようにしてください。同じ名前を持った関数が、異なる目的を持っていると混乱の元になります。

関数オーバーロードとスコープ 🔗

関数オーバーロードは、同一のスコープ内で宣言されている関数同士の中でのみ成立します。

#include <iostream>

void print_value(int v)
{
    std::cout << "int: " << v << std::endl;
}

namespace n {
    void print_value(double v)
    {
        std::cout << "double: " << v << std::endl;
    }

    void func()
    {
        print_value(10);    // 実引数は int だが、n::print_value(double) が呼ばれる
        print_value(10.5);  // n::print_value(double) が呼ばれる
    }
}

int main()
{
    n::func();
}

実行結果:

double: 10
double: 10.5

グローバル名前空間にある print_value関数と、n という名前空間にある print_value関数は、名前が同じで、仮引数の型が異なっていますが、スコープが異なるため、関数オーバーロードとはなりません。

実際、両方の関数宣言が見えている位置から、実引数の型を変えて呼び出しを行ってみると、両方とも、同じ関数を呼んでいます。

この状況で関数オーバーロードを成立させるには、2つの関数の名前が同じスコープ内に入るようにしなければなりません。たとえば、using宣言を使うと、名前を現在のスコープ内に取り込むことができるのでした(第4章)。

#include <iostream>

void print_value(int v)
{
    std::cout << "int: " << v << std::endl;
}

namespace n {
    void print_value(double v)
    {
        std::cout << "double: " << v << std::endl;
    }

    void func()
    {
        using ::print_value;   // ::print_value を、func() のスコープに取り込む
        using n::print_value;  // n::print_value を、func() のスコープに取り込む

        print_value(10);    // 実引数は int なので、::print_value(int) が呼ばれる
        print_value(10.5);  // 実引数は double なので、n::print_value(double) が呼ばれる
    }
}

int main()
{
    n::func();
}

実行結果:

int: 10
double: 10.5

関数オーバーロードの解決手順 🔗

オーバーロードされている関数を呼び出すとき、どの関数を呼ぶのかを解決する手順は、厳密にルール化されています。ほぼ期待どおりの関数が呼び出されると考えておいても、まずは問題ないのですが、暗黙の型変換、スコープ、関数呼び出しの方法の違い(f()、p->()、x::f())や文脈の違いなど、さまざまな要素が絡むことで、非常に複雑になっています。

以下の解説も大幅に簡略化したものですが、それでも、覚えておく必要性はあまりないです。

大まかな解決手順としてはまず、最終的に呼び出すべき関数になるかもしれない関数たち(Viable functions: 呼び出し可能な関数)を列挙します。「呼び出し可能な関数」が出そろったところで、実引数と仮引数の一致度を調べて、1つの関数(Best viable function: 呼び出し可能な最適関数)に絞り込むという順序です。

「呼び出し可能な関数」を探すときには、より狭いスコープの関数によって隠されていて呼び出せないだとか、引数の型や個数が一致していない(暗黙の型変換を行ったとしても適合できない)といったことが判定され、不適切な関数が排除されます。「呼び出し可能な関数」が1つも見当たらなければコンパイルエラーになります。

ここでは1つの手順であるかのようにまとめましたが、本来は2つの段階を踏みます。1段階目では、名前の探索によって、候補になる関数(Candidate functions: 候補関数)を絞り込みます。この時点では、その関数が実際には呼び出し可能でなくても候補に挙がることがあります。このルールは、関数呼び出しの文脈の違いによって異なっており、非常に細かく文書化されています。

その後、2段階目の手順で、実際に呼び出すことが可能であるかどうかを判断し、「呼び出し可能な関数」として絞り込みます。

「呼び出し可能な関数」が出そろったら、それぞれの関数ごとに実引数と仮引数の一致度を調べて、もっとも一致度が高い関数、つまり「呼び出し可能な最適関数」を決定します。

引数の一致度は、実引数に型変換を加える必要がないものが最高であり、そうでなければ、どのような型変換が必要であるかによって順位付けされます。具体的には次のようになっています。

  1. 型変換が必要ない。あるいは、非常に一般的で些細な変換のみを行う(たとえば以下のような)
    • 配列名がポインタに変換される
    • 関数名が関数ポインタに変換される
    • const が付加される
  2. 型の拡張(汎整数拡張や、float から double への格上げ)によって適合する
  3. 標準的に起こる暗黙の型変換によって適合する
  4. ユーザー定義の暗黙の型変換によって適合する
  5. 仮引数が … であることによる一致

各関数の各引数を1つ1つ調べて、以上のどこに該当するかを判定します。すべての引数の一致度が、ほかの関数を調べた結果のどれよりも明確に勝っていた場合にだけ、「呼び出し可能な最適関数」が決定できます。明確に勝っているものがなければ、曖昧な呼び出しとみなされ、コンパイルエラーになります。

たとえば、引数が2個の関数2つが候補に残っているとき、第1引数では関数Aが勝り、第2引数では関数Bが勝っているというケースでは、どちらが明確に勝っているということができません。

曖昧な呼び出し 🔗

「呼び出し可能な最適関数」が1つに定まらない場合、コンパイルエラーになります。この際には、プログラマーの手によって何らかの解決を計る必要があります。

#include <iostream>

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

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

int main()
{
    print_value(1.0f);  // print_value(float) を呼び出す
    print_value(1.0);   // print_value(double) を呼び出す
    print_value(1);     // コンパイルエラー
}

「print_value(1);」という呼び出しは、実引数が 1 です。これは int型ですから、float型にも double型でも一致しませんが、暗黙の型変換は可能です。問題は、float に変換することも double に変換することも同レベルの行為であって、どちらを優先すべきであるか判断できません。そのため、曖昧な呼び出しとされて、コンパイルエラーになります。

1つの解決策は、によって型を明確にすることです。

#include <iostream>

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

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

int main()
{
    print_value(1.0f);  // print_value(float) を呼び出す
    print_value(1.0);   // print_value(double) を呼び出す
    print_value(static_cast<double>(1));     // print_value(double) を呼び出す
}

実行結果:

float: 1
double: 1
double: 1

ただ、できるだけキャストは避けるべきものであると考えるかもしれません。また、int型の実引数を指定することが、理にかなったものであるのなら、そもそも int型版の関数を追加するべきでしょう。

#include <iostream>

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

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

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

int main()
{
    print_value(1.0f);  // print_value(float) を呼び出す
    print_value(1.0);   // print_value(double) を呼び出す
    print_value(1);     // print_value(int) を呼び出す
}

実行結果:

float: 1
double: 1
int: 1

関数を削除する 🔗

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

#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版なのだから、書き換えられるようにすることにも問題はありません。


デフォルト実引数 🔗

関数オーバーロードと関連する機能に、デフォルト実引数というものがあります。この機能は、関数の実引数を指定しないことを許し、その場合にデフォルトの値を指定したことにします。

デフォルト実引数を使用するには、次のように関数宣言のところに式を書き込みます。

戻り値の型 関数名([仮引数の並び,] 仮引数の型 仮引数の名前 =);

デフォルト実引数を指定したい仮引数に対して、= 式 という形で付加します。当たり前ですが、式を評価して得られる値の型が、仮引数の型に合っていなければなりません。

構文的には、デフォルト “仮引数” であるような感じがするかもしれませんが、意味的にデフォルト “実引数” です(英語では、Default Parameters ではなく Default Arguments です)。関数呼び出しの際に、デフォルト実引数が指定されている引数に対して、実引数を指定しなかった場合に、デフォルト実引数が指定されたことになります。

【上級】ほとんどの場合、「式」の部分に定数を指定しますが、そのような規定はありません(ただし、型が適切であることをコンパイル時を確認するために、静的でなければなりません)。デフォルト実引数の評価は、その関数を呼び出すとき、つまり実行時に行われます。たとえば void f(int v = g()); のような式を与えることもできる訳ですが、g関数が毎回異なる値を返すような関数である場合、大きな混乱を招くと思われます。ほぼ間違いなく、デフォルト実引数は定数としておくのが無難です。

複数の仮引数に対して、デフォルト実引数を与えることもできますが、デフォルト実引数を持った仮引数よりも後ろの仮引数が、デフォルト実引数を持たないという状態は許可されません

void f1(int v1 = 0);                          // OK
void f2(int v1, int v2 = 100);                // OK
void f3(int v1, int v2 = 100, int v3 = 200);  // OK
void f4(int v1, int v2 = 100, int v3);        // エラー

以下は使用例です。

#include <iostream>

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

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;
}

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

実行結果:

3…1
2

divide関数は、引数n を 引数d で除算し、その商を戻り値として返し、剰余を引数s に指定したメモリアドレスに格納します。divide関数を呼び出す際に、次のように書いた場合、引数s の部分には、デフォルト実引数である NULL が補われます。

a = divide(10, 4);  // 第3引数は NULL になる

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

#include <iostream>

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

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;
}

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

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

実行結果:

3…1
2

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

関数オーバーロードの場合、引数s がないタイプを呼び出したときに、NULL が補われていることは、関数を呼び出す側からは分かりません。もちろん、devide関数の中身まで見れば分かりますが、他人が作ったライブラリなどであれば、ソースコードが見られるとは限りません。

一方、デフォルト実引数の場合は、関数宣言のところに式が書かれているので、関数を呼び出す側からでも、どんな値が補われるか分かります。他人が作ったライブラリであっても、関数を呼び出すにはヘッダファイルが必要になり、そこに関数宣言が書かれているはずです。

そのため、関数オーバーロードであれば、関数作成者の意志で補う値を変更できます。関数の使用者側からすると、そもそも実装の詳細は見えていないので、関数作成者に変更の自由があります。

ただし、オーバーロードされた関数の定義がヘッダファイルに書かれているのなら、使用者側のプログラムの再コンパイルを必要とします。

デフォルト実引数の場合でも、関数作成者の意志で値を変更することは可能ですが、関数宣言はつねに見えてしまっているので、安易な変更はできません。関数の使用者は、デフォルト実引数を確認したうえで、それを使ってよいか、明示的に指定しなければならないか判断して、呼び出しを行っているはずだからです。

また、オーバーロードの場合、関数そのものが複数あるので、1つ1つの関数は別々のメモリアドレスに配置されます。デフォルト実引数の場合は、関数の実体は1つしかありません。この違いは、関数ポインタ(C言語編第38章)を使う場合に影響します。


練習問題 🔗

問題① 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);
}


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 C++編【言語解説】第8章「関数オーバーロード」の修正に合わせて、内容更新。

 「関数を削除する」の項を追加。

 新規作成。



前の章へ (第9章 型の変換)

次の章へ (第11章 関数テンプレート)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る