関数の新機能
C++では、C言語よりも柔軟に関数を扱える機能が追加されています。
関数のオーバーロード
C言語では同じ名前の関数を定義するとエラーとなります。
C++では、引数の異なる同じ名前の関数を複数定義できます。
これを関数のオーバーロード(多重定義)といいます。
#include <iostream>
//xとyの大きいほうを返す(int型)
int max(int x, int y)
{
return x > y ? x : y;
}
//xとyの大きいほうを返す(double型)
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型に変換すべきかが明確ではなく、コンパイルできません。
このような場合は明示的にキャストして関数に渡す必要があります。
関数テンプレート
上記のサンプルコードのような処理は関数のオーバーロードを用いても良いですが、int型、double型、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型)をテンプレート実引数といいます。
仮引数は英語でパラメータ(parameter)、実引数は英語でアーギュメント(arguments)なので、テンプレート実引数は「テンプレートアーギュメント」なのですが、あまりこうは呼ばれないようです。
テンプレート実引数は、関数の実引数からデータ型が推測できる場合は省略が可能です。
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);
}
なお、テンプレート仮引数が複数ある場合、仮引数名は「T」「U」「V」…と続けることが多いようです。
これは単純にTからアルファベット順に並べているだけです。
一文字にこだわる必要はないので好きに決めて構いません。
関数テンプレートはtemplate<class T>
という構文でも使用できます。
(classというのはC++から導入された新しい機能です)
どちらを使っても構いませんが、関数テンプレートはclass以外を指定することができるので、意味的にはtypename
(データ型名)を使用した方がいいかもしれません。
ヘッダファイルに分割する場合
ある程度の規模のプログラムになるとファイルを複数に分割することになりますが、関数テンプレートを使用している場合は注意点があります。
通常、ヘッダーファイル(.h/.hppファイル)には関数の宣言を記述し、関数の定義はソースファイル(.cppファイル)に記述します。
しかし関数テンプレートを使用した関数で同じことをするとビルド(実行ファイルの生成)に失敗します。
#pragma once
template<typename T>
T max(T, T);
template<typename T>
T max(T x, T y)
{
return x > y ? x : y;
}
//エラーになる例
#include <iostream>
#include "max.h"
int main()
{
int m = max(1, 2);
}
このコードのコンパイル自体は成功するのですが、複数のオブジェクトファイル(コンパイル後に生成されるファイル)を合体するリンクという作業でエラーが発生し、実行ファイルを生成することができません。
Visual Studioの場合は「未解決の外部シンボル」というエラーが表示されます。
LNK2019 未解決の外部シンボル "int __cdecl max<int>(int,int)" (??$max@H@@YAHHH@Z) が関数 _main で参照されました
これは要するに「max」という名前の関数を使用しているが、その実体が見つからないというエラーです。
関数テンプレートは文字通り関数を生成するための「テンプレート(ひな形)」であって、それ自体は関数としては機能しません。
例えばテンプレート引数にint型を指定して呼び出すと、int型版の関数の実体が生成されます。
次にdouble型を指定して呼び出せば、先ほどとはまた別のdouble型版の関数の実体が生成されます。
これらの実体が関数として機能します。
そして、コンパイルというのはソースファイル単位で行われるのですが、このとき他のソースファイルの内容については関知しません。
「max.cpp」のコンパイル時は他のソースファイル(main.cppなど)からどのような呼び出しが行われているか、あるいは使用すらされていないのかを知ることができません。
つまり関数テンプレートmax
はどこからも呼び出されていないのと同じ状態となり、実体が生成されません。
関数の実体がどこにも存在しないため、他のソースファイルからの呼び出し時にリンクエラーになるわけです。
長くなりましたが、解決策は単純にヘッダファイル内に定義を書くことです。
呼び出し側から関数テンプレートの定義が見えるので、問題なく実体化が可能です。
ただし、他人に公開する場合は関数の実装が隠蔽できないというデメリットがあります。
あるいは汎用性が失われますが、あらかじめ使用するデータ型の関数定義をcppファイル内に記述することでも実体を生成できるのでリンクに成功します。
template<typename T>
T max(T x, T y)
{
return x > y ? x : y;
}
//int型版とdouble型版の関数の実体をここで生成する
template int max<int>(int, int);
template double max<double>(double, double);
これはテンプレートの明示的実体化という方法です。
上記はint型とdouble型の関数を実体化しています。
詳しくはテンプレートの特殊化の項で改めて説明します。
デフォルト引数
C言語では関数で定義されている通りに実引数を渡さないと関数を呼び出すことはできません。
C++でも基本的に同じですが、省略しても良い引数というのを定義することができます。
#include <iostream>
//文字列sに文字cが登場する数を返す
//引数cは省略が可能
size_t count(const char* s, char c = ' ')
{
size_t ret = 0;
while (*s != '\0')
{
if (*s == c)
ret++;
s++;
}
return ret;
}
int main()
{
using std::cout; using std::endl;
using std::cin;
const char* str = "This is a pen";
//引数を省略して呼び出し
cout << count(str) << endl;
//省略しないで呼び出し
cout << count(str, 'i') << endl;
cin.get();
}
3 2
関数count
の第二引数は=
で値を代入しているかのような記述になっています。
これをデフォルト引数(オプション引数)と言います。
このような書き方をすると、仮引数c
は関数の呼び出し時に省略が可能になります。
省略した場合は引数c
には=
の右側に指定した値が引数に指定されたものとして扱われます。
上記コードの場合は第二引数を省略すると自動的に半角スペースが指定されたものとして関数が実行されます。
もちろん引数を省略せずに呼び出すこともできます。
(28行目)
デフォルト引数は引数の最後に置く
関数の引数が複数ある場合、デフォルト引数を指定する引数は最後に配置する必要があります。
//OK
int func1(int x, int y = 0){}
//NG
int func2(int x = 0, int y){}
//OK
int func3(int x, int y = 0, int z = 0){}
//OK
int func4(int x = 0, int y = 0){}
func2
は、引数x
に続く引数y
がデフォルト引数を持っていないのでエラーとなります。
デフォルト値付きの引数の後ろにデフォルト値なしの引数がなければOKなので、引数がすべてデフォルト値付きである場合はどのような順番でも構いません。
関数のオーバーロードとデフォルト引数
デフォルト引数と同等の処理は、関数のオーバーロードを使って書くこともできます。
#include <iostream>
size_t count(const char*);
size_t count(const char*, char);
//文字列sに半角スペースが登場する数を返す
size_t count(const char* s)
{
return count(s, ' ');
}
//文字列sに文字cが登場する数を返す
size_t count(const char* s, char c)
{
size_t ret = 0;
while (*s != '\0')
{
if (*s == c)
ret++;
s++;
}
return ret;
}
int main()
{
using std::cout; using std::endl;
using std::cin;
const char* str = "This is a pen";
//引数を省略して呼び出し
cout << count(str) << endl;
//省略しないで呼び出し
cout << count(str, 'i') << endl;
cin.get();
}
オーバーロードで関数count
を二つ定義します。
処理の実体は引数を省略しない版の関数です。
引数省略版からは、省略しない版の関数に新たな引数を指定して呼び出しているだけです。
戻り値も、省略しない版から受け取った戻り値をそのまま返すだけです。
オーバーロードによる実装でもデフォルト引数による実装でも、関数呼び出し側からは同じように使用できます。
ただし、関数を関数ポインタで扱う場合には違いがあります。
関数をオーバーロードした場合は関数の実体は二つ作られます。
そのため、関数ポインタがどちらの実体を指すかによって動作に違いが生じます。
(そもそも引数が異なるので、それぞれの関数を指すためのポインタ変数の型が異なる)
#include <iostream>
size_t count(const char* s)
{/*省略*/}
size_t count(const char* s, char c)
{/*省略*/}
int main()
{
//受け取る変数の型によって指し示す関数が判別される
size_t (*countP1)(const char*) = count;
size_t (*countP2)(const char*, char) = count;
std::cout << countP1("This is a pen") << std::endl;
std::cout << countP2("This is a pen", 'i') << std::endl;
}
デフォルト引数を使用する場合は関数の実体は一つです。
しかし関数ポインタを使用する場合はデフォルト引数の情報は失われるので、実引数は明示的に指定する必要があります。
#include <iostream>
size_t count(const char* s, char c = ' ')
{/*省略*/}
int main()
{
size_t(*countP)(const char*, char) = count;
//引数が足りないのでエラー
//std::cout << countP("This is a pen") << std::endl;
std::cout << countP("This is a pen", ' ') << std::endl;
}
インライン関数
関数の実行は、関数内での処理のほかに引数や戻り値のコピーなどの処理が行われます。
こういった本来の目的(関数内で行う処理)とは関係のない処理をオーバーヘッドといいます。
つまり、ある処理を行う場合にそこに直接記述する場合と関数化して呼び出す場合とでは後者の方がわずかにコストがかかるわけです。
int max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 5, b = 7;
int m1 = a > b ? a : b;
int m2 = max(a, b); //こっちのほうが少し遅いかもしれない
}
上記コードのような簡単な処理の場合はインライン関数を使用するとコードが高速化する可能性があります。
インライン関数は関数定義の戻り値の前にinline
というキーワードを指定します。
//インライン関数
inline int max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 5, b = 7;
int m1 = a > b ? a : b;
int m2 = max(a, b);
}
関数の呼び出し側には変更点はありません。
インライン関数は、コンパイル時に可能であればその場に関数の処理を展開してしまいます。
int m2 = max(a, b);
//↓
int m2 = (a > b ? a : b);
こうすることで関数呼び出しにかかるオーバーヘッドが無くなり、処理が速くなります。
ただしinline
を付ければどのような関数でもインライン化できるわけではありません。
インライン化によって速度の向上が見込めるとコンパイラが判断した場合にインライン化されます。
反対に、inline
キーワードが指定されていない関数であってもコンパイラの判断でインライン化されることもあります。
そのため、プログラムの最適化の目的で関数をインライン化するメリットはほとんどないと言っていいでしょう。
クラスのメンバ関数をクラス定義内に記述した場合、自動的にinlineキーワードが指定されたものとして扱われます。
実際にインライン関数化されるかどうかはコンパイラ次第です。
class C
{
void f1() {} //inline
void f2(); //ただの宣言
void f3(); //ただの宣言
};
void C::f2() {} //非inline
inline void C::f3() {} //inline
関数の複数回の定義
ソースファイルを分割するとき、通常はヘッダファイル(.h/.hpp)内には関数等の宣言を記述し、定義はソースファイル(.cpp)内に記述します。
しかしテンプレートを使用する場合など、ヘッダファイル内に定義を記述したい場合があります。
(理由はテンプレートの項を参照のこと)
include
文(includeディレクティブ)の機能はコンパイル時に指定のファイルの内容をその場に展開することです。
ヘッダファイルは複数のソースファイルからインクルードされることがあり(むしろその前提)、つまりは複数のソースファイルに同じ内容が記述されることになります。
CやC++には、同じ名前の宣言は複数回しても良いが定義は複数回してはならないという単一定義規則(One Definition Rule、ODR)があります。
(C++では関数のオーバーロードが可能なので、正確には「名前」ではなく同じアドレス先のデータに対する複数回の定義が禁止されている)
ヘッダファイルに定義を書くと、複数のソースファイルからインクルードされたときに多重定義となってしまいます。
コンパイルはソースコード単位で行われるため、コンパイル自体は可能ですが、コンパイルによって生成されたオブジェクトファイルの結合(リンク)に失敗しビルドできません。
//max.h
#ifndef MYMAX_
#define MYMAX_
//関数の定義
int max(int a, int b)
{
return a >= b ? a : b;
}
#endif
//main.cpp
#include <iostream>
#include "max.h"
//↑ここにmax.hの内容が展開され、関数maxが定義される
int main()
{
std::cout << max(1, 2);
}
//test.cpp
#include "max.h"
//↑ここで関数maxの二重定義になる
どうしてもヘッダファイル内に定義を行いたい場合は、関数にinline
を指定します。
これにより、この関数は「複数回の定義が許される」という意味になります。
#ifndef MYMAX_
#define MYMAX_
//関数の定義
inline int max(int a, int b)
{
return a >= b ? a : b;
}
#endif
ただしinline
を指定した関数はプログラム全体で定義が同一である必要があります。
ひとつのヘッダファイルに定義して複数のファイルからインクルードする場合は定義が変わることはないので問題ありません。
ヘッダファイルにはインクルードガードがありますが、これで多重定義は回避できません。
インクルードガードは「ひとつのソースファイルからの多重インクルード」を防ぐためのものです。
ひとつのソースファイルから同じヘッダファイルを二回インクルードすることなどないと思うかもしれませんが、例えばあるライブラリを使用する場合、そのライブラリが使用しているヘッダファイルを把握しておかなければ、そのヘッダファイルをソースコード上で再度インクルードしてしまう可能性は大いにあります。
さらに言えば、複数のライブラリを使用する場合、それぞれのライブラリが同じヘッダファイルをインクルードしている可能性もあります。
インクルードされているヘッダファイルはコンパイル時に全てソースファイル上に展開されるのですが、このときすでに展開されているヘッダファイルを除外するのがインクルードガードの役割です。
インクルードガードは「コンパイル時」のエラーを回避するものです。
これに対して、複数のソースファイルから同じヘッダファイルをインクルードする場合、インクルードガードは機能しません。
コンパイルというのはソースファイル単位で行われ(コンパイル単位、翻訳単位という)、あるコンパイル単位は別のコンパイル単位の内容を知ることはできません。
要するに「a.cpp」ファイルは「b.cpp」ファイルがどのヘッダファイルを使用しているかを知ることができないので、同じヘッダファイルを使用していることを検出できません。
そのヘッダファイル内に「定義」があると多重定義になります。
それぞれのコンパイル単位では構文エラーがあるわけではないのでコンパイル自体は成功しますが、コンパイルによって生成されたオブジェクトファイル同士の結合時に「ひとつの関数に複数の定義が存在する」ことになり、リンクエラーとなりビルドに失敗します。
inline
が指定された関数(C++17からは変数もインライン化可能)は、定義が同一である場合に限り複数定義が許されるため、多重定義エラーを防ぐことができます。
関数定義のinline化は「リンク時」のエラーを回避するものです。
プロトタイプ宣言は必須
C言語では、関数を使用するよりも手前でその関数が定義されていればプロトタイプ宣言は必要ありませんでしたが、C++では省略してはならないとされています。
(VC++では省略できてしまいますが)
このサイトのサンプルコードは、関数のプロトタイプ宣言を省略して記述しているものがたくさんあります。
手抜きと言えば手抜きですが、コードの行数を少なくした方が見やすいのではないか、という配慮でもあります。
きちんとしたプログラムを書く場合は関数のプロトタイプ宣言は必ず書くようにしましょう。