C++の関数

C++では、C言語よりも柔軟に関数を扱える機能が追加されています。

関数のオーバーロード

C言語では同じ名前の関数を定義するとエラーとなります。
C++では、関数の引数が異なれば同じ名前の関数を複数定義できます。
これを関数のオーバーロード(多重定義)といいます。


#include <iostream>

int max(int x, int y)
{
    return x > y ? x : y;
}

double max(double x, double y)
{
    return x > y ? x : y;
}

int main()
{
    using std::cout; using std::endl;
    using std::cin;

    int num1 = 5, num2 = 7;
    double num3 = 1.5, num4 = 0.2;

    cout << max(num1, num2) << endl;
    cout << max(num3, num4) << endl;
    cin.get();
}

関数maxは二つの値のうち大きい方を返す関数です。
このコードはC言語ではエラーとなりますが、C++ではきちんと動作します。

関数の呼び出し側ではint版を呼び出すかdouble版を呼び出すかは明示していませんが、引数に与えられたデータ型から自動的に正しい方の関数を呼び出してくれます。

戻り値のデータ型は同じでも構いませんし、違っても構いません。

なお、オーバーロードした関数は可能な限り同等の機能を持たせるべきです。
名前が同じなのに機能が異なるのは混乱の元です。

オーバーロードと暗黙の型変換

関数の実引数に与えられたデータ型は、仮引数のデータ型と一致しない場合には暗黙の型変換が行われます。
関数のオーバーロードにより複数の型を受け取れる場合、どちらに変換すべきかが曖昧な場合はエラーとなります。


#include <iostream>

int max(int x, int y)
{
    return x > y ? x : y;
}

double max(double x, double y)
{
    return x > y ? x : y;
}

int main()
{
    using std::cout; using std::endl;
    using std::cin;

    //エラー
    cout << max(2, 3.5) << endl;
    cin.get();
}

数値リテラルはint型とみなされ、小数を含む場合はdouble型とみなされます。
このコードはdouble型をint型に変換して比較すべきか、int型をdouble型とみなして比較すべきかが明確ではなく、コンパイルできません。

このような場合はキャストして関数に渡しましょう。

プロトタイプ宣言は必須

C言語でも関数のプロトタイプ宣言はありましたが、C++にも当然あります。
C言語では、コード上で関数を使用するより前に関数が定義されていればプロトタイプ宣言は必要ありませんでしたが、C++では省略してはならないとされています。
(実際には省略できてしまいますが)

なので、上のサンプルコードを正しく書き直すなら以下のようになります。


#include <iostream>

int max(int, int);
double max(double, double);

int max(int x, int y)
{
    return x > y ? x : y;
}

double max(double x, double y)
{
    return x > y ? x : y;
}

int main()
{
    //省略
}

このサイトのサンプルコードは、関数のプロトタイプ宣言を省略して記述しているものがたくさんあります。
手抜きと言えば手抜きですが、コードの行数を少なくした方が見やすいのではないか、という配慮でもあります。
きちんとしたプログラムを書く場合は関数のプロトタイプ宣言は必ず書くようにしましょう。

関数テンプレート

上記のサンプルコードのような処理は関数のオーバーロードを用いても良いですが、int型、short型、long型…と、使用する可能性のあるデータ型をすべて定義するのは結構大変です。
関数内の処理を見ると、引数のデータ型が異なるだけで中身はどちらも全く同じです。
このような場合は関数テンプレートという機能を用いたほうがより簡潔に書けます。


#include <iostream>

//プロトタイプ宣言
template<typename T>
T max(T, T);

//関数テンプレート
template<typename T>
T max(T x, T y)
{
    return x > y ? x : y;
}

int main()
{
    using std::cout; using std::endl;
    using std::cin;

    int num1 = 5, num2 = 7;
    double num3 = 1.5, num4 = 0.2;

    //関数テンプレートの呼び出し
    cout << max<int>(num1, num2) << endl;
    cout << max<double>(num3, num4) << endl;

    cin.get();
}

上のコードの23、24行目ではどちらも同じ関数maxを呼び出していますが、それぞれ引数の型が異なります。
関数を呼び出す時、関数名の後の<>記号の中にデータ型を指定することで、関数内では「T」がそのデータ型に置き換えられるわけです。

関数テンプレート

「template<typename 〇>」は「そういう決まり」として覚えてしまいましょう。
「T」というのは慣習的につけられる名前ですが、別に何でも構いません。
(おそらく「Type=データ型」なのでその頭文字)

この一文に続いて、関数を定義します。
関数内では「T」は呼び出し元で指定したデータ型として扱えます。
つまり上の画像の場合では「int型」と全く同じ意味になります。
引数の型の指定にも使えますし、戻り値の型にも指定できますし、関数内でT型の変数を宣言して使用することもできます。

この「T」をテンプレートパラメータと言います。
呼び出し側で指定するデータ型(例ではint型)をテンプレート引数といいます。

テンプレート引数は、実引数からデータ型が推測できる場合は省略が可能です。


int num1 = 5, num2 = 7;

cout << max<int>(num1, num2) << endl;
//↓<int>は省略できる
cout << max(num1, num2) << endl;

使用したいデータ型が複数ある場合はコンマで区切って定義します。


//関数テンプレート
template<typename T1, typename T2>
void func(T1 x, T2 y)
{
    //何か処理
}

int main()
{
    //関数呼び出し側
    func<int, double>(1, 2.0);
}

なお、関数テンプレートは「template<class T>」という宣言でも使用できます。
classというのはC++から導入された新しい機能です。

どちらを使っても構いませんが、関数テンプレートはclass以外を指定することができるので、意味的にはtypename(データ型名)を使用した方がいいかもしれません。

デフォルト引数

C言語では関数で定義されている通りに実引数を渡さないと関数を呼び出すことはできません。
C++でも基本的に同じですが、省略しても良い引数というのを定義することができます。


#include <iostream>

//文字列sに文字cが登場する数を返す
//引数cは省略が可能
int count(char *s, char c = ' ')
{
    int ret = 0;
    while (*s != '\0')
    {
        if (*s == c)
            ret++;
        s++;
    }
    return ret;
}

int main()
{
    using std::cout; using std::endl;
    using std::cin;

    char *str = "This is a pen";

    //引数を省略して呼び出し
    cout << count(str) << endl;

    //省略しないで呼び出し
    cout << count(str, 'i') << endl;

    cin.get();
}
3
2

関数countの第二引数は「=」で値を代入しているかのような記述になっています。
これをデフォルト引数(オプション引数)と言います。

このような書き方をすると、仮引数cにはデフォルト値(初期値)が与えられ、関数の呼び出し時に省略が可能になります。
省略した場合は自動的にデフォルト値が引数に指定されたものとして扱われます。

このサンプルコードでは、第二引数を省略すると自動的に半角スペースが指定されたものとして関数が実行されます。

もちろん引数を省略せずに呼び出すこともできます。
(28行目)

デフォルト引数は引数の最後に置く

関数の引数が複数ある場合、デフォルト引数を指定する引数は最後に配置する必要があります。


//OK
int func1(int x, int y = 0){}

//NG
int func2(int x = 0, int y){}

//OK
int func3(int x = 0, int y = 0){}

func2は、引数xに続く引数yがデフォルト引数を持っていないのでエラーとなります。
デフォルト値付きの引数の後ろにデフォルト値なしの引数がなければOKなので、引数がすべてデフォルト値付きである場合はどのような順番でも構いません。

関数のオーバーロードとデフォルト引数

デフォルト引数と同等の処理は、関数のオーバーロードを使って書くこともできます。


#include <iostream>

//文字列sに半角スペースが登場する数を返す
int count(char *s)
{
    return count(s, ' ');
}

//文字列sに文字cが登場する数を返す
int count(char *s, char c)
{
    int ret = 0;
    while (*s != '\0')
    {
        if (*s == c)
            ret++;
        s++;
    }
    return ret;
}

int main()
{
    using std::cout; using std::endl;
    using std::cin;

    char *str = "This is a pen";

    //引数を省略して呼び出し
    cout << count(str) << endl;

    //省略しないで呼び出し
    cout << count(str, 'i') << endl;

    cin.get();
}

オーバーロードでcount関数を二つ定義します。
処理の実体は引数を省略しない版の関数です。
引数省略版からは、省略しない版の関数に新たな引数を指定して呼び出しているだけです。
戻り値も、省略しない版から受け取った戻り値をそのまま返すだけです。

オーバーロードによる実装でもデフォルト引数による実装でも、関数呼び出し側からは同じように使用できます。
(関数ポインタを使う場合など、高度な内容になってくるとそれぞれの差はありますが、内容が初心者向けではありません)