C では max_int / max_double / max_long と型ごとに名前を変えて書いていた関数群が、C++ では同じ max という名前で書けます。これが関数オーバーロードです。あわせて、引数を省略できるデフォルト引数、両者の組み合わせ、曖昧エラーの避け方までを整理します。
C では同名の関数を複数宣言することができません。そのため型ごとに名前をずらすか、マクロで逃げる、という二択になりがちでした。C++ ではこれを言語機能で解決します。
呼び出し側の記述が減るだけでなく、型を取り違えるバグが起きにくくなるのが大きな利点です。max_int に double を渡して暗黙変換で精度が落ちる、のような事故が構造的に防げます。
#define max(a,b) ((a)>(b)?(a):(b)) というマクロも古典的な解決策ですが、型チェックが効かない・副作用のある引数を複数回評価してしまう、などの落とし穴がありました。C++ のオーバーロードはこれを型安全に置き換えたものと位置付けられます。
同名の関数を複数書くときの条件はシンプルです。
const・&・&& の違いでもオーバーロード可能// ✅ OK(引数の型が違う) int f(int); int f(double); // ✅ OK(引数の個数が違う) int g(int); int g(int, int); // ❌ NG(戻り値だけ違う) int h(int); double h(int); // redefinition エラー // ✅ OK(const の有無で区別できる=メンバ関数) class X { int value(); // 非 const 版 int value() const; // const 版 };
h(1); と呼んだ場合、どちらの h を呼ぶべきか決まらないためです。コンパイラは呼び出し地点の引数だけを見て関数を選ぶので、戻り値の情報は使えません。
コンパイラが「どのオーバーロードを呼ぶか」を決める手順はおおむね 3 段階です。
void f(int); void f(long); f(3.14); // ❌ エラー: double → int と double → long の両方が標準変換 // で同じ順位になり、どちらか決められない
対処法は 3 つ。
f(static_cast<int>(3.14));void f(double); を用意候補が複数あるときは、引数の型にそのまま一致する版が最優先で選ばれます。「暗黙変換が 1 回必要」より「変換なしで呼べる」方が常に勝ちます。
void print(int x) { std::cout << "int: " << x; } void print(double x) { std::cout << "double: " << x; } print(42); // → "int: 42" (完全一致) print(3.14); // → "double: 3.14"(完全一致) print(42L); // → "double: 42" (long→double が long→int より優先)
関数の引数に既定値を付けると、呼び出し側で省略できます。これもオーバーロードと並んで、C にはない便利機能です。
// ✅ OK: 右から順に省略可 void draw(int x, int y, int color = 0, int alpha = 255); // ❌ NG: 途中だけ既定にはできない void draw(int x, int y = 0, int color); // エラー
.hpp)に宣言と既定値、.cpp に定義本体、という構成が定石です。既定値を複数の場所に書くと、どちらが有効かで混乱します。
同じ目的を達成する道具が 2 つ(オーバーロードとデフォルト引数)あるので、使い分けに迷うことがあります。基本的な判断基準は以下です。
| やりたいこと | 適切な道具 | 理由 |
|---|---|---|
| 引数の個数を変えたい(型は同じ) | デフォルト引数 | 1 つの実装で済む/管理しやすい |
| 引数の型が違う複数版を提供したい | オーバーロード | 型ごとに処理が違うのでデフォルト引数では表せない |
| 引数の個数も型も違う | オーバーロード | デフォルト引数の右から順のルールに反する |
| 省略時に呼ばれる動作を別実装にしたい | オーバーロード | デフォルト引数では実装を共有するしかない |
// 両方が「1 引数版」として成り立つため曖昧 void f(int a); void f(int a, int b = 0); f(10); // ❌ どちらを呼ぶか曖昧
こういう重複を作らないよう、オーバーロードとデフォルト引数は交差しない形で使うのが安全です。
std::to_string(int) / std::to_string(double) / … のように全型版を提供)int と long、int と unsigned int のようにオーバーロードを作ると、整数リテラルで曖昧になりがちusing Base::func; を書かないと基底のオーバーロードが見えなくなる(第 36 回で扱う)= 0 が int → ポインタ型への変換候補になると、思わぬオーバーロードが選ばれるstd::vector など STL コンテナと auto が登場します。
ここまでの理解を 3 問で確認してみましょう。
f(1); と呼んだとき、どちらを選ぶか決められない」ためです。void f(int a = 0, int b, int c)void f(int a, int b, int c = 10)void f(int a, int b = 0, int c)void f(int a = 1, int b, int c = 5)f(1) のように後ろから省略するので、右側に既定値を集める設計になっています。void f(int) と void f(int, int=0) を両方定義すると呼び出しが整理されるvoid f(int) と void f(int, int=0) を両方宣言すると、f(10) のような 1 引数呼び出しが曖昧になりコンパイルエラーになります。オーバーロードとデフォルト引数は交差しない形で使うのが原則です。