Menu

C++の関数オーバーロード:同じ名前、異なる引数

C++の関数オーバーロードを使うと、引数リストが異なっていれば複数の関数が1つの名前を共有できます。オーバーロード解決がどのように一致を選ぶのか、なぜ戻り値の型だけでは区別にならないのか、そして避けるべき曖昧さとデフォルト引数の落とし穴を学びましょう。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

1つの名前、多くのバージョン

異なる種類のデータに対して同じ操作が必要になることはよくあります。int を表示する、string を表示する、double を表示する、といった具合です。言語によっては printIntprintStringprintDouble を作ることになるでしょう。C++ では、それらすべてに同じ名前を付けることができ、引数によって区別します。これが関数オーバーロードです。

ルールはシンプルです。引数リストが異なる限り、複数の関数が名前を共有できます。引数の数、型、または順序のいずれかで違っていればよいのです。コンパイラは各呼び出し箇所の引数を見て、一致するバージョンをあなたの代わりに選びます。

3つの関数、1つの名前。各呼び出しは、引数に合う引数型を持つバージョンに着地します。これこそが std::cout << x を int、double、string のいずれに対しても同じように機能させているものです。operator<< は何度もオーバーロードされています。

何が別のオーバーロードと見なされるか

オーバーロードは引数リストのみで区別されます。次のように変えられます。

int  area(int side);                 // 引数1つ
int  area(int width, int height);    // 引数2つ  -> 別物
double area(double r);               // 型が違う -> 別物

void log(string msg, int level);     // 順序が重要…
void log(int level, string msg);     // …なのでこれも別物

これらはそれぞれ正当な、独立したオーバーロードです。コンパイラは area という名前のすべての関数から候補集合を作り、その後で引数の数と型によって一致を取ります。

戻り値の型だけでは足りない

ここがほとんどの人がつまずく落とし穴です。戻り値の型ではオーバーロードできません。 戻り値はオーバーロードの選択に何の役割も果たしません。なぜなら、コンパイラは戻ってくるものを見るよりずっと前に、引数からどの関数を呼ぶかを決めるからです。

int    convert(double x);   // OK
double convert(double x);   // エラー:再定義 - 戻り値の型だけが違う

これはコンパイルできません。引数リストが同一なら、オーバーロードの観点では2つの宣言は同じ関数であり、再定義エラーになります。結果の型で分岐したい場合は、引数を変えてください(あるいは呼び出し箇所でテンプレートやキャストである static_cast を使ってください)。

オーバーロード解決はどのように勝者を選ぶか

呼び出しを行うと、コンパイラは実行可能なすべてのオーバーロードをランク付けし、最良の一致を選びます。おおまかには、次の順で優先します。

  1. 完全一致(変換不要)。
  2. 昇格(例:char または short -> intfloat -> double)。
  3. 標準変換(例:int -> doubledouble -> int、基底クラスへのポインタ)。

ちょうど1つのオーバーロードが他のすべてより厳密に優れている場合、それが勝ちます。完全一致が変換に勝つ様子を見てみましょう。

'A'char ですが、int への昇格は double への変換よりも上位にランクされるため、int のオーバーロードが呼ばれます。これらのランク付けルールこそが、オーバーロード解決がたいてい「ちょうど正しいことをする」理由であり、時々あなたを驚かせる理由でもあります。

曖昧さの落とし穴

2つのオーバーロードが同程度に良く、どちらも厳密に優れていない場合、コンパイラは推測を拒み、曖昧な呼び出しを報告します。教科書的な例は、それぞれが同じランクの変換を必要とする2つのオーバーロードです。

void f(int x);
void f(double x);

f(0L);   // エラー:曖昧 - long -> int と long -> double は同ランクの変換

intdoublelong の完全一致ではなく、両方の変換が同じランクにあるため、呼び出しは曖昧です。きれいな解決策が2つあります。

関連する驚きとして、文字列リテラルを渡す場合があります。void g(const string&)void g(bool) はどちらも g("hi") に名乗りを上げ、bool が勝つことがあります。なぜなら、const char*std::string を構築するより少ないステップで bool に変換される(非nullなら -> true)からです。文字列リテラルが不可解にも bool オーバーロードを呼んでいるのを見かけたら、これが理由です。完全一致を取らせるために const char* または const string& のオーバーロードを追加してください。

オーバーロードとデフォルト引数は相性が悪い

デフォルト引数はオーバーロードの代わりにはならず、両者を組み合わせると曖昧さが生じます。どちらも同じ呼び出しに応えられるため、コンパイラは選べません。

void connect(string host, int port = 8080);   // 引数1つで呼べる
void connect(string host);                     // これも引数1つで呼べる

connect("localhost");   // エラー:曖昧 - どちらも引数1つに一致する

呼び出しの形ごとにアプローチを1つ選びましょう。振る舞いが同一で単にオプション引数が欲しいだけならデフォルト引数を使い、異なる引数リストが本当に異なるコードを実行すべきならオーバーロードを使ってください。2つのシグネチャが同じ引数の数で衝突するように混ぜるのは、確実に曖昧さエラーになります。

押さえておくべき区別がもう1つあります。オーバーロードはオーバーライドではありません。オーバーロードは、同じスコープ内で同じ名前だが異なる引数を持つ関数の間で、コンパイル時に解決されます。オーバーライドは派生クラスの virtual 関数を実行時に置き換えるもので、同じシグネチャを必要とします。これは後ほど仮想関数で扱うテーマです。

次は:ラムダ

オーバーロードは、コンパイル時に選ばれる複数の型付き実装を1つの名前に与えます。しかし時には、名前付き関数をまったく必要としないこともあります。使う場所のすぐそこに定義する、使い捨ての小さな関数が欲しい、しかもしばしば sort のようなアルゴリズムに渡すために、というケースです。それがまさにラムダです。インラインで書け、周囲の変数をキャプチャでき、1つの式で渡せる無名関数です。次は、その書き方と、完全な名前付き関数を上回るのはどんなときかを見ていきます。

よくある質問

C++の関数オーバーロードとは何ですか?

関数オーバーロードを使うと、引数リストが(数・型・順序のいずれかで)異なる限り、同じ名前の関数を複数定義できます。コンパイラは渡された引数に基づいてどれを呼ぶかを選ぶので、print(42)print("hi") は2つの異なる print 関数を呼び出せます。

C++の2つの関数は戻り値の型だけで区別できますか?

いいえ。オーバーロードは引数リストで異なっていなければなりません。int f(int)double f(int) はコンパイルエラーになります。戻り値の型はオーバーロード解決に使われるシグネチャの一部ではありません。なぜなら、コンパイラは戻り値が使われるよりも前に、呼び出し箇所の引数からオーバーロードを選ぶからです。

オーバーロードされた関数で「曖昧な呼び出し」エラーが起きる原因は何ですか?

2つのオーバーロードが同程度に良い一致で、コンパイラがどちらかを優先できないときに起きます。典型例は、f(int)f(double)f(0L)long)で呼ぶ場合で、どちらも同じランクの変換を必要とします。完全一致のオーバーロードを追加するか、引数を望む型へキャストすれば解決できます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める