関数オーバーロード | Programming Place Plus C++編【言語解説】 第8章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新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宣言を使うと、名前を現在のスコープ内に取り込むことができるのでした(第3章)。

#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. ユーザー定義の暗黙の型変換(第19章
  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

ヌルポインタの表現と関数オーバーロードの問題

第2章でヌルポインタを NULL で表現するか、0 で表現するかという話に触れましたが、これはオーバーロードされた関数の呼び出しを曖昧にしてしまう問題にも絡みます。

#include <iostream>

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

void func(void* p)
{
    std::cout << "void*" << std::endl;
}

int main()
{
    func(0);
}

実行結果:

int

「func(0)」という呼び出しによって、仮引数が int型の方の関数が選択されています。これは当然の話ではあるのですが、この実引数「0」がヌルポインタを意図したものであったとすればどうでしょう? その場合は、仮引数が void* の方が選択されることを期待していた可能性がありそうです。

一方、「func(NULL)」のように呼び出すとどうなるでしょう? NULL の置換結果は整数型の 0 ですから、やはり、int型の引数を持つ関数が呼び出されます。これは完全に意図に反していると思われます。

このように、ヌルポインタの表現による問題があるため、いつもキャストを明示する必要があります。

#include <iostream>

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

void func(void* p)
{
    std::cout << "void*" << std::endl;
}

int main()
{
    func(static_cast<void*>(0));
    func(static_cast<void*>(NULL));
}

実行結果:

void*
void*

煩わしいですし、特に、実引数が NULL の方はひどいものです。そもそも NULL を使っている以上、ポインタであるという意図は明らかであるはずなのですから。

このように、仮引数を整数型とポインタ型とでオーバーロードする関数とは相性が非常に悪いです。


デフォルト実引数

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

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

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

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

構文的には、デフォルト “仮引数” であるような感じがするかもしれませんが、意味的にデフォルト “実引数” です(英語では、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つの整数値が同じかどうかは ==演算子で調べられますが、char型で表現された2つの文字列が同じかどうか調べるには ==演算子が使えません。どちらでも使えるような equal関数を、関数オーバーロードを利用して作成してください。

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

#include <iostream>

void func();
void func(int a = 0);
void func(int a = 0, int b);

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

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


解答ページはこちら

参考リンク


更新履歴

’2018/8/16 全体的に見直し修正。
– 「関数オーバーロードとスコープ」「関数オーバーロードの解決手順」の項を追加。
– 「曖昧な呼び出し」の項の内容を細分化して、「ヌルポインタの表現と関数オーバーロードの問題」とした。
– 「デフォルト引数」という表記を、デフォルト実引数に改めた。

’2017/5/30 「関数オーバーロード」の項の一部を、「曖昧な呼び出し」として、切り出した。

’2014/1/23 「ヌルポインタ」の項の一部を、第8章へ移動。他の部分を、「関数オーバーロード」の項へ合体。

’2014/1/14 VisualC++2008 の対応終了。

’2014/1/13 clang 3.0 に対応。

’2013/12/28 新規作成。



前の章へ (第7章 C++ の型とキャスト)

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

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

Programming Place Plus のトップページへ



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