オーバーロード | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、関数のオーバーロードについて取り上げます。オーバーロードは、同じ名前の異なる関数を宣言する機能です。仮引数の型などに違いがあっても、行うべき仕事の内容が同じときに、同じ名前を使うことができるため、自然で簡潔なプログラムを書く助けになります。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



オーバーロード(多重定義) 🔗

オーバーロード(多重定義) (overload、overloading) あるいは関数オーバーロード (function overload、function overloading) は、同じスコープに、同じ名前の異なる関数や関数テンプレートを定義する機能です。

これまでにも「コンストラクタ」のページでコンストラクタを複数定義できることを説明したほか、「文字列操作」のページでは、std::string のさまざまなメンバ関数がオーバーロードされていることを紹介しています。

【上級】デストラクタはオーバーロードできません。

オーバーロードが可能であるためには、それぞれの宣言(オーバーロード宣言 (overloaded declarations))の内容が異なっていて、スコープが同一である必要があります1。そのうえで後述するルールがあります。記述の方法自体は、普通の関数を宣言・定義するときとまったく同じです。

class C {
public:
    // コンストラクタのオーバーロード
    C(int a);
    C(int a, int b);

    // メンバ関数のオーバーロード
    int f();
    int f(int a);
    long long int f(int a, int b);

    // 静的メンバ関数のオーバーロード
    static int sf();
    static int sf(int a);
};

// 関数のオーバーロード
void f(int a);
void f(double a);

// 関数テンプレートのオーバーロード
template <typename T>
void f(T a);
template <typename T>
void f(T a, T b);

スコープが異なっている場合は、外側のスコープに書いた宣言によって内側のスコープの関数宣言が隠蔽されるため(「スコープと名前空間」のページを参照)、オーバーロードをしていることにはなりません。


以下の関係性ではオーバーロードできません2

トップレベルの CV修飾子 (top-level cv-qualifiers)」とは、たとえば const int x のように、変数に対して修飾を行っている const や volatile のことです。const int* p の場合は p が const なのではなく指し示す先にあるものが const ということであり、これはトップレベルの CV修飾子とは呼びません。関数の仮引数で intconst int を使い分けたとしても、関数の型としては同一とみなされます(関数の本体としては、その仮引数の値を書き換えられないという違いをもちますが、呼び出す側としては何も違いはないため)。

以下の関係性はオーバーロードを妨げません。

オーバーロードと関数テンプレート

前のページで解説した関数テンプレートとオーバーロードはいずれも、同じ名前の関数のパターンを複数作り出す機能といえますが、それぞれに異なる特徴があります。

まず、オーバーロードは本体のコードが異なる複数の関数を作るのに対して、関数テンプレートの本体のコードは1つだけです。このためオーバーロードでは、仮引数などの違いに応じてそれぞれ実装する手間はありますが、本体のコードはそれぞれ異なったものにできます(ただし同じ名前の関数に、まったく異なる種類の仕事をさせるのは混乱のもとです)。

もう1つの違いとして、オーバーロードでは存在している型に対する関数のパターンしか作れませんが、関数テンプレートはテンプレート仮引数を使って、未確定な部分を作れるため、まだ存在していない未知の型にまで対応できます。


ただし、関数テンプレートをオーバーロードすることはできます。また、通常の関数と関数テンプレートや、メンバ関数とメンバ関数テンプレートの組み合わせでオーバーロードすることもできます。次のサンプルプログラムでは、add という名前の関数テンプレートと関数が定義されています。

#include <iostream>

class Money {
public:
    explicit Money(int amount) : m_amount{amount}
    {}

    inline int get_amount() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

// 関数テンプレート版の add
template <typename T>
T add(T a, T b)
{
    return a + b;
}

// Money型に特化した add関数のオーバーロード
Money add(const Money& a, const Money& b)
{
    return Money{a.get_amount() + b.get_amount()};
}

int main()
{
    auto r1 = add(100, 200);
    std::cout << r1 << "\n";

    Money m1 {1000};
    Money m2 {500};
    auto r2 = add(m1, m2);
    std::cout << r2.get_amount() << "\n";
}

実行結果:

300
1500

この場合、引数が Money型の場合には Money add(const Money& a, const Money& b) のほうが呼び出され、それ以外の場合では関数テンプレート版の add が呼び出されます。Money型の実引数を関数テンプレート版の仮引数でも受け取れますが、通常の関数がある場合は通常の関数のほうが優先されます。

このように、特定の引数の型と個数の組み合わせのときにだけ専用バージョンの関数を使い、それ以外のときには汎用バージョンの関数(テンプレート)を使うという使い分けが実現できます。

constメンバ関数と非constメンバ関数のオーバーロード

constメンバ関数か非constメンバ関数かという違いだけでもオーバーロードできることは、知っておく価値があります。

class C {
public:
    // 以下のオーバーロードは OK
    int f();
    int f() const;
};

呼び出し元になるオブジェクトが constオブジェクトなら constメンバ関数版が、そうでないなら非constメンバ関数版が呼び出されます。

constメンバ関数版と非constメンバ関数版を作る事情があるということは、これらのメンバ関数はデータメンバを書き換えることを目的としていないと思われます。データメンバを書き換えなければならない仕事を行うなら、constメンバ関数を作る必要はないはずです。一方で、単にデータメンバの値を出力するだけといったものでもないでしょう。そういう関数なのであれば constメンバ関数版だけがあればいいはずです。

このようなオーバーロードを行う目的は、データメンバの参照やポインタを返すことである場合が多いです。

class C {
public:
    struct Data {
        int a;
        int b;
        int c;
    };

    // 非constメンバ関数。呼び出し元で参照を受け取って、データメンバを書き換えることを許す
    inline Data& get_data()
    {
        return m_data;
    }

    // constメンバ関数。呼び出し元で参照を受け取っても、データメンバは書き換えられない
    inline const Data& get_data() const
    {
        return m_data;
    }

private:
    Data m_data;
};

constメンバ関数版では const参照で、非constメンバ関数版では非const の参照で返すようになっています。いずれのメンバ関数も、本体のコードは return m_data; の1行だけですが、もしコードがもっと長大だったり複雑だったりすると、同じコードが重複することが保守上の問題になります。あとでどちらか片方だけを修正してしまうというよくあるミスの元です。

同じコードになるのなら別のメンバ関数に共通化すればいいと考えるかもしれませんが、その共通関数を constメンバ関数にも非constメンバ関数にもできません。

class C {
public:
    inline Data& get_data()
    {
        return common_get_data();
    }
    inline const Data& get_data() const
    {
        return common_get_data();
    }

private:
    // これが constメンバ関数だったら、非constメンバ関数版の get_data() が返したい Data& を返せない。
    // これが非constメンバ関数だったら、constメンバ関数版の get_data() から呼べない。
    ??? common_get_data() ???
    {
        return /* 複雑なコード */;
    }
};

この問題を解決する方法を2つ紹介します。

共通コードを constメンバ関数に書く

共通の関数を作るのではなく、constメンバ関数版のほうに目的のコードを書いて、非constメンバ関数版ではそれを強引に呼び出すというものです。

class C {
public:
    inline Data& get_data()
    {
        // constメンバ関数版を呼び出し、戻り値の const を外して返す。
        return const_cast<Data&>(static_cast<const C*>(this)->get_data());
    }
    const Data& get_data() const
    {
        return /* 複雑なコード */;
    }
};

非constメンバ関数版がやっていることはこうです。

  1. *this を constオブジェクトにするため、static_cast で this を constポインタにする
  2. 1 で得たポインタを使って同名のメンバ関数を呼ぶことで、constメンバ関数版を呼び出す
  3. 返された結果(const Data&)から const を取り外すため、const_cast を行う
  4. 3 で得た結果を返す

非constメンバ関数版の本体では、thisポインタは C* という型ですから、そのまま get_dataメンバ関数を呼び出すと、自分自身を呼び出す結果になってしまいます。constメンバ関数版を呼び出すには、強引に自分自身を const にしてしまえばよく、そのために thisconst C* にキャストしています(*thisconst C& にすることでも可能です)。このキャストは static_cast で行えます(このあと登場する const_cast でも可能)。

constメンバ関数が返す結果は const が付加された const Data& という型ですから、非constメンバ関数が返したい Data& にするためには const を取り外す必要があります。参照やポインタの const は const_cast で取り外せます。const_cast の文法は static_cast や reinterpret_cast(「配列とポインタ」のページを参照)と同様です。

const_cast<>()

const_cast は、参照やポインタについている const や volatile(未解説)を取り外したり、付加したりできます。これ以外の場面で登場する const を操作することはできません。

const を取り外してしまうことに不安を感じるかもしれません。実際、const_cast は基本的には避けるべきものです。const を外すことは、コンパイラのチェックを黙らせているだけであって、指し示しているオブジェクトを書き換えることが安全である保証はありません。

今回の使い方の場合は、非constメンバ関数版を呼び出すことからスタートしているので、このオブジェクトが書き換えられる可能性は想定内といえます。そのため、この使い方は基本的に安全です。

const_cast を使うほかの場面として、関数内で書き換えないのにも関わらず、仮引数のポインタに const が付加されていない関数(しかも修正することもできない)を呼び出すときが挙げられます。たとえばC言語の文字列リテラルの型は char[] であるため、文字列を受け取る仮引数に const が付いていないことがあります。そのような関数を C++ に持ち込むと、C++ の文字列リテラルは const char[] であるため、const を外さなければ渡すことができません。

共通コードをメンバ関数テンプレートに書く

もう1つの方法は、メンバ関数テンプレート(「関数テンプレート」のページを参照)にコードを共通化する方法です3

class C {
public:
    struct Data {
        int a;
        int b;
        int c;
    };

    inline Data& get_data()
    {
        return common_get_data(this);
    }

    inline const Data& get_data() const
    {
        return common_get_data(this);
    }

private:
    Data m_data;

private:
    template <typename T>
    static auto& common_get_data(T* self)
    {
        return self->m_data;
    }
};

common_get_dataメンバ関数テンプレートにコードを抜き出しています。

まず、このメンバ関数テンプレートは静的メンバ関数にしておかなければなりません。静的メンバ関数はクラスに結びついた存在であり、特定のオブジェクトとは無関係であるため、thisポインタとの関係性がありません(「静的メンバ」のページを参照)。したがって、呼び出し元が constメンバ関数(thisポインタが constポインタ)でも、非constメンバ関数(thisポインタが非constポインタ)でも問題なく呼び出せます。

しかし静的メンバ関数になってしまうと、thisポインタが使えないがために、データメンバにアクセスできなくなってしまいます。そこで、引数で thisポインタを渡してやればいいですが、ここでも thisポインタの const の有無が問題になります。仮引数を T* にすることで、const の有無の判断をテンプレート実引数推論(「関数テンプレート」のページを参照)に任せることで回避できます。

さらに戻り値の型を auto& にすることで、戻り値の型は return self->m_data; の結果の型になります(「関数から値を返す」のページを参照)。self の型が C* と判断されたのなら Data& ですし、const C* と判断されたのなら const Data& となります。

こうして const_cast を行うことなく、テンプレートと auto による自動的な型推論の中に const の有無の判断を隠して、constメンバ関数と非constメンバ関数のコードの共通化を図れます。

オーバーロード解決 🔗

オーバーロードされている関数を呼び出すとき、実引数の内容から判断して、どの関数を選ぶのが適切であるかを決定するルールが定められており、オーバーロード解決 (overload resolution) と呼ばれています。この判断はコンパイル時点で行われます。

オーバーロード解決のルールは非常に複雑ですが4、ほとんどの場合で自然だと感じられる結果になるように設計されています。以下にオーバーロード解決の流れを説明していますが、ほとんどの場合、自然だと感じられる結果になるように設計されているので、ルールを正確に把握しておかなければならないものではありません。

オーバーロード解決のステップは次のようになっています。

  1. 名前探索を行い、候補関数の一覧をつくる
  2. 候補関数の中から、呼び出すことができる関数を、適切関数として絞り込む
  3. 実引数から仮引数への暗黙的な型変換を考慮して、最適関数を決定する

名前探索を行い、候補関数をつくる

名前探索 (name lookup) は、ソースコード上で使用されている名前が、実際には何を意味したものなのかを決定することをいいます。名前探索自体も相当に複雑なので5、これについても概要だけ示すことにします。

名前探索のルールは、スコープ解決演算子(::) による修飾を行っているかどうかによって異なります。

スコープ解決演算子で修飾されている名前に対する名前探索は、修飾の名前探索 (qualified name lookup) と呼ばれます。この場合、:: の左側に記述したクラス、名前空間、scoped enum から該当する名前を探索します。:: の左側に何もない場合はグローバル名前空間から探索します。

スコープ解決演算子で修飾されていない名前に対する名前探索は、非修飾の名前探索 (unqualified name lookup) と呼ばれます。この場合、その名前が使われている場所から発見できる名前を探索します。usingディレクティブ(「スコープと名前空間」のページを参照)や using宣言(「スコープと名前空間」のページを参照)の効力の影響を受けます。

非修飾の名前探索には、実引数依存の名前探索(ADL) (Argument Dependent name Lookup) と呼ばれる更なるルールが存在します6。これは、関数呼び出しの際、該当する関数を探すときに、実引数で指定した内容も考慮に加えるというものです。たとえば、f(T x); のように関数f を呼び出すとき、この呼び出しが記述されている場所から見えている f だけでなく、実引数の x の型が所属している名前空間も探索範囲に加えるということです。

namespace n1 {
    struct T {
        int a;
        int b;
    };
    void f(T x);
}

namespace n2 {
    void g()
    {
        n1::T x {};
        f(x);  // OK. x が n1::T であることから、n1 も探索し、n1::f を見つける
    }
}

こうした名前探索を行った結果、いくつかの関数や関数テンプレートを見つける可能性があります。これらの関数のことを候補関数 (candidate functions) と呼びます。

候補関数は名前の一致は確認されていますが、その関数が呼び出し可能であるかどうかを考慮していません。たとえば、以下のような関数であっても候補関数には選ばれます。

候補関数を1つ以上発見できた場合は、候補関数を絞り込むステップへ進みます。候補関数が1つも発見できなければ、使おうとしている名前が何であるのか決定できないため、コンパイルエラーとなります。

候補関数を絞り込む 🔗

候補関数の中から、実際に呼び出せる関数、つまり適切関数 (viable functions) を絞り込みます7

「viable functions」をうまく日本語訳することが難しいため、適切関数、有効な関数、妥当な関数などさまざまな訳語があります。

まず、仮引数の個数に対して、実引数の個数が十分であるかを調べます。その結果、以下のいずれかを満たしていれば、次の判断に進むことができます。満たさなければ適切関数ではありません。

【C言語プログラマー】... は printf関数などでおなじみの可変個引数のことです。

次に、実引数が、対応する仮引数の型と一致する、あるいは暗黙に型変換できるかどうかを調べます。型を一致させられるのであれば適切関数とみなされ、そうでなければ適切関数ではありません。

適切関数が1つだけであれば、その関数がオーバーロード解決の結果となります。複数の適切関数が残った場合は次のステップへ進んで1つに定めます。適切関数がなければコンパイルエラーです。

最適関数を決定する 🔗

適切関数の中から、最もふさわしい関数を1つに定めます。ここで選ばれる関数を、最適関数 (best viable functions) と呼びます8

「best viable functions」もうまく日本語訳することが難しいですが、適切関数のうち最もふさわしい関数ということで、ここでは最適関数としています。

基本的な考え方としては、実引数の型が、仮引数の型にどれだけ近いかを比較し、最も近しい関数が選び出されるということです。引数が複数あることもありますから、第1引数なら一方の関数のほうが近くても、第2引数では他方の関数のほうが近くて、同点と言わざるをえない場合もあります。そのような場合、最適関数が1つに定めきれず、曖昧であるという主旨のコンパイルエラーになります。

型が完全に一致していれば、もっとも近いといえます。

void f(int a, int b);
void f(int a, double b);

f(100, 0);  // f(int, int) と完全一致。これが選ばれる

暗黙の型変換の内容が同じであるなら、それが必要な引数が少ない方が近いといえます。

void f(int a, int b, long c);
void f(int a, long b, long c);

f(0, 0, 0);  // f(int, int, long) は int -> long の型変換が1つだけ必要。これが選ばれる
             // f(int, long, long) は int -> long の型変換が2つ必要。

この例で、2つ目の関数f の第3引数が int であったら、暗黙の型変換が必要な個数が同じになるため、どちらがより一致しているとはいえず、コンパイルエラーになります。

void f(int a, int b, long c);
void f(int a, long b, int c);

f(0, 0, 0);  // f(int, int, long) も f(int, long, int) も、
             // int -> long の型変換が2つ必要なため曖昧である

通常の関数と、関数テンプレートがある場合、通常の関数のほうがより近いといえます。

void f(int a, int b);

template <typename T>
void f(T a, T b);

f(100, 0);     // 関数テンプレートの f にも一致するが、通常の関数である f(int, int) が選ばれる
f(12.5, 15.5); // 関数テンプレートの f が選ばれる


また、暗黙の型変換は以下のように分類できます。

  1. 標準の型変換
  2. ユーザー定義の型変換
  3. エリプシス変換

上にあるものほど優先されます。つまり上にある型変換を使っているもののほうが、より近しい関数であると判定されます。

標準の型変換 (standard conversion) は、C++ の標準機能として備わっている暗黙の型変換のことです9。たとえば以下のような変換が標準の型変換です。

ユーザー定義の型変換 (user-defined conversion) は、プログラマーが明示的に定義した型変換のことです。変換コンストラクタ(「コンストラクタ」のページを参照)や、変換関数(未解説)が該当します10

エリプシス変換 (ellipsis conversion) は、仮引数が ... となっている場合に行われる型変換のことです。この機能は未解説です。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次の関数 f1~f8 で、オーバーロードが成功するものとエラーになるものを分けてください。

void f1();
void f1(int a, int b);

int f2(int a);
double f2(int a);

int f3(int a, int b = 0);
int f3(int a = 0, int b = 0);

int f4(int a, int b);
template <typename T>
int f4(T a, T b);

template <typename T>
T f5();
template <typename T>
T f5(T a, T b);

class C1 {
public:
    int f6();
    int f6() const;
};

class C2 {
public:
    int f7();
    static int f7(int a);
};

class C3 {
public:
    int f8();

private:
    int f8(int a, int b);
};

解答・解説

問題2 (基本★)

以下の関数f の呼び出しが、それぞれ異なる関数を呼び出すように関数f のオーバーロードを作成してください。また、これら以外の型の実引数を渡した場合に呼び出される関数テンプレートf も作成してください。

int main()
{
    int a {100};
    short b {200};
    double c {15.5};

    f(a);
    f(b);
    f(c);
    f(nullptr);
}

解答・解説

問題3 (応用★★)

RingBufferクラステンプレートの実装でも、frontメンバ関数と backメンバ関数が、メンバ関数と constメンバ関数の違いによってオーバーロードされています。本体のコードをメンバ関数テンプレートを使って共通化してください。

現状の RingBufferクラステンプレートの実装は、「関数テンプレート」のページにあります。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




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