質問: とりあえず、c++11 をある程度(言語が巨大なのでほんの少し)勉強してから、その上を勉強しようかなと思っているレベルの者です。 ### 知りたいこと c++ のクラスに、論理否定演算子「!」をオーバーロードする場合はどんな場合なのでしょうか? 例えば std::basic_ios では operator!() がオーバーロードされていますが、同時に、キャスト演算子 explicit operator bool() const;もオーバーロードされています。 しかし、operator bool() があれば、たとえexplicitがついていても、否定演算子!のオペランドの場合は明示的なキャストなしにbool型に変換されます。 またbool() と operator!() の両方がクラスAでオーバーロードされている場合、a が A のインスタンスのとき、!a と !static_cast(a) は同じ動作をするようにしないと使う方が混乱します。 とすると、多くの場合、 explicit operator bool() のみがあれば大丈夫のような気がするのですが、あえて 否定演算子のオーバーロードをする積極的な理由は何なのでしょうか? たとえば、std::shared_ptr では!のオーバーロードはないようですね。 もし、論理演算子をオーバーロードする必用が在る場合でも「&&」「||」は短絡評価ができなくなるため危険ですから、普通は「!」しかできませんが。 自分でクラスを作るとき、!のオーバーロードを行うか否か?の指針が知りたいです。 ### 試したこと ``` // test-not2.cpp #include class A { public: operator int() const {return 0;} // explicit をつけると当然のようにエラー }; class B { public: explicit operator bool() const {return false;} }; class C { public: bool operator!() const { return false;} }; class D { public: explicit operator bool() const { std::cerr << "D::operator bool()" << std::endl; return false; } bool operator!() const { std::cerr << "D::operator !()" << std::endl; return false; } }; using std::cout; using std::endl; int main(){ A a; B b; C c; D d; cout << "!a: " << !a << endl; cout << "!b: " << !b << endl; cout << "!c: " << !c << endl; cout << "!d: " << !d << endl; } ``` 出力 ``` bash@test-not2$ ./a.out !a: 1 !b: 1 !c: 0 !d: D::operator !() 0 bash@test-not2$ ``` g++ (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0 では、両方オーバーロードした場合、否定演算子が優先されたようです。
回答: > > 多くの場合、 explicit operator bool() のみがあれば大丈夫のような気がするのですが、あえて 否定演算子のオーバーロードをする積極的な理由は何なのでしょうか? > > > 私も質問者myoonさん、catsforepawさんと同意見です。ごく一部の特殊なユースケースを除いて、否定演算子(`operator!`)はオーバーロードすべきでないと考えます。 あえて演算子オーバーロードの意味があるとすれば; * Boost::sprit等のC++文法を拡張したDSL(Domain-Specific Language)ライブラリを実装するとき。驚き最小の原則に従い、独自DSLであっても`!`は“論理否定演算”を表すべきと思います。 * 式テンプレート(ET; Expression Template)ライブラリを実装するとき。式`!x`を「xの論理否定演算を表す型」へと変換するため、明示的な演算子オーバーロードが必要となります。 > > 例えば std::basic_ios では operator!() がオーバーロードされていますが、同時に、キャスト演算子 explicit operator bool() const;もオーバーロードされています。 > > たとえば、std::shared_ptr では!のオーバーロードはないようですね。 > > > 端的には **歴史的な経緯** で説明できます。外部QAサイトStackOverflowに "Why does std::basic_ios overload the unary logical negation operator?" という質問・回答がありました。 * 古代のC++言語では`basic_ios::operator!`をオーバーロードする以外に、IOストリームを真偽値に変換する手段がありませんでした。つまり`if (!s)`のような否定形しか記述できなかったようです。 * C++03の頃は`basic_ios::operator void*`ユーザ変換関数が提供されており、`if (s)`, `if (!s)` のいずれの書き方もできるようになりました。 * C++11で言語仕様そのものが拡張され、 `explicit operator bool()` という表記が許されるようになりました。機能的には `operator void*`/`operator!` 両方をカバーし、かつプログラマの意図しない暗黙変換が起きるリスクも回避されます。 * 一方で `operator void*` はプログラマを混乱させる恐れがあったため、このタイミングで削除されています。 * `std::shared_ptr` はC++11で初めて標準ライブラリ入りしたため、最初から `explicit operator bool()` のみを提供すると考えられます。 > > 自分でクラスを作るとき、!のオーバーロードを行うか否か?の指針が知りたいです。 > > > `!`に限らず、演算子オーバーロードの提供有無は慎重に設計すべきです。ISO C++公式FAQ(下記)やC++ Core Guideline C.over: Overloading and overloaded operators もご参考にどうぞ。 The previous FAQs tell me which operators I can override; but which operators should I override? > > Bottom line: don’t confuse your users. > > > Remember the purpose of operator overloading: to reduce the cost and defect rate in code that uses your class. If you create operators that confuse your users (because they’re cool, because they make the code faster, because you need to prove to yourself that you can do it; doesn’t really matter why), you’ve violated the whole reason for using operator overloading in the first place. > > > (おまけ情報です) > > もし、論理演算子をオーバーロードする必用が在る場合でも「&&」「||」は短絡評価ができなくなるため危険です > > > C++11時点(かつC++14までの)仕様では認識通りです。C++17では「`&&`, `||`論理演算子オーバーロードでも短絡評価が保証される」ように言語仕様が改善されました。とはいえ、C++コンパイラがどのように振舞うかはまだ微妙ですので、質問文中の認識でいたほうが安全だと思います。