C++ Learning

第5回 関数オーバーロードとデフォルト引数

C では max_int / max_double / max_long型ごとに名前を変えて書いていた関数群が、C++ では同じ max という名前で書けます。これが関数オーバーロードです。あわせて、引数を省略できるデフォルト引数、両者の組み合わせ、曖昧エラーの避け方までを整理します。

このページで押さえること
✅ 最低限ここだけ覚える
  • 同名関数を引数の型/個数で区別できる
  • 戻り値の型だけが違うオーバーロードは不可
  • デフォルト引数は右から順に指定する
  • 曖昧になる場合はコンパイルエラーになる
⭐ 余裕があれば読む
  • オーバーロード解決の 3 ステップ
  • 暗黙変換が絡む 曖昧(ambiguous) の例
  • デフォルト引数はヘッダ側に書く
  • オーバーロード vs デフォルト引数の使い分け

1. C ではこう書いていた/C++ ではどう書くか

C では同名の関数を複数宣言することができません。そのため型ごとに名前をずらすか、マクロで逃げる、という二択になりがちでした。C++ ではこれを言語機能で解決します。

max.cC
// 型ごとに別名 int max_int(int a, int b); double max_dbl(double a, double b); long max_lng(long a, long b); // 呼び出し側で型に応じて使い分け int x = max_int(1, 2); double y = max_dbl(1.0, 2.0);
名前が3 通り/型を間違うとバグ
max.cppC++
// 同じ名前でオーバーロード int max(int a, int b); double max(double a, double b); long max(long a, long b); // 呼び出し側は名前 1 つ int x = max(1, 2); double y = max(1.0, 2.0);
名前は1 つ/コンパイラが型で振り分ける

呼び出し側の記述が減るだけでなく、型を取り違えるバグが起きにくくなるのが大きな利点です。max_intdouble を渡して暗黙変換で精度が落ちる、のような事故が構造的に防げます。

C 時代のマクロ解決: #define max(a,b) ((a)>(b)?(a):(b)) というマクロも古典的な解決策ですが、型チェックが効かない・副作用のある引数を複数回評価してしまう、などの落とし穴がありました。C++ のオーバーロードはこれを型安全に置き換えたものと位置付けられます。

2. オーバーロードの基本ルール

同名の関数を複数書くときの条件はシンプルです。

// ✅ 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 を呼ぶべきか決まらないためです。コンパイラは呼び出し地点の引数だけを見て関数を選ぶので、戻り値の情報は使えません。
余裕があれば読む ― ここから先は応用
最低限は上の 2 節で完結(C との対比/基本ルール/デフォルト引数)。解決の詳細と曖昧エラー回避は、実際に引っかかったときに戻ってくれば OK。

3. 解決の仕組みと曖昧エラー

コンパイラが「どのオーバーロードを呼ぶか」を決める手順はおおむね 3 段階です。

オーバーロード解決の 3 ステップ

  1. 候補集合を作る:同名の関数を全部集める
  2. 実引数の型に合うものだけを残す(完全一致 → 型昇格 → 標準変換 → ユーザ定義変換 の順に優先)
  3. 最良のものが 1 つに決まればそれを呼ぶ。複数が同じ優先度で残れば曖昧(ambiguous)エラー

曖昧エラーの例

void f(int);
void f(long);

f(3.14);   // ❌ エラー: double → int と double → long の両方が標準変換
              //          で同じ順位になり、どちらか決められない

対処法は 3 つ。

完全一致が強い

候補が複数あるときは、引数の型にそのまま一致する版が最優先で選ばれます。「暗黙変換が 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 より優先)

4. デフォルト引数

関数の引数に既定値を付けると、呼び出し側で省略できます。これもオーバーロードと並んで、C にはない便利機能です。

C: ラッパー関数で対応C
void log_msg_full(const char* msg, int level); void log_msg(const char* msg) { log_msg_full(msg, 3); // 既定 level=3 }
関数を2 つ書く必要
C++: デフォルト引数C++
void log_msg(const char* msg, int level = 3); // 呼び出し側 log_msg("ok"); // level=3 が使われる log_msg("err", 1); // level=1 を明示
関数は1 つ/呼び出し側が省略可

ルール

// ✅ 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 に定義本体、という構成が定石です。既定値を複数の場所に書くと、どちらが有効かで混乱します。

5. オーバーロード × デフォルト引数の組み合わせ

同じ目的を達成する道具が 2 つ(オーバーロードとデフォルト引数)あるので、使い分けに迷うことがあります。基本的な判断基準は以下です。

やりたいこと適切な道具理由
引数の個数を変えたい(型は同じ) デフォルト引数 1 つの実装で済む/管理しやすい
引数のが違う複数版を提供したい オーバーロード 型ごとに処理が違うのでデフォルト引数では表せない
引数の個数も型も違う オーバーロード デフォルト引数の右から順のルールに反する
省略時に呼ばれる動作を別実装にしたい オーバーロード デフォルト引数では実装を共有するしかない

曖昧の危険:組み合わせ時

// 両方が「1 引数版」として成り立つため曖昧
void f(int a);
void f(int a, int b = 0);

f(10);  // ❌ どちらを呼ぶか曖昧

こういう重複を作らないよう、オーバーロードとデフォルト引数は交差しない形で使うのが安全です。

6. 使いどころと落とし穴

オーバーロードが輝く場面

デフォルト引数が輝く場面

よくある落とし穴

次章へ: ここまでで STEP 1(C → C++ 橋渡し)の山は越えました。次章(第 6 回)は確認問題で一度知識を整理します。そのあと STEP 2 のモダン C++ 基礎(統一初期化・nullptr・constexpr)に進み、STEP 3 で std::vector など STL コンテナと auto が登場します。
広告スペース

確認クイズ

ここまでの理解を 3 問で確認してみましょう。

Q1. 次の C++ コードはコンパイルできる?

int    f(int);
double f(int);
はい、戻り値の型が違うのでオーバーロードが成立する
いいえ、戻り値の型だけが違うオーバーロードは不可
はい、ただし呼び出し時にキャストが必須になる
いいえ、同名関数は 1 つしか宣言できない
オーバーロードは引数の型/個数で区別します。戻り値の型だけが違うのは不可です。これは「戻り値を使わずに f(1); と呼んだとき、どちらを選ぶか決められない」ためです。

Q2. デフォルト引数の指定として正しいものはどれ?

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)
どれでも OK
デフォルト引数は右から順に付ける必要があります。途中の引数だけ既定値にすることはできません。呼び出し側でf(1) のように後ろから省略するので、右側に既定値を集める設計になっています。

Q3. オーバーロードとデフォルト引数の使い分けとして不適切なのは?

引数の個数を変えたいが型は同じ → デフォルト引数
引数の型を変えた複数版を提供したい → オーバーロード
後方互換を保ったまま関数に引数を追加したい → 末尾にデフォルト引数を追加
void f(int)void f(int, int=0) を両方定義すると呼び出しが整理される
void f(int)void f(int, int=0) を両方宣言すると、f(10) のような 1 引数呼び出しが曖昧になりコンパイルエラーになります。オーバーロードとデフォルト引数は交差しない形で使うのが原則です。
この記事をシェア