C++における関数オーバーロードの規格を理解するためのサンプルコードをまとめる。
オーバーロード解決は七つの文脈で行われる(JTC1/SC22/WG21 N4659 16.3/p2)。本サイトはこれを関数コールとユーザー定義変換の二つに分類した。
分類 | 文脈 | N4659 |
関数コール | 名前付き関数コール | 16.3.1.1.1 |
関数オブジェクトコール | 16.3.1.1.2 |
演算子コール | 16.3.1.2 |
コンストラクタコール | 16.3.1.3 |
ユーザー定義変換 | クラス型ユーザー定義変換 | 16.3.1.4 |
非クラス型ユーザー定義変換 | 16.3.1.5 |
直接バインドユーザー定義変換 | 16.3.1.6 |
オーバーロード解決を意識するのは通常なら関数コールに属する文脈だろう。ユーザー定義変換は暗黙的変換シーケンスを構成する要素として認識するが、その文脈もオーバーロード解決されるという認識は希薄かもしれない。関数コールは実引数を仮引数型へ暗黙的変換するかもしれず、つまり関数コールのオーバーロード解決がユーザー定義変換のオーバーロード解決を伴う可能性がある。一方で暗黙的なユーザー定義変換は最大1ステップのみ許され(16.3.3.1/p4)、ユーザー定義変換が他のユーザー定義変換を伴う事は無い。
本記事は各文脈の関数オーバーロードおいて、関数候補、実行可能関数、最優関数の選択を概説し、サンプルコードを示す。本記事においてオブジェクトはC++規格定義(4.5/p1)とする。
関数コール
名前付き関数コール
後置式(式リスト)の形式をとり(N4659 16.3.1.1/p1)、後置式は名前付き関数(named function)である。名前付き関数は後置式.id式(後置式はクラス型オブジェクト)、後置式->id式(後置式はクラス型オブジェクトポインタ)、一次式のいずれかで、前者二つは修飾付き関数コール(qualified funciton call)、後者一つは修飾なし関数コール(unqualified function call)と呼ぶ(16.3.1.1.1/p1)。修飾付き関数コールは後置式に与えるクラスオブジェクトを暗黙オブジェクト仮引数とするメンバ関数を候補とする(p2)。修飾なしの場合は一次式に自由関数(メンバでない関数)あるいはメンバ関数を与えて(8.2.2/p1)候補とし、後者の場合は暗黙オブジェクト仮引数に(*this)を与える(16.3.1.1.1/p3)。
実行可能関数は全ての文脈において、与えられた実引数で実行できる関数とする(16.3.2/p1)。
最優関数の選択は以下とする(16.3.3/p1)。ICSi(F)は、i番目の実引数から関数Fのi番目の仮引数型への暗黙的変換シーケンスと定義する。二つの実行可能関数F1とF2で、F1がF2より優れるのは、全ての実引数iでICSi(F1)がICSi(F2)より劣らず、ある実引数jでICSj(F1)がICSj(F2)より優れる場合とする。メンバ関数の場合、最初の実引数/仮引数は(スタティックメンバの場合も含めて)暗黙オブジェクトとして、ICS1(F)は暗黙オブジェクト実引数から暗黙オブジェクト仮引数への変換シーケンスとする。Fがスタティックメンバ関数であればICS1(F)とICS1(G)に優劣差なしとする。この基準は全ての文脈で最優先となるが、他の文脈では別の基準が追加される場合がある。
自由関数コールのサンプルコードを示す。
struct A {int i_;};
struct B:A {};
struct C:B {};
void f(int) {}
void f(short) {}
void f(int,int) {}
void f(short,short) {}
void f(A) {}
void f(B) {}
void f(int A::*) {}
void f(int C::*) {}
void f(double&) {}
void f(double&&) {}
int main()
{
f(0);
f((short)0);
f((char)0);
f(0,(char)0);
f(0,(short)0);
f(C());
f(&C::i_);
double v=0.0;
f(v);
f(0.0);
void (*fp)(int)=f;
fp(0);
fp((short)0);
fp((char)0);
return 0;
}
メンバ関数コールのサンプルコードを示す。
void f(int) {}
void f(short) {}
class CC
{
private:
void f(short) {}
public:
void f(int) {}
void f(double) & {}
void f(double) && {}
void class_scope_call()
{
f(0);
f((short)0);
f((char)0);
f(0.0);
using ::f;
f(0);
f((short)0);
CC::f(0);
this->f(0);
}
};
int main()
{
auto cc=CC{};
cc.class_scope_call();
cc.f(0);
cc.f((char)0);
cc.f(0.0);
CC{}.f(0.0);
return 0;
}
関数オブジェクトコール
クラスオブジェクトの()演算子コールや変換関数による関数ポインタ型などのコールの文脈でも後置式(式リスト)の形式をとり(N4659 16.3.1.1/p1)、後置式はクラス型オブジェクトである。後置式がcv T型のオブジェクトEに評価されるとすれば(E).operator()を候補とする(16.3.1.1.2/p1)。さらにT(あるいは基底型)が関数ポインタ型、関数への参照型、関数ポインタへの参照型への変換関数を持ち、変換関数のcv修飾がcv Tのそれより大きい場合、変換結果の関数を代理コール関数(surrogate call function)として候補に加える(p2)。
実行可能関数と最優関数の選択は名前付き関数コールに準ずる。
struct A {int i_;};
struct B:A {};
struct C:B {};
void f(int,int) {}
void f(short,short) {}
void f1(A) {}
void f2(B) {}
void f3(int A::*) {}
void f4(int C::*) {}
struct FF
{
void operator()(int) {}
void operator()(short) {}
using fp=void(*)(int,int);operator fp() {return f;}
operator typeof(void(*)(short,short))() {return f;}
using f1p=void(*)(A);operator f1p() {return f1;}
operator decltype(f2)*() {return f2;}
operator decltype(&f3)() {return f3;}
operator auto() {return f4;}
};
int main()
{
auto ff=FF{};
ff(0);
ff((short)0);
ff((char)0);
ff(0,(char)0);
ff(C{});
ff(&C::i_);
return 0;
}
演算子コール
被演算子のいずれもがクラス型あるいは列挙型でなければ演算子は組み込み演算子(N4659 8)である(16.3.1.2/p1)。いずれかがクラス型あるいは列挙型の場合、ユーザー定義の演算子関数が使用されるかもしれず、またはクラス型なら組み込み演算子に渡す型へユーザー定義変換するかもしれない(p2)。一部の組み込み演算子は列挙型を被演算子にできるが(8.8/p1、8.9/p1、8.11/p1、8.12/p1、8.13/p1)、ユーザー定義した演算子関数に置き変わるかもしれない(16.3.1.2/p3.3.4)。演算子関数は非スタティックなメンバ関数であるか、少なくとも一つの仮引数型がクラス、クラスへの参照、列挙型、列挙型への参照である非メンバ関数である(16.5/p6)。オーバーロード解決は演算子を等価の関数コールに置き換えるが、評価順は常に組み込み演算子のそれに従う。
実行可能関数と最優関数の選択は名前付き関数コールに準ずる。
struct A {int i_;};
struct B:A {};
struct C:B {};
A operator+(A rhs) {return {+rhs.i_};}
A operator+(A lhs,A rhs) {return {lhs.i_+rhs.i_};}
A operator+(B lhs,A rhs) {return {lhs.i_+rhs.i_};}
A operator+(A lhs,C rhs) {return {lhs.i_+rhs.i_};}
A& operator++(A& rhs) {++rhs.i_;return rhs;}
A operator++(A lhs,int) {return {lhs.i_++};}
struct D:A
{
D operator+() {return {+i_};}
D operator+(A rhs) {return {i_+rhs.i_};}
D& operator=(A rhs) {i_=rhs.i_;return *this;}
D& operator[](int i) {i_=i;return *this;}
D* operator->() {return this;}
D& operator++() & {++i_;return *this;}
D operator++(int) {return {i_++};}
};
enum E {e0};
int operator+(int lhs,E rhs) {return lhs+(int)rhs;}
enum class EE {e0};
int operator+(int lhs,EE rhs) {return lhs+(int)rhs;}
int main()
{
A a01=+A();
A a02=A()+A();
A a03=A()+B();
A a04=A()+C();
A a05=B()+D();
A a06=D()+B();
A a07=A()++;
++a07;
D d01=+D();
D d02=D()+D();
d02=C();
d02=D();
D d03=D()[0];
int ixx=D()->i_;
D d04=D()++;
++d04;
int i01=0+0;
int i02=0+e0;
int i03=e0+0;
int i04=0.0+e0;
int i05=0+EE::e0;
int i07=0.0+EE::e0;
return 0;
}
コンストラクタコール
クラス(T)オブジェクトのデフォルト/直接初期化の文脈で、直接初期化、Tあるいは派生型からのコピー初期化、デフォルト初期化でコンストラクタを選択する(N4659 16.3.1.3/p1)。コピー初期化の文脈にない場合は全てのコンストラクタを候補とし、コピー初期化の場合は全ての変換コンストラクタ(15.3.1/p1)を候補とする。
実行可能関数と最優関数の選択は名前付き関数コールに準ずる。
#include <initializer_list>
struct A {int i_;};
struct B:A {};
struct C:B {};
struct X
{
X() {}
X(const X&) {}
X(int) {}
explicit X(short) {}
explicit X(int,int) {}
X(short,short) {}
X(A) {}
X(B) {}
X(int A::*) {}
X(int C::*) {}
X(const struct Z&);
X(X,struct Y,struct Z);
X(std::initializer_list<struct Y>);
};
struct Y:X
{
Y() {}
explicit Y(const Y&) {}
};
struct Z:Y {};
X::X(const Z&) {}
X::X(X,Y,Z) {}
X::X(std::initializer_list<Y>) {}
int main()
{
X x01;
X x02(0);
X x03((short)0);
X x04((char)0);
X x05(0,(char)0);
X x06(0,(short)0);
X x07(C{});
X x08(&C::i_);
X x0a(X{});
X x0b(Y{});
X x0c(Z{});
X x0d(X{},Y{},Z{});
X x11{};
X x12{0};
X x13{(short)0};
X x14{(char)0};
X x15{0,(char)0};
X x16{0,(short)0};
X x17{C()};
X x18{&C::i_};
X X1a{X()};
X x1b{Y()};
X x1d{X(),Y(),Z()};
X x1e{Y(),Y(),Y()};
X x2a=X();
X x2b=Y();
X x2c=Z();
Y y2b=Y();
return 0;
}
ユーザー定義変換
クラス型ユーザー定義変換
クラス(cv1 T)オブジェクトのコピー初期化におけるユーザー定義変換の文脈で、初期化式をユーザー定義変換する(N4659 16.3.1.4/p1)。
- Tの変換コンストラクタを候補とする。
- 初期化式がクラスcv S型であればSおよび基底型の非explicitな変換関数を考慮する。あるコンストラクタの第一仮引数がcv修飾されているかもしれないTへの参照で、そのコンストラクタをcv2 T型オブジェクト直接初期化の文脈で単一の実引数でコールして一時オブジェクトを第一仮引数にバインドする場合、explicitな変換関数も考慮する。これらの変換関数のうち、Sのスコープにあってcv修飾を除いた型がTあるいは派生型であれば候補とする。参照型を返す変換関数は参照方法に依存して左辺値あるいは期限切れ値式を返すとして、候補選択では参照を除いた型とする。
実行可能関数と最優関数の選択は名前付き関数コールに準ずるが、その結果として最優関数が複数となった場合は以下の基準を追加する。二つの実行可能関数F1とF2で、F1の戻り値型から初期化される型への標準変換シーケンスがF2のそれより優れれば、F1はF2に優れる。
#include <initializer_list>
struct A {int i_;};
struct B:A {};
struct C:B {};
struct X
{
X() {}
X(const X&) {}
X(int) {}
explicit X(short) {}
explicit X(int,int) {}
X(short,short) {}
X(A) {}
X(B) {}
X(int A::*) {}
X(int C::*) {}
X(const struct Z&);
X(X,struct Y,struct Z);
X(std::initializer_list<struct Y>);
};
struct Y:X
{
Y() {}
explicit Y(const Y&) {}
};
struct Z:Y {};
X::X(const Z&) {}
X::X(X,Y,Z) {}
X::X(std::initializer_list<Y>) {}
struct Q
{
explicit operator Y() {return Y();}
operator X() {return X();}
};
int main()
{
X x02=0;
X x03=(short)0;
X x04=(char)0;
X x07=C();
X x08=&C::i_;
X x0q=Q();
X x11={};
X x12={0};
X x14={(char)0};
X x17={C()};
X x18={&C::i_};
X X1a={X()};
X x1b={Y()};
X x1d={X(),Y(),Z()};
X x1e={Y(),Y(),Y()};
X x1q={Q()};
Y y2q(Q{});
return 0;
}
非クラス型ユーザー定義変換
クラス型(cv S)の式から非クラス型(cv1 T)を初期化する変換関数の文脈で、初期化式に変換関数を適用する(N4659 16.3.1.5/p1)。
- Sおよび基底型の変換関数を考慮する。SのスコープにあってTあるいはTへ標準変換シーケンスで変換可能な型への非explicitな変換関数を候補とする。直接初期化ならSのスコープにあってTあるいはTへ修飾変換可能な型へのexplictな変換関数も候補とする。候補選択にあたってcv修飾された型を返す変換関数はcv修飾されていない型とする。参照型を返す変換関数は参照方法に依存して左辺値式あるいは期限切れ値式を返すとして、候補選択では参照を除いた型とする。
実行可能関数と最優関数の選択は名前付き関数コールに準ずるが、その結果として最優関数が複数となった場合は以下の基準を追加する。二つの実行可能関数F1とF2で、F1の戻り値型から初期化される型への標準変換シーケンスがF2のそれより優れれば、F1はF2に優れる。
struct J
{
operator int() {return 0;}
explicit operator short() {return (short)0;}
operator void*() {return (void*)0;}
};
struct K:J
{
operator char() {return (char)0;}
explicit operator void*() {return (void*)0;}
};
int main()
{
int i01(J{});
short i02(J{});
char i03(J{});
void* i04(J{});
int i05(K{});
short i06(K{});
char i07(K{});
void* i08(K{});
int i11=J();
short i12=J();
char i13=J();
void* i14=J();
int i15=K();
char i17=K();
return 0;
}
直接バインドユーザー定義変換
汎左辺値式あるいは純右辺値式へ参照を直接バインドする変換関数の文脈で、初期化式に変換関数を適用した結果にバインドする(N4659 16.3.1.6/p1)。cv1 Tへの参照をクラスcv S型の初期化式で初期化する。
- Sおよび基底型の変換関数を考慮する。Sのスコープにあってcv2 T2への左辺値参照(左辺値参照あるいは関数への右辺値参照を初期化する場合)、あるいはcv2 T2かそれへの右辺値参照(右辺値参照あるいは関数への左辺値参照を初期化する場合)を返し、cv1 Tがcv2 T2への参照互換である非explicitな変換関数を候補とする。直接初期化ならSのスコープにあってcv2 T2への左辺値参照、cv2 T、cv2 Tへの右辺値参照を返し、T2がTと同じか修飾変換可能なexplicitな変換関数も候補とする。
実行可能関数と最優関数の選択は名前付き関数コールに準ずるが、その結果として最優関数が複数となった場合は以下の基準を追加する。二つの実行可能関数F1とF2で、F1の戻り値型から初期化される型への標準変換シーケンスがF2のそれより優れれば、F1はF2に優れる。
#include <utility>
struct A {int i_;};
struct B:A {};
struct C:B {};
struct M
{
static C c_;
operator A&() {return c_;}
operator const B&() {return c_;}
explicit operator B&&() {return std::move(c_);}
operator C() {return c_;}
};
C M::c_;
struct N:M
{
operator B&&() {return std::move(c_);}
};
int main()
{
A& a01(M{});
const A& a02(M{});
const B& b02(M{});
B&& b03(M{});
const C& c02(M{});
C&& c03(M{});
A& a04(N{});
const A& a05(N{});
A&& a06(N{});
const B& b05(N{});
B&& b06(N{});
const C& c05(N{});
C&& c06(N{});
A& a11=M();
const A& a12=M();
const B& b12=M();
const C& c12=M();
C&& c13=M();
A& a14=N();
const A& a15=N();
A&& a16=N();
const B& b15=N();
B&& b16=N();
const C& c15=N();
C&& c16=N();
return 0;
}
ユーザー定義変換を伴う関数コール
各文脈のオーバーロード解決は、その一つ一つがサイト作成者レベルには理解不能な難解さだ。なお悪い事に関数コールの実引数を仮引数型へユーザー定義変換すれば、関数コールの文脈とユーザー定義変換の文脈が交錯する。サンプルコードはこれまで利用したユーザー定義変換できるクラスを実引数として受ける関数オーバーロードで、事態がいかに複雑化するかをデモンストレーションする。その内容を理解したところで得るものも少なく、あくまでもユーザー定義変換濫用の戒めとする。
#include <utility>
struct A {int i_;};
struct B:A {};
struct C:B {};
struct X
{
X() {}
X(const X&) {}
X(int) {}
explicit X(short) {}
explicit X(int,int) {}
X(short,short) {}
X(A) {}
X(B) {}
X(int A::*) {}
X(int C::*) {}
X(const struct Z&);
X(X,struct Y,struct Z);
X(std::initializer_list<struct Y>);
};
struct Y:X
{
Y() {}
explicit Y(const Y&) {}
};
struct Z:Y {};
X::X(const Z&) {}
X::X(X,Y,Z) {}
X::X(std::initializer_list<Y>) {}
struct Q
{
explicit operator Y() {return Y();}
operator X() {return X();}
};
struct J
{
operator int() {return 0;}
explicit operator short() {return (short)0;}
operator void*() {return (void*)0;}
};
struct K:J
{
operator char() {return (char)0;}
explicit operator void*() {return (void*)0;}
};
struct M
{
static C c_;
operator A&() {return c_;}
operator const B&() {return c_;}
explicit operator B&&() {return std::move(c_);}
operator C() {return c_;}
};
C M::c_;
struct N:M
{
operator B&&() {return std::move(c_);}
};
void fff(A,B,C) {}
void fff(A,A,A) {}
void fff(X,Y,Z) {}
void fff(X,X,X) {}
void fff(int,int,int) {}
void fff(char,short,int) {}
void fff(int,B&&,C&&) {}
int main()
{
fff(A(),B(),C());
fff(B(),B(),B());
fff(C(),C(),C());
fff(X(),Y(),Z());
fff(Y(),Y(),Y());
fff(Q(),Y(),Z());
fff(Q(),Q(),Z());
fff(Q(),Q(),Q());
fff(0,0,0);
fff(J(),J(),J());
fff(K(),(short)0,K());
fff(J(),N(),N());
fff(0,{(short)0,(short)0},Z());
fff(Y(),Z(),{X(),Y(),Z()});
fff(&C::i_,{Y(),Y(),Y()},{{Y(),Y()},Y(),Z()});
fff({J()},{{J()},Y(),Z()},{Y(),Y(),Y(),Y()});
fff({J()},{{{J()},Y(),Z()},Y(),Z()},{Y(),Y(),Y(),Y(),Y()});
return 0;
}
教科書をサイト作成者訳で引用する(Herb Sutter, Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, p.164)。
暗黙的なユーザー定義変換は、それが変換関数であれ変換コンストラクタであれ、ほとんどの場合で避けるべきである。一般にそれが安全でない主な理由を示す。
- オーバーロード解決に干渉する可能性がある
- "間違った"コードを警告なしにコンパイルする可能性がある
C++11以降の非explicitなコンストラクタは全て変換コンストラクタなので、この規範に従えば全てのコンストラクタをexplicitすべしとなる。初期化節{...}からの暗黙的変換を抑止するには全くその通りなのだが、C++03以前からの旧癖は2個以上の実引数を受ける場合を見逃してしまう。