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

先頭へ戻る

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

この章の概要

この章の概要です。

関連する話題が、以下のページにあります。

関数オーバーロード

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

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

C言語では、同じことをする関数であっても、型ごとに異なる名前の関数を用意しなければなりませんでした。 例えば、平方根を求める標準関数sqrt(⇒リファレンス)には、 型に応じて幾つかの亜種があります。

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

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

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

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

重要な点として、オーバーロードを使う際には、それぞれの関数が同じ目的を持っているようにして下さい。 名前は同じなのに、違うことをしてはいけません。


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

#include <cmath>

int main()
{
	std::sqrt(2.0f);  // float sqrt(float) が使われる
	std::sqrt(2.0);   // double sqrt(double) が使われる
	std::sqrt(2.0L);  // long double sqrt(long double) が使われる
}

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

#include <cstdio>
#include <string>

void write(int n);
void write(double n);
void write(std::string s);

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

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

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

void write(std::string s)
{
	std::printf("%s\n", s.c_str());
}

実行結果:

100
3.500000
xyz

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

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

#include <iostream>

int getValue();
double getValue();

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

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

int getValue()
{
	return 100;
}

double getValue()
{
	return 3.5;
}

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

曖昧な呼び出し

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

#include <iostream>

void func(float f);
void func(double f);

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

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

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

func(1); という呼び出しでは、実引数は 1 なので int型になります。 int型は、float型へも double型へも暗黙的に昇格できますが、今回の場合、float型と double型のどちらに昇格すれば良いか判断できないため、 コンパイルエラーになります。
この場合、キャストによって型を明確にすればコンパイル可能です。

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


また、前章で、ヌルポインタの表現について書きましたが、 ヌルポインタを整数の 0 で表現すると、次のような使い方で、意図に反した結果を生むかも知れません。

#include <iostream>

void func(int n);
void func(void* p);

int main()
{
	func(0);
}

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

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

実行結果:

int

func関数に渡した実引数 0 が、int型版を呼ぶつもりで指定した 0 なら良いのですが、 ヌルポインタのつもりで指定した 0 であると、思いがけず誤った関数が呼び出されてしまいます。
void*型版の func関数を呼び出したかったのであれば、明示的にキャストする必要があります。

func(static_cast<void*>(0));  // void*型版が呼び出される

念のため書いておくと、これは C++規格の NULLマクロを使っていても同じです。 C++規格では NULL の置換結果は整数の 0 なので、上記の例とまったく同じ結果になります。 ですからやはり、次のように明示的にキャストしないといけません。

func(static_cast<void*>(NULL));  // void*型版が呼び出される

これはとても格好悪いですが、やむを得ません。

ヌルポインタを 0 で表現する場合の問題を解決するために、ヌルポインタをクラスで表現する手法が存在します。 詳細は解説しませんが、例えばこの辺り(Wikibooks内 More C++ Idioms)を参照して下さい。 また、C++11 以降であれば、nullptrキーワードを使えば良いです。

デフォルト引数

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

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

#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 に指定したアドレスに格納します。

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

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

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

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

a = divide(10, 4);


デフォルト引数は、関数オーバーロードによって代替できます。 今回のサンプルプログラムであれば、次のように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言語編第37章)を使う場合に影響します。

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


練習問題

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

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

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


解答ページはこちら

参考リンク

更新履歴

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

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

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

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

'2013/12/28 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ