関数のオーバーロード
C++ では、同じスコープ内で同じ名前の複数の関数を指定できます。 これらの関数は、 オーバーロードされた 関数または オーバーロードと呼ばれます。 オーバーロードされた関数を使用すると、関数の型と引数の数に応じて、関数に異なるセマンティクスを指定できます。
たとえば、引数を print
受け取る関数があると std::string
します。 この関数は、 型 double
の引数を受け取る関数とは大きく異なるタスクを実行する場合があります。 オーバーロードにより、 や print_double
などのprint_string
名前を使用する必要ができなくなります。 コンパイル時に、コンパイラは、呼び出し元によって渡される引数の型と数に基づいて、使用するオーバーロードを選択します。 を呼び出 print(42.0)
すと、 void print(double d)
関数が呼び出されます。 を呼び出 print("hello world")
すと、 void print(std::string)
オーバーロードが呼び出されます。
メンバー関数とフリー関数の両方をオーバーロードできます。 次の表は、関数宣言 C++ のどの部分を使用して、同じスコープ内の同じ名前の関数のグループを区別するかを示しています。
オーバーロードに関する考慮事項
関数宣言要素 | オーバーロードに使用されますか? |
---|---|
関数の戻り値の型 | いいえ |
引数の数 | はい |
引数の型 | はい |
省略記号の有無 | はい |
typedef 名の使用 |
いいえ |
未指定の配列の範囲 | いいえ |
const または volatile |
はい (関数全体に適用される場合) |
参照修飾子 (& と && ) |
はい |
例
次の例は、関数オーバーロードを使用する方法を示しています。
// function_overloading.cpp
// compile with: /EHsc
#include <iostream>
#include <math.h>
#include <string>
// Prototype three print functions.
int print(std::string s); // Print a string.
int print(double dvalue); // Print a double.
int print(double dvalue, int prec); // Print a double with a
// given precision.
using namespace std;
int main(int argc, char *argv[])
{
const double d = 893094.2987;
if (argc < 2)
{
// These calls to print invoke print( char *s ).
print("This program requires one argument.");
print("The argument specifies the number of");
print("digits precision for the second number");
print("printed.");
exit(0);
}
// Invoke print( double dvalue ).
print(d);
// Invoke print( double dvalue, int prec ).
print(d, atoi(argv[1]));
}
// Print a string.
int print(string s)
{
cout << s << endl;
return cout.good();
}
// Print a double in default precision.
int print(double dvalue)
{
cout << dvalue << endl;
return cout.good();
}
// Print a double in specified precision.
// Positive numbers for precision indicate how many digits
// precision after the decimal point to show. Negative
// numbers for precision indicate where to round the number
// to the left of the decimal point.
int print(double dvalue, int prec)
{
// Use table-lookup for rounding/truncation.
static const double rgPow10[] = {
10E-7, 10E-6, 10E-5, 10E-4, 10E-3, 10E-2, 10E-1,
10E0, 10E1, 10E2, 10E3, 10E4, 10E5, 10E6 };
const int iPowZero = 6;
// If precision out of range, just print the number.
if (prec < -6 || prec > 7)
{
return print(dvalue);
}
// Scale, truncate, then rescale.
dvalue = floor(dvalue / rgPow10[iPowZero - prec]) *
rgPow10[iPowZero - prec];
cout << dvalue << endl;
return cout.good();
}
上記のコードは、ファイル スコープ内の 関数のオーバーロードを print
示しています。
既定の引数は、関数型の一部とは見なされません。 したがって、オーバーロードされた関数の選択では使用されません。 既定の引数のみが異なる 2 つの関数は、オーバーロードされた関数ではなく複数の定義と見なされます。
既定の引数は、オーバーロードされた演算子に指定することはできません。
引数の一致
コンパイラは、現在のスコープ内の関数宣言と関数呼び出しで指定された引数との最適な一致に基づいて、呼び出すオーバーロードされた関数を選択します。 適切な関数がある場合、その関数が呼び出されます。 このコンテキストでの "適切" は、次のいずれかを意味します。
厳密な一致が見つかりませんでした。
単純変換が実行されました。
整数の上位変換が実行されました。
目的の引数の型への標準変換が存在します。
必要な引数型へのユーザー定義変換 (変換演算子またはコンストラクター) が存在します。
省略記号によって表される引数が見つかりませんでした。
コンパイラは、引数ごとに候補関数のセットを作成します。 候補関数は、その位置の実引数を仮引数の型に変換できる関数です。
一連の "最も一致する関数" は各引数にビルドされ、選択した関数はすべての設定の交差部分です。 交差部分に複数の関数が含まれている場合、オーバーロードはあいまいで、エラーが生成されます。 最終的に選択される関数は、少なくとも 1 つの引数について、グループ内の他のすべての関数よりも常に一致します。 明確な勝者がない場合、関数呼び出しによってコンパイラ エラーが生成されます。
次の宣言を考えます (関数には、以下の説明での識別のために Variant 1
、Variant 2
、および Variant 3
とマーク付けしています)。
Fraction &Add( Fraction &f, long l ); // Variant 1
Fraction &Add( long l, Fraction &f ); // Variant 2
Fraction &Add( Fraction &f, Fraction &f ); // Variant 3
Fraction F1, F2;
次のステートメントを考えます。
F1 = Add( F2, 23 );
前のステートメントは 2 つのセットをビルドします。
セット 1: 型の最初の引数を持つ候補関数 Fraction |
セット 2: 2 番目の引数を型に変換できる候補関数 int |
---|---|
バリアント 1 | バリアント 1 (int は、標準変換を使用して long に変換することができます) |
バリアント 3 |
セット 2 の関数は、実際のパラメーター型から仮パラメーター型への暗黙的な変換を持つ関数です。 これらの関数の 1 つに、実際のパラメーター型を対応する仮パラメーター型に変換するための最小の "コスト" があります。
これら 2 つのセットの積集合は、バリアント 1 です。 あいまいな関数呼び出しの例は次のとおりです。
F1 = Add( 3, 6 );
前の関数呼び出しは次のセットをビルドします。
セット 1: int 型の第 1 引数を持つ候補関数 |
セット 2: int 型の第 2 引数を持つ候補関数 |
---|---|
バリアント 2 (int は、標準変換を使用して long に変換することができます) |
バリアント 1 (int は、標準変換を使用して long に変換することができます) |
これらの 2 つのセットの積集合が空であるため、コンパイラはエラー メッセージを生成します。
引数の一致においては、n 個の既定の引数を持つ関数は、それぞれ異なる数の引数を持つ n+1 個の個別の関数として扱われます。
省略記号 (...
) はワイルドカードとして機能し、実際の引数と一致します。 オーバーロード関数のセットを十分に注意して設計しないと、多くのあいまいさの原因になる可能性があります。
Note
オーバーロードされた関数のあいまいさは、関数呼び出しが出現するまで判断できません。 その時点で、設定は関数呼び出しの引数ごとに構築され、明確なオーバーロードがあるかどうかを確認できます。 つまり、特定の関数呼び出しによって呼び出されるまで、あいまいさがコード内に残る可能性があります。
引数の型の違い
オーバーロード関数では、異なる初期化子を受け取る引数型が区別されます。 したがって、指定された型の引数とその型への参照は、オーバーロードの目的では同一であると見なされます。 これらは同じ初期化子を受け取るため、同じと見なされます。 たとえば、max( double, double )
は max( double &, double & )
と同じであると見なされます。 このような関数を 2 つ宣言すると、エラーが発生します。
同じ理由から、 または volatile
によってconst
変更された型の関数引数は、オーバーロードの目的で基本型と異なる方法で処理されません。
ただし、関数のオーバーロード メカニズムでは、const
と volatile
で修飾される参照と基本型への参照を区別できます。 そのため、次のようなコードが可能となります。
// argument_type_differences.cpp
// compile with: /EHsc /W3
// C4521 expected
#include <iostream>
using namespace std;
class Over {
public:
Over() { cout << "Over default constructor\n"; }
Over( Over &o ) { cout << "Over&\n"; }
Over( const Over &co ) { cout << "const Over&\n"; }
Over( volatile Over &vo ) { cout << "volatile Over&\n"; }
};
int main() {
Over o1; // Calls default constructor.
Over o2( o1 ); // Calls Over( Over& ).
const Over o3; // Calls default constructor.
Over o4( o3 ); // Calls Over( const Over& ).
volatile Over o5; // Calls default constructor.
Over o6( o5 ); // Calls Over( volatile Over& ).
}
出力
Over default constructor
Over&
Over default constructor
const Over&
Over default constructor
volatile Over&
const
オブジェクトおよび volatile
オブジェクトへのポインターもまた、オーバーロードの目的では基本型へのポインターとは異なるものと見なされます。
引数の一致と変換
コンパイラは、関数宣言の引数と実際の引数を照合するとき、厳密な一致が見つからない場合は、正しい型を取得するために標準またはユーザー定義の変換を指定できます。 変換は、次の規則に従って行われます。
複数のユーザー定義変換を含む変換のシーケンスは考慮されません。
中間変換を削除することによって短縮できる変換のシーケンスは考慮されません。
変換の結果のシーケンス (ある場合) は、 最適な一致シーケンスと呼ばれます。 標準変換を使用して型のオブジェクトを型int
unsigned long
に変換するには、いくつかの方法があります (「標準変換」で説明されています)。
int
からlong
に変換してから、long
からunsigned long
に変換します。int
からunsigned long
に変換します。
最初のシーケンスは目的の目標を達成しますが、短いシーケンスが存在するため、最適な一致シーケンスではありません。
次の表は、簡易変換と呼ばれる 変換のグループを示しています。 単純な変換では、コンパイラが最適な一致として選択するシーケンスに対する影響は限定的です。 単純な変換の効果は、テーブルの後に記述されます。
単純な変換
引数の型 | 変換された型 |
---|---|
type-name |
type-name& |
type-name& |
type-name |
type-name[] |
type-name* |
type-name(argument-list) |
(*type-name)(argument-list) |
type-name |
const type-name |
type-name |
volatile type-name |
type-name* |
const type-name* |
type-name* |
volatile type-name* |
変換が試行されたシーケンスは次のとおりです。
完全一致。 関数の呼び出しに使用された型と関数プロトタイプで宣言された型の完全な一致は、常に最適な一致です。 単純変換のシーケンスは、完全一致として分類されます。 ただし、次の変換を実行しないシーケンスは、実行するシーケンスよりも優先順位が高いと見なされます。
ポインターから、
const
へのポインターへ (type-name*
からconst type-name*
)。ポインターから、
volatile
へのポインターへ (type-name*
からvolatile type-name*
)。参照から、
const
への参照へ (type-name&
からconst type-name&
)。参照から、
volatile
への参照へ (type-name&
からvolatile type&
)。
上位変換を使用した一致。 整数の上位変換、
float
からdouble
への変換、および単純変換だけを含む、完全一致として分類されないシーケンスは、上位変換を使用した一致として分類されます。 完全一致による一致ほどではありませんが、標準変換を使用するより上位変換を使用する方が一致の程度が高くなります。標準変換を使用した一致。 完全一致としても、標準変換および単純変換だけを含む上位変換を使用した一致としても分類されないシーケンスは、標準変換を使用した一致として分類されます。 このカテゴリ内では、次の規則が適用されます。
派生クラスへのポインターから直接または間接基底クラスへのポインターへの変換は、
void *
またはconst void *
への変換よりも適しています。派生クラスへのポインターから基底クラスへのポインターに変換すると、基底クラスが直接基底クラスに近ければ近いほど、一致の程度が向上します。 クラス階層が次の図に示すようになっているとします。
推奨される変換を示すグラフ。
D*
型から C*
型への変換は、D*
型から B*
型への変換よりも適しています。 同様に、D*
型から B*
型への変換は、D*
型から A*
型への変換よりも適しています。
この同じ規則が、参照変換に適用されます。 D&
型から C&
型への変換は、D&
型から B&
型への変換などよりも適しています。
この同じ規則は、ポインターからメンバーへの変換にも適用されます。 T D::*
型から T C::*
型への変換は、T D::*
型から T B::*
型への変換などよりも適しています (T
はメンバーの型)。
上記の規則は、派生の特定のパスに沿ってのみ適用されます。 次の図に示すグラフについて考えます。
優先される変換を示す多重継承グラフ。
C*
型から B*
型への変換は、C*
型から A*
型への変換よりも適しています。 理由は、これらが同じパスにあり、B*
の方が近いからです。 しかし、C*
型から D*
型への変換が、A*
型への変換より適しているというわけではありません。これらの変換は違うパスをたどるので、優先順位が決まらないからです。
ユーザー定義変換を使用した一致。 これは、完全一致、上位変換を使用した一致、または標準変換を使用した一致のどれにも分類できないシーケンスです。 ユーザー定義変換との一致として分類するには、シーケンスにユーザー定義変換、標準変換、または単純な変換のみを含める必要があります。 ユーザー定義の変換との一致は、省略記号 (
...
) を持つ一致よりも一致が良いと見なされますが、標準変換との一致ほど良い一致とは見なされません。省略記号を使用した一致。 宣言内の省略記号に一致するシーケンスは、すべて省略記号による一致として分類されます。 これは最も弱い一致と見なされます。
組み込みの上位変換または変換が存在しない場合、ユーザー定義の変換が適用されます。 これらの変換は、一致する引数の型に基づいて選択されます。 次のコードについて考えてみます。
// argument_matching1.cpp
class UDC
{
public:
operator int()
{
return 0;
}
operator long();
};
void Print( int i )
{
};
UDC udc;
int main()
{
Print( udc );
}
UDC
クラスに使用できるユーザー定義変換は、int
型および long
型からの変換です。 したがって、コンパイラは照合対象のオブジェクトの型、UDC
に応じて変換を検討します。 への int
変換が存在し、選択されています。
引数の照合処理中は、引数とユーザー定義変換の結果の両方に標準変換を適用できます。 したがって、次のコードは機能します。
void LogToFile( long l );
...
UDC udc;
LogToFile( udc );
この例では、コンパイラは ユーザー定義変換 を呼び出して 型 operator long
に変換 udc
します long
。 型long
へのユーザー定義変換が定義されていない場合、コンパイラは最初にユーザー定義operator int
変換を使用して型UDC
を型int
に変換します。 次に、宣言の引数と一致するように、型から型int
long
への標準変換を適用します。
引数の一致にユーザー定義変換が必要な場合、最適な一致を評価するときに標準変換は使用されません。 複数の候補関数がユーザー定義変換を必要としている場合でも、それらの候補関数は等しいと判断されます。 次に例を示します。
// argument_matching2.cpp
// C2668 expected
class UDC1
{
public:
UDC1( int ); // User-defined conversion from int.
};
class UDC2
{
public:
UDC2( long ); // User-defined conversion from long.
};
void Func( UDC1 );
void Func( UDC2 );
int main()
{
Func( 1 );
}
Func
のどちらのバージョンにも、int
型をクラス型の引数に変換するユーザー定義変換が必要です。 発生しうる変換は、次のとおりです。
int
型からUDC1
型への変換 (ユーザー定義変換)。int
型からlong
型への変換後、UDC2
型への変換 (2 段階の変換)。
2 番目の変換には標準変換とユーザー定義変換の両方が必要ですが、これら 2 つの変換は同じと見なされます。
Note
ユーザー定義の変換は、構築による変換または初期化による変換と見なされます。 コンパイラは、最適な一致を判断するときに、両方のメソッドが等しいと見なします。
引数の一致と this
ポインター
クラス メンバー関数は、 として static
宣言されているかどうかに応じて、異なる方法で処理されます。 static
関数にはポインターを提供 this
する暗黙的な引数がないため、通常のメンバー関数よりも 1 つ少ない引数があると見なされます。 それ以外の場合は、同じように宣言されます。
関数が呼び出されているオブジェクト型に一致する暗黙的this
なポインターを必要としないstatic
メンバー関数。 または、オーバーロードされた演算子の場合、演算子が適用されるオブジェクトに一致する最初の引数が必要です。 オーバーロードされた演算子の詳細については、「 オーバーロードされた演算子」を参照してください。
オーバーロードされた関数の他の引数とは異なり、コンパイラは一時オブジェクトを導入せず、ポインター引数と一致しようとしたときに変換を this
試行しません。
->
メンバー選択演算子を使用してクラス class_name
のメンバー関数にアクセスする場合、this
ポインター引数は class_name * const
の型を持ちます。 メンバーが const
または volatile
として宣言されている場合、型はそれぞれ const class_name * const
と volatile class_name * const
になります。
.
メンバー選択演算子は、暗黙の &
(アドレス) 演算子がオブジェクト名の前に付けられている場合を除き、まったく同じ方法で動作します 次の例では、このしくみを説明します。
// Expression encountered in code
obj.name
// How the compiler treats it
(&obj)->name
引数マッチングに関して、->*
演算子および .*
(メンバーへのポインター) 演算子の左オペランドは .
演算子および ->
(メンバー選択) 演算子と同じ方法で処理されます。
メンバー関数の参照修飾子
参照修飾子を使用すると、指 this
すオブジェクトが右辺値か左辺値かに基づいてメンバー関数をオーバーロードできます。 この機能を使用して、データへのポインター アクセスを提供しないことを選択するシナリオで不要なコピー操作を回避します。 たとえば、クラス C
でコンストラクターの一部のデータを初期化し、そのデータのコピーをメンバー関数 get_data()
で返すとします。 型 C
のオブジェクトが破棄されようとしている右辺値の場合、コンパイラはオーバーロードを get_data() &&
選択します。これは、データをコピーする代わりに移動します。
#include <iostream>
#include <vector>
using namespace std;
class C
{
public:
C() {/*expensive initialization*/}
vector<unsigned> get_data() &
{
cout << "lvalue\n";
return _data;
}
vector<unsigned> get_data() &&
{
cout << "rvalue\n";
return std::move(_data);
}
private:
vector<unsigned> _data;
};
int main()
{
C c;
auto v = c.get_data(); // get a copy. prints "lvalue".
auto v2 = C().get_data(); // get the original. prints "rvalue"
return 0;
}
オーバーロードに関する制限事項
許容可能なオーバーロード関数のセットには複数の制限が適用されます。
一連のオーバーロードされた関数の 2 つの関数には、異なる引数リストが必要です。
戻り値の型だけに基づいて、同じ型の引数リストを持つ関数をオーバーロードすることはエラーです。
Microsoft 固有の仕様
指定されたメモリ モデル修飾子に基づいて、戻り値の型に基づいてオーバーロード
operator new
できます。Microsoft 固有の仕様はここまで
メンバー関数をオーバーロードすることはできません。一方が
static
であり、もう一方が であるためstatic
です。typedef
宣言では新しい型は定義されません。既存の型のシノニムが導入されています。 オーバーロード メカニズムには影響しません。 次のコードについて考えてみます。typedef char * PSTR; void Print( char *szToPrint ); void Print( PSTR szToPrint );
先行する 2 つの関数の引数リストはまったく同じです。
PSTR
は、char *
型のシノニムです。 メンバーのスコープでは、このコードによりエラーが発生します。列挙型は別個の型です。また、オーバーロードされた関数を識別するために使用できます。
型 "array of" と "pointer to" は、オーバーロードされた関数を区別する目的では同一と見なされますが、1 次元配列に対してのみ使用されます。 これらのオーバーロードされた関数が競合し、エラー メッセージが生成されます。
void Print( char *szToPrint ); void Print( char szToPrint[] );
次元配列が大きい場合、2 番目以降の次元は型の一部と見なされます。 これらは、オーバーロードされた関数を区別するために使用されます。
void Print( char szToPrint[] ); void Print( char szToPrint[][7] ); void Print( char szToPrint[][9][42] );
オーバーロード、オーバーライド、隠ぺい
同じスコープ内の同じ名前の 2 つの関数宣言は、同じ関数、または 2 つの個別のオーバーロードされた関数を参照できます。 宣言の引数リストに (前のセクションで説明したように) 型が同じ引数が含まれている場合、関数宣言は同じ関数を参照しています。 それ以外の場合は、オーバーロードを使用して選択された 2 つの異なる関数を参照します。
クラススコープは厳密に観察されます。 基底クラスで宣言された関数は、派生クラスで宣言された関数と同じスコープ内にありません。 派生クラスの関数が基底クラスの関数と同じ名前 virtual
で宣言されている場合、派生クラス関数は基底クラス関数を オーバーライド します。 詳細については、「仮想関数」を参照してください。
基底クラス関数が として virtual
宣言されていない場合、派生クラス関数はそれを 非表示 にすると言われます。 オーバーライドと隠ぺいはどちらもオーバーロードとは異なります。
ブロック スコープは厳密に観察されます。 ファイル スコープで宣言された関数は、ローカルで宣言された関数と同じスコープ内にありません。 ローカルで宣言された関数の名前がファイル スコープで宣言された関数と同じ場合、ローカルで宣言された関数では、オーバーロードが発生する代わりに、ファイル スコープ関数を非表示にします。 次に例を示します。
// declaration_matching1.cpp
// compile with: /EHsc
#include <iostream>
using namespace std;
void func( int i )
{
cout << "Called file-scoped func : " << i << endl;
}
void func( char *sz )
{
cout << "Called locally declared func : " << sz << endl;
}
int main()
{
// Declare func local to main.
extern void func( char *sz );
func( 3 ); // C2664 Error. func( int ) is hidden.
func( "s" );
}
前のコードは、関数 func
からの 2 つの定義を示します。 char *
型の引数を受け取る定義は、extern
ステートメントのため、main
に対してローカルです。 したがって、型 int
の引数を受け取る定義は隠ぺいされ、func
の最初の呼び出しはエラーになります。
オーバーロードされたメンバー関数の場合は、関数の各バージョンにそれぞれ異なるアクセス権限を与えることができます。 これらは、外側のクラスのスコープ内にあると見なされるため、オーバーロードされた関数です。 Deposit
メンバー関数をオーバーロードしている次のコードを考えます。1 つのバージョンはパブリック、もう 1 つのバージョンはプライベートです。
このサンプルの目的は、預金を実行するために正しいパスワードが必要となる Account
クラスを提供することです。 これは、オーバーロードを使用して行われます。
Account::Deposit
呼び出し内の Deposit
への呼び出しでは、プライベート メンバー関数が呼び出されます。 Account::Deposit
はメンバー関数であり、クラスのプライベート メンバーにアクセスできるため、この呼び出しは適切です。
// declaration_matching2.cpp
class Account
{
public:
Account()
{
}
double Deposit( double dAmount, char *szPassword );
private:
double Deposit( double dAmount )
{
return 0.0;
}
int Validate( char *szPassword )
{
return 0;
}
};
int main()
{
// Allocate a new object of type Account.
Account *pAcct = new Account;
// Deposit $57.22. Error: calls a private function.
// pAcct->Deposit( 57.22 );
// Deposit $57.22 and supply a password. OK: calls a
// public function.
pAcct->Deposit( 52.77, "pswd" );
}
double Account::Deposit( double dAmount, char *szPassword )
{
if ( Validate( szPassword ) )
return Deposit( dAmount );
else
return 0.0;
}