パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.7.0.5(5)
ソースコード(関数オーバーロードサンプル)

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(int)をコール、無変換(完全一致ランク)
f((short)0); // f(short)をコール、無変換(完全一致ランク)
f((char)0); // f(int)をコール、char→intは整数拡張(拡張ランク)、char→shortは整数変換(変換ランク)
// 拡張ランクは変換ランクより優れる
f(0,(char)0); // f(int,int)をコール
// f(int,int)はint→int(完全一致ランク)、char→int(拡張ランク)
// f(short,short)はint→short(変換ランク)、char→short(変換ランク)
// 完全一致ランクは変換ランクより優れ、拡張ランクは変換ランクより優れる
f(0,(short)0); // エラー、ただしmingw-w64デフォルト(-Wno-pedantic)はf(int,int)をコール
// f(int,int)はint→int(完全一致ランク)、short→int(拡張ランク)
// f(short,short)はint→short(変換ランク)、short→short(完全一致ランク)
// 完全一致ランクは変換ランクより優れ、拡張ランクは完全一致ランクより劣り、曖昧
// ただし-Wno-pedanticは拡張ランクが変換ランクより優れるとしてf(int,int)をコール
f(C()); // f(B)をコール、C→BとC→Aはともに派生から基底変換(変換ランク)だが、継承に近い方が優れる
f(&C::i_); // f(int A::*)をコール、ポインタからメンバ変換(変換ランク)、継承に遠い方が優れる
// 値カテゴリによるオーバーロード解決
double v=0.0;
f(v); // f(double&)をコール、実引数が左辺値式
f(0.0); // f(double&&)をコール、実引数が右辺値式
// 関数ポインタ型変数初期化や関数への参照型変数初期化によるオーバーロード解決(N4659 16.4/p3)
// 変数によるコールにオーバーロードは無関係
void (*fp)(int)=f; // 関数ポインタ型、オーバーロード解決はfpの型による、fは関数からポインタ変換される
fp(0); // f(int)をコール
fp((short)0); // f(int)をコール、実引数型は無関係
fp((char)0); // f(int)をコール、実引数型は無関係
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の修飾無しコールはメンバ関数
f(0); // CC::f(int)をコール
f((short)0); // CC::f(short)をコール
f((char)0); // CC::f(int)をコール
f(0.0); // CC::f(double)&をコール
// グローバル名前空間の名前fを導入、fの修飾無しコールは名前空間の関数
using ::f;
f(0); // f(int)をコール
f((short)0); // f(short)をコール
CC::f(0); // CC::f(int)をコール
this->f(0); // CC::f(int)をコール
}
};
int main()
{
auto cc=CC{};
cc.class_scope_call();
// ccのメンバ関数をコール
cc.f(0); // CC::f(int)をコール
//cc.f((short)0); // エラー、CC::f(short)はアクセス不可
cc.f((char)0); // CC::f(int)をコール
cc.f(0.0); // CC::f(double)&をコール、暗黙オブジェクト実引数が左辺値式
CC{}.f(0.0); // CC::f(double)&&をコール、暗黙オブジェクト実引数が右辺値式
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) {}
// 代理コール関数(オーバーロード関数をコール)、関数ポインタ型への変換関数
// 変換関数ターゲットの関数型は明示するが、typedefあるいはusingで型別名が必要
// GCC拡張のtypeof演算子を使えば型別名は不要
// https://gcc.gnu.org/onlinedocs/gcc/Typeof.html
using fp=void(*)(int,int);operator fp() {return f;}
operator typeof(void(*)(short,short))() {return f;}
// 関数への参照型でも同じ
//using fr=void(&)(int,int);operator fr() {return f;}
//operator typeof(void(&)(short,short))() {return f;}
// 代理コール関数(非オーバーロード関数をコール)、関数ポインタ型への変換関数
// 変換関数ターゲットの関数型は関数識別子から推定することもできる
// 型推定にautoも使えるがoperator auto()はオーバーロードできず一つしか許されない
// https://stackoverflow.com/questions/67550830/multiple-conversion-functions-as-operator-auto-in-class
// operator auto()の妥当性は規格上はっきりしない
// https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1670
using f1p=void(*)(A);operator f1p() {return f1;}
operator decltype(f2)*() {return f2;}
operator decltype(&f3)() {return f3;}
operator auto() {return f4;}
// 関数への参照型でも同じ
//using f1r=void(&)(A);operator f1r() {return f1;}
//operator decltype(f2)&() {return f2;}
//operator decltype(f3)&() {return f3;}
//operator auto&() {return f4;}
};
int main()
{
auto ff=FF{};
ff(0); // FF::operator(int)をコール
ff((short)0); // FF::operator(short)をコール
ff((char)0); // FF::operator(int)をコール
ff(0,(char)0); // f(int,int)をコール
//ff(0,(short)0); // エラー、名前付き関数コールの場合と異なりなぜか-Wno-pedanticでもエラーになる
ff(C{}); // f2(B)をコール
ff(&C::i_); // f3(int A::*)をコール
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(); // operator+(A)をコール
A a02=A()+A(); // operator+(A,A)をコール
A a03=A()+B(); // operator+(A,A)をコール
A a04=A()+C(); // operator+(A,C)をコール
A a05=B()+D(); // operator+(B,A)をコール
A a06=D()+B(); // D::operator+(A)をコール
A a07=A()++; // operator++(A,int)をコール
++a07; // operator++(A&)をコール
D d01=+D(); // D::operator+()をコール
D d02=D()+D(); // D::operator+(A)をコール
d02=C(); // D::operator=(A)をコール
d02=D(); // 暗黙宣言/定義されるD::operator=(const D&)をコール
D d03=D()[0]; // D::operator[](int)をコール
int ixx=D()->i_; // D::operator->()をコール
D d04=D()++; // D::operator++(int)をコール
++d04; // D::operator++()&をコール
int i01=0+0; // 組み込みoperator+(int,int)をコール
int i02=0+e0; // operator+(int,E)をコール
int i03=e0+0; // 組み込みoperator+(int,int)をコール、E→intは整数変換(拡張ランク)、int→Eは不可
int i04=0.0+e0; // エラー、ただしmingw-w64デフォルト(-Wno-pedantic)は組み込みoperator+(double,int)をコール
// operator+(double,int)はdouble→double(完全一致ランク)、E→int(拡張ランク)
// operator+(int,E)はdouble→int(変換ランク)、E→E(完全一致ランク)
// 完全一致ランクは変換ランクより優れ、拡張ランクは完全一致ランクより劣り、曖昧
// ただし-Wno-pedanticは拡張ランクが変換ランクより優れるとしてoperator+(double,int)をコール
int i05=0+EE::e0; // operator+(int,EE)をコール
//int i06=EE::e0+0; // エラー、EE→intは不可、int→EEも不可
int i07=0.0+EE::e0; // operator+(int,EE)をコール、EE→int不可でoperator+(double,int)は候補とならず曖昧にならない
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::X()をコール、X x01();とすると関数宣言文となってしまう(C++最厄構文)
X x02(0); // X::X(int)をコール
X x03((short)0); // X::X(short)をコール
X x04((char)0); // X::X(int)をコール
X x05(0,(char)0); // X::X(int,int)をコール
X x06(0,(short)0); // エラー、ただしmingw-w64デフォルト(-Wno-pedantic)はX::X(int,int)をコール
X x07(C{}); // X::X(B)をコール、X x07(C());はC++最厄構文
X x08(&C::i_); // X::X(int A::*)をコール
X x0a(X{}); // コピー省略でX::X(const X&)をコールしない、X x0a(X());はC++最厄構文
X x0b(Y{}); // X::X(const X&)をコール、基底から派生変換、X x0b(Y());はC++最厄構文
X x0c(Z{}); // X::X(const Z&)をコール、X x0c(Z());はC++最厄構文
X x0d(X{},Y{},Z{}); // X::X(X,Y,Z)をコール、X x0d(X(),Y(),Z());はC++最厄構文
//X x0e(Y{},Y{},Y{}); // エラー、実行可能が無い、X x0e(Y(),Y(),Y());はC++最厄構文
// 直接リスト初期化、全てのコンストラクタを候補とする
X x11{}; // X::X()をコール
X x12{0}; // X::X(int)をコール
X x13{(short)0}; // X::X(short)をコール
X x14{(char)0}; // X::X(int)をコール
X x15{0,(char)0}; // X::X(int,int)をコール
X x16{0,(short)0}; // エラー、ただしmingw-w64デフォルト(-Wno-pedantic)はX::X(int,int)をコール
X x17{C()}; // X::X(B)をコール
X x18{&C::i_}; // X::X(int A::*)をコール
X X1a{X()}; // コピー省略でX::X(const X&)をコールしない
X x1b{Y()}; // X::X(std::initializer_list<Y>)をコール、Yはコピー省略(y2b参照)
//X x1c{Z()}; // エラー、X::X(std::initializer_list<Y>)を選択、Yはコピー省略されない(y2c参照)
X x1d{X(),Y(),Z()}; // X::X(X,Y,Z)をコール
X x1e{Y(),Y(),Y()}; // X::X(std::initializer_list<Y>)をコール
// コピー初期化(実引数が同じ型か派生型)、全ての変換(非explicit)コンストラクタを候補とする
X x2a=X(); // コピー省略でX::X(const X&)をコールしない
X x2b=Y(); // X::X(const X&)をコール、派生から基底変換
X x2c=Z(); // X::X(const Z&)をコール
Y y2b=Y(); // コピー省略でY::Y(const Y&)はコールしない
// C++14以前はY::Y(const Y&)がexplicitでエラー、C++17以降はコピー省略保証でOK
//Y y2c=Z(); // エラー、派生から基底変換してコールするY::Y(const Y&)がexplicit
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()
{
// コピー初期化、全ての変換(非explicit)コンストラクタと(あれば)非explicitな変換関数を候補とする
// C++17以降ならコピー省略保証でX::X(const X&)は=deleteでもexplicitでも良い
X x02=0; // X::X(int)をコール
X x03=(short)0; // X::X(int)をコール、X::X(short)はexplicit
X x04=(char)0; // X::X(int)をコール
X x07=C(); // X::X(B)をコール
X x08=&C::i_; // X::X(int A::*)をコール
X x0q=Q(); // Q::operator X()をコール
//Y y0q=Q(); // エラー、Q::operator Y()がexplicit
// コピーリスト初期化、全てのコンストラクタと(あれば)非explicitな変換関数を候補とし、explicitを選択すれば不適格
// C++17以降ならコピー省略保証でX::X(const X&)は=deleteでも良いがexplicitはx1qのみエラーとなる
X x11={}; // X::X()をコール
X x12={0}; // X::X(int)をコール
//X x13={(short)0}; // エラー、X::X(short)を選択するがexplictで不適格
X x14={(char)0}; // X::X(int)をコール
//X x15={0,(char)0}; // エラー、X::X(int,int)を選択するがexplicitで不適格
//X x16={0,(short)0}; // エラー、mingw-w64デフォルト(-Wno-pedantic)もX::X(int,int)を選択して不適格
X x17={C()}; // X::X(B)をコール
X x18={&C::i_}; // X::X(int A::*)をコール
X X1a={X()}; // コピー省略でX::X(const X&)をコールしない
X x1b={Y()}; // X::X(std::initializer_list<Y>)をコール、Yはコピー省略
//X x1c={Z()}; // エラー、X::X(std::initializer_list<Y>)を選択、Yはコピー省略されない
X x1d={X(),Y(),Z()}; // X::X(X,Y,Z)をコール
X x1e={Y(),Y(),Y()}; // X::X(std::initializer_list<Y>)をコール
X x1q={Q()}; // Q::operator X()をコール、X::X(const X&)がexplicitだとエラー
//Y y1q={Q()}; // エラー、Q::operator Y()がexplicit
// コピー初期化がexplicitな変換関数を候補とできるケース
// Yの直接初期化でYはQからコピー初期化される一時オブジェクトに直接バインドするが、
// こういった場合のコピー初期化に限りexplicitなQ::operator Y()も候補となる
// https://cplusplus.github.io/CWG/issues/899.html
// https://cplusplus.github.io/CWG/issues/1087.html
Y y2q(Q{}); // Q::operator Y()をコール、エラーとならない、Y y2q(Q());はC++最厄構文
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()
{
// 直接初期化、全ての変換関数を候補とする、初期化式J()またはK()とするとC++最厄構文
int i01(J{}); // J::operator int()をコール
short i02(J{}); // J::operator short()をコール
char i03(J{}); // J::operator int()をコール、char→int(拡張ランク)はchar→short(変換ランク)より優れる
void* i04(J{}); // J::operator void*()をコール
int i05(K{}); // J::operator int()をコール
short i06(K{}); // J::operator short()をコール
char i07(K{}); // K::operator char()をコール
void* i08(K{}); // K::operator void*()をコール、J::operator void*()は隠蔽されてKのスコープにない
// コピー初期化、全ての非explicitな変換関数を候補とする
int i11=J(); // J::operator int()をコール
short i12=J(); // J::operator int()をコール、J::operator short()はexplicit
char i13=J(); // J::operator int()をコール
void* i14=J(); // J::operator void*()をコール
int i15=K(); // J::operator int()をコール
//short i16=K(); // エラー、K::operator short()はexplicit、J::operator int()とK::operator char()が曖昧
char i17=K(); // K::operator char()をコール
//void* i18=K(); // エラー、K::operator void*()はexplicit、J::operator void*()は隠蔽されて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> // std::move requires.
struct A {int i_;};
struct B:A {};
struct C:B {};
struct M
{
static C c_; // staticとしないと一時オブジェクトからの変換が不適格
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()
{
// 直接初期化、全ての変換関数を候補とする、初期化式M()またはN()とするとC++最厄構文
A& a01(M{}); // M::operator A&()をコール
const A& a02(M{}); // M::operator A&()をコール、A&→const A&は可
//A&& a03(M{}); // mingw-w64はエラー、しかしM::operator B&&()をコールするべきでは?
//B& b01(M{}); // エラー、const B&→B&は不可
const B& b02(M{}); // M::operator const B&()をコール
B&& b03(M{}); // M::operator B&&()をコール
//C& c01(M{}); // エラー、非const左辺値参照に一時オブジェクトはバインドできない
const C& c02(M{}); // M::operator C()をコール、一時オブジェクトにバインド
C&& c03(M{}); // M::operator C()をコール、一時オブジェクトにバインド
A& a04(N{}); // M::operator A&()をコール
const A& a05(N{}); // M::operator A&()をコール、A&→const A&は可
A&& a06(N{}); // N::operator B&&()をコール、M::operator B&&()は隠蔽されてNのスコープにない
//B& b04(N{}); // エラー、const B&→B&は不可
const B& b05(N{}); // M::operator const B&()をコール
B&& b06(N{}); // N::operator B&&()をコール、M::operator B&&()は隠蔽されてNのスコープにない
//C& c05(N{}); // エラー
const C& c05(N{}); // M::operator C()をコール
C&& c06(N{}); // M::operator C()をコール
// コピー初期化、全ての非explicitな変換関数を候補とする
A& a11=M(); // M::operator A&()をコール
const A& a12=M(); // M::operator A&()をコール
//A&& a13=M(); // エラー、M::operator B&&()がexplicit
//B& b11=M(); // エラー、const B&→B&は不可
const B& b12=M(); // M::operator const B&()をコール
//B&& b13=M(); // エラー、M::operator B&&()がexplicit
//C& c11=M(); // エラー、非const左辺値参照に一時オブジェクトはバインドできない
const C& c12=M(); // M::operator C()をコール
C&& c13=M(); // M::operator C()をコール
A& a14=N(); // M::operator A&()をコール
const A& a15=N(); // M::operator A&()をコール
A&& a16=N(); // N::operator B&&()をコール、M::operator B&&()は隠蔽されてNのスコープにない
//B& b14=N(); // エラー
const B& b15=N(); // M::operator const B&()をコール
B&& b16=N(); // N::operator B&&()をコール、M::operator B&&()は隠蔽されてNのスコープにない
//C& c14=N(); // エラー
const C& c15=N(); // M::operator C()をコール
C&& c16=N(); // M::operator C()をコール
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_; // staticとしないと一時オブジェクトからの変換が不適格
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(A,B,C)をコール
fff(B(),B(),B()); // fff(A,A,A)をコール
fff(C(),C(),C()); // fff(A,B,C)をコール
fff(X(),Y(),Z()); // fff(X,Y,Z)をコール
fff(Y(),Y(),Y()); // fff(X,X,X)をコール
//fff(Z(),Z(),Z()); // エラー
fff(Q(),Y(),Z()); // fff(X,Y,Z)をコール
fff(Q(),Q(),Z()); // fff(X,X,X)をコール
fff(Q(),Q(),Q()); // fff(X,X,X)をコール
fff(0,0,0); // fff(int,int,int)をコール
fff(J(),J(),J()); // fff(int,int,int)をコール
//fff(J(),(short)0,J()); // エラー
//fff(K(),K(),K()); // エラー
fff(K(),(short)0,K()); // fff(char,short,int)をコール
//fff(J(),M(),M()); // エラー
fff(J(),N(),N()); // fff(int,B&&,C&&)をコール
// 以下は全てfff(X,X,X)をコール
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個以上の実引数を受ける場合を見逃してしまう。