web-dev-qa-db-ja.com

異なるコンパイラによって呼び出される異なるキャスト演算子

次の短いC++プログラムについて考えてみます。

_#include <iostream>

class B {
public:
    operator bool() const {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}
_

異なるコンパイラでコンパイルすると、さまざまな結果が得られます。 Clang3.4とGCC4.4.7では、trueを出力しますが、Visual Studio2013はfalseを出力します。つまり、_(bool)b_で異なるキャスト演算子を呼び出します。規格による正しい動作はどれですか?

私の理解では、operator bool()は変換を必要としませんが、operator int()intからboolへの変換を必要とするため、コンパイラーは最初のものを選択する必要があります。 constはそれで何かをしますか、const-conversionはコンパイラによってより「高価」であると見なされますか?

constを削除すると、すべてのコンパイラが等しくfalseを出力として生成します。一方、2つのクラスを組み合わせると(両方の演算子は同じクラスになります)、3つのコンパイラーすべてがtrue出力を生成します。

79
buc

標準の状態:

派生クラスの変換関数は、2つの関数が同じ型に変換されない限り、基本クラスの変換関数を非表示にしません。

§12.3[class.conv]

つまり、_operator bool_は_operator int_によって隠されていません。

標準の状態:

過負荷の解決中、暗黙のオブジェクト引数は他の引数と区別できません。

§13.3.3.1[over.match.funcs]

この場合の「暗黙のオブジェクト引数」はbで、タイプは_B2 &_です。 _operator bool_には_const B2 &_が必要であるため、コンパイラーは_operator bool_を呼び出すためにbにconstを追加する必要があります。これ(他のすべてが等しい)は、_operator int_をより適切に一致させます。

標準では、次の場合に_static_cast_(Cスタイルのキャストがこのインスタンスで実行している)をタイプT(この場合はint)に変換できると規定されています。

宣言T t(e);は、いくつかの発明された一時変数tに対して、整形式です。

§5.2.9[expr.static.cast]

したがって、intboolに変換でき、boolも同様にboolに変換できます。

標準の状態:

Sとその基本クラスの変換関数が考慮されます。 S内に隠されておらず、タイプTまたは標準の変換シーケンスを介してタイプTに変換できるタイプ)を生成する非明示的な変換関数は候補関数です。

§13.3.1.5[over.match.conv]

したがって、オーバーロードセットは_operator int_と_operator bool_で構成されます。他のすべての条件が同じであれば、_operator int_の方が適しています(定数を追加する必要がないため)。したがって、_operator int_を選択する必要があります。

(おそらく直感に反して)標準は、(上記で確立された)オーバーロードセットに追加された戻り値の型(つまり、これらの演算子が変換する型)を考慮しないことに注意してください。ただし、次のいずれかの引数の変換シーケンスが提供されます。それらは、他の引数の変換シーケンスよりも優れています(これは、一貫性があるため、この場合に当てはまります)。

標準の状態:

これらの定義を前提として、すべての引数iについて、ICSi(F1)がICSi(F2)よりも悪い変換シーケンスではない場合、実行可能な関数F1は別の実行可能な関数F2よりも優れた関数であると定義されます。

  • 一部の引数jの場合、ICSj(F1)はICSj(F2)よりも優れた変換シーケンスです。そうでない場合は、
  • コンテキストはユーザー定義の変換による初期化であり、F1の戻り値の型から宛先の型(つまり、初期化されるエンティティの型)への標準変換シーケンスは、戻り値の型からの標準変換シーケンスよりも優れた変換シーケンスです。 F2の宛先タイプへの変換。

§13.3.3[over.match.best]

この場合、引数は1つだけです(暗黙のthisパラメーター)。 _B2 &_ => _B2 &_(_operator int_を呼び出す)の変換シーケンスは、_B2 &_ => _const B2 &_(_operator bool_を呼び出す)よりも優れています。したがって、_operator int_は、実際にはboolに直接変換されないという事実に関係なく、オーバーロードセットから選択されます。

ショート

変換関数operator int()は、operator bool() constを介してclangによって選択されます。これは、bが定数修飾されていないのに対し、boolの変換演算子は。

簡単な理由は、bboolに変換するときの、過負荷解決の候補関数(暗黙のオブジェクトパラメーターが設定されている)は

_operator bool (B2 const &);
operator int (B2 &);
_

ここで、bはconst修飾されていないため、2番目の方がより適切に一致します。

両方の関数が同じ資格を共有している場合(両方ともconstかどうか)、直接変換を提供するため、_operator bool_が選択されます。

キャスト表記による変換、段階的に分析

ブール値のostreamインサーター([ostream.inserters.arithmetic]によるstd :: basic_ostream :: operator <<(bool val))が、bからへの変換の結果の値で呼び出されることに同意する場合boolその変換を掘り下げることができます。

1.キャスト表現

ブール値へのbのキャスト

_(bool)b
_

に評価します

_static_cast<bool>(b)
_

C++ 11、5.4/4 [expr.cast]によると、_const_cast_は適用されないため(ここでconstを追加または削除しない)。

この静的変換は、C++ 11、5.2.9/4 [expr.static.cast]ごとに許可されます。ただし、発明された変数tのbool t(b);が整形式。このようなステートメントは、C++ 11、8.5/15 [dcl.init]に従って直接初期化と呼ばれます。

2.直接初期化bool t(b);

最も言及されていない標準段落の条項16は次のように述べています(私の強調):

イニシャライザのセマンティクスは次のとおりです。宛先タイプは初期化されるオブジェクトまたは参照のタイプであり、ソースタイプは初期化式のタイプです。

[...]

[...] ソースタイプが(おそらくcv修飾)の場合クラスタイプ、変換関数が考慮されます。

該当する変換関数が列挙され、過負荷の解決によって最適なものが選択されます。

2.1どの変換機能が利用できますか?

C++ 11、12.3/5 [class.conv]からわかるように、使用可能な変換関数はoperator int ()operator bool() constです。

派生クラスの変換関数は、2つの関数が同じ型に変換されない限り、基本クラスの変換関数を非表示にしません。

C++ 11、13.3.1.5/1 [over.match.conv]は次のように述べています。

Sとその基本クラスの変換関数が考慮されます。

ここで、Sは変換元のクラスです。

2.2どの変換機能が適用できますか?

C++ 11、13.3.1.5/1 [over.match.conv](私の強調):

1[...]「cv1T」が初期化されるオブジェクトの型であり、「cv S」が初期化式の型であり、Sがクラスであると仮定します。タイプの場合、候補関数は次のように選択されます。Sとその基本クラスの変換関数が考慮されます。 S内に隠されておらず、タイプT または標準の変換シーケンスを介してタイプTに変換できるタイプを生成する非明示的な変換関数は候補関数です。

したがって、operator bool () constは_B2_内に隠されておらず、boolを生成するため、適用可能です。

intは標準の変換シーケンスを介してブール値に変換できる型であるため、最後の標準引用符で強調されている部分は、operator int ()を使用した変換に関連しています。 intからboolへの変換はシーケンスではなく、C++ 11、4.12/1 [conv.bool]で許可されている単純な直接変換です。

算術、スコープなしの列挙、ポインター、またはメンバー型へのポインターのprvalueは、bool型のprvalueに変換できます。ゼロ値、ヌルポインター値、またはヌルメンバーポインター値はfalseに変換されます。その他の値はすべてtrueに変換されます。

これは、operator int ()も適用可能であることを意味します。

2.3どの変換機能が選択されていますか?

適切な変換関数の選択は、過負荷解決(C++ 11、13.3.1.5/1 [over.match.conv])を介して実行されます。

過負荷解決は、呼び出す変換関数を選択するために使用されます。

クラスメンバー関数のオーバーロード解決に関しては、特別な「癖」が1つあります。それは、暗黙的なオブジェクトパラメータです。

C++ 11、13.3.1 [over.match.funcs]ごとに、

[...]静的および非静的メンバー関数の両方に暗黙のオブジェクトパラメーターがあります[...]

ここで、非静的メンバー関数のこのパラメーターのタイプは、第4節によると次のとおりです。

  • ref-qualifierなしまたは&ref-qualifierありで宣言された関数の「cvXへの左辺値参照」

  • && ref-qualifierで宣言された関数の「cvXへの右辺値参照」

ここで、Xは関数がメンバーであるクラスであり、cvはメンバー関数宣言のcv修飾です。

これは、C++ 11、13.3.1.5/2 [over.match.conv])によって、変換関数による初期化で、

引数リストには、初期化式である1つの引数があります。 [注:この引数は、変換関数の暗黙的なオブジェクトパラメーターと比較されます。 —エンドノート]

過負荷解決の候補関数は次のとおりです。

_operator bool (B2 const &);
operator int (B2 &);
_

明らかに、operator int ()は修飾変換を必要としたため、タイプ_B2_の非定数オブジェクトを使用して変換が要求された場合は、operator bool ()の方が適しています。

両方の変換関数が同じconst資格を共有している場合、それらの関数の過負荷解決はもはやトリックを行いません。この場合、変換(シーケンス)ランキングが行われます。

3.両方の変換関数が同じconst資格を共有しているのに、なぜoperator bool ()が選択されるのですか?

_B2_からboolへの変換は、ユーザー定義の変換シーケンスです(C++ 11、13.3.3.1.2/1 [over.ics.user]

ユーザー定義の変換シーケンスは、最初の標準変換シーケンス、ユーザー定義の変換、2番目の標準変換シーケンスで構成されます。

[...]ユーザー定義の変換が変換関数によって指定されている場合、最初の標準変換シーケンスは、ソースタイプを変換関数の暗黙的なオブジェクトパラメーターに変換します。

C++ 11、13.3.3.2/3 [over.ics.rank]

[...]は、より良い変換シーケンスとより良い変換の関係に基づいて、暗黙的な変換シーケンスの半順序を定義します。

[...]ユーザー定義の変換シーケンスU1は、同じユーザー定義の変換関数またはコンストラクターまたは集計の初期化を含み、U1の2番目の標準変換シーケンスがより優れている場合、別のユーザー定義の変換シーケンスU2よりも優れた変換シーケンスです。 U2の2番目の標準変換シーケンス。

2番目の標準変換はoperator bool()の場合はboolからbool(ID変換)ですが、operator int ()の場合の2番目の標準変換はintからboolへのブール変換です。

したがって、operator bool ()を使用した変換シーケンスは、両方の変換関数が同じconst資格を共有している場合に適しています。

9
Pixelchemist

C++のbool型には、trueとfalseの2つの値があり、対応する値は1と0です。基本クラス(B)のbool演算子を明示的に呼び出すB2クラスにbool演算子を追加すると、固有の混乱を回避できます。 falseとして。これが私の変更したプログラムです。その場合、演算子boolは演算子boolを意味し、決して演算子intではありません。

#include <iostream>

class B {
public:
    operator bool() const {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
    operator bool() {
        return B::operator bool();
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

あなたの例では、(bool)bがB2のbool演算子を呼び出そうとしており、B2はbool演算子を継承しており、int演算子は、支配規則により、int演算子が呼び出され、B2で継承されたbool演算子が呼び出されます。ただし、B2クラス自体にbool演算子を明示的に含めることで、問題は解決されます。

1

以前の回答のいくつかは、すでに多くの情報を提供しています。

私の貢献は、「キャスト操作」は「オーバーロード操作」と同様にコンパイルされることです。操作ごとに一意の識別子を持つ関数を作成し、後で必要な演算子またはキャストに置き換えることをお勧めします。

#include <iostream>

class B {
public:
    bool ToBool() const {
        return false;
    }
};

class B2 : public B {
public:
    int ToInt() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << b.ToBool() << std::endl;
}

そして、後で、演算子またはキャストを適用します。

#include <iostream>

class B {
public:
    operator bool() {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

ちょうど私の2セント。

0
umlcat