質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

4回答

4119閲覧

c++ のクラスで否定演算子!を積極的にオーバーロードする場合はどんな時なのでしょうか?

myoon

総合スコア100

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

4グッド

3クリップ

投稿2018/12/27 19:05

編集2018/12/27 19:41

とりあえず、c++11 をある程度(言語が巨大なのでほんの少し)勉強してから、その上を勉強しようかなと思っているレベルの者です。

知りたいこと

c++ のクラスに、論理否定演算子「!」をオーバーロードする場合はどんな場合なのでしょうか?
例えば std::basic_ios では operator!() がオーバーロードされていますが、同時に、キャスト演算子 explicit operator bool() const;もオーバーロードされています。

しかし、operator bool() があれば、たとえexplicitがついていても、否定演算子!のオペランドの場合は明示的なキャストなしにbool型に変換されます。
またbool() と operator!() の両方がクラスAでオーバーロードされている場合、a が A のインスタンスのとき、!a と !static_cast<bool>(a) は同じ動作をするようにしないと使う方が混乱します。
とすると、多くの場合、 explicit operator bool() のみがあれば大丈夫のような気がするのですが、あえて 否定演算子のオーバーロードをする積極的な理由は何なのでしょうか?
たとえば、std::shared_ptr では!のオーバーロードはないようですね。
もし、論理演算子をオーバーロードする必用が在る場合でも「&&」「||」は短絡評価ができなくなるため危険ですから、普通は「!」しかできませんが。

自分でクラスを作るとき、!のオーバーロードを行うか否か?の指針が知りたいです。

試したこと

c++

1// test-not2.cpp 2#include <iostream> 3 4class A { 5public: 6 operator int() const {return 0;} // explicit をつけると当然のようにエラー 7}; 8 9class B { 10public: 11 explicit operator bool() const {return false;} 12}; 13 14class C { 15public: 16 bool operator!() const { return false;} 17}; 18 19class D { 20public: 21 explicit operator bool() const { 22 std::cerr << "D::operator bool()" << std::endl; 23 return false; 24 } 25 bool operator!() const { 26 std::cerr << "D::operator !()" << std::endl; 27 return false; 28 } 29}; 30 31 32using std::cout; 33using std::endl; 34 35int main(){ 36 A a; 37 B b; 38 C c; 39 D d; 40 41 cout << "!a: " << !a << endl; 42 cout << "!b: " << !b << endl; 43 cout << "!c: " << !c << endl; 44 cout << "!d: " << !d << endl; 45} 46

出力

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
では、両方オーバーロードした場合、否定演算子が優先されたようです。

yuba, yohhoy, atata0319, KSwordOfHaste👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

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++コンパイラがどのように振舞うかはまだ微妙ですので、質問文中の認識でいたほうが安全だと思います。

追記:&&, ||論理演算子オーバーロードではオペランドの短絡評価は行われません。例えばa && ba.operator&&(b)またはoperator&&(a, b)と評価されるため、短絡評価は技術的に実現不可能でした。

投稿2018/12/28 04:08

編集2021/12/23 08:22
yohhoy

総合スコア6189

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

myoon

2018/12/30 10:21 編集

非常に丁寧で分かりやすい説明ありがとうございました。 DSLや式テンプレートが(詳細は置いといて、目的として)何をやろうとしているのかつかむのに時間がかかりました。 確かに、このようなことをやろうとしたら、!も使う時はオーバーロードする必用がありますね。 このような使い方をする場合、ADLが余計な仕事をしてしまって、プログラマーが想定しなかった動きをしないか?という心配が出てきました。ADLの動きを完全に把握していないので、これに関して分からないことが出てきたら別質問にいたします。 std::basic_iosは歴史的理由なのだろうか?と考えていましたが、やはりそうなのですね。 自作のクラスで演算子のオーバーロードが必用な場合としては、代入演算子や、イテレータを自作する場合などがありますね。 C++17では式の評価順序が一部定められたということは知っていましたが短絡評価にも対応していたのですね。ええと、使ったことはないですがvararrayでは以前から「&&」「||」のオーバーロードは行われている(短絡評価は効かないとの前提の上で)ようですから、整合性はどうなっているのでしょうか。特殊用途に演算子を使う場合もこの問題がどう解決されているか知る必用はありますね。 とりあえず、疑問の答えは分かりましたが、学ばなければならないことが増えてしまいました。 ともあれ、ありがとうございます。
myoon

2018/12/29 09:47

「やはり!のオーバーロードは必用だ」という意見が出るかもしれないので、しばらく受付中にしてから閉じようと思います
yohhoy

2018/12/29 10:24 編集

> 使ったことはないですがvararrayでは以前から「&&」「||」のオーバーロードは行われている(短絡評価は効かないとの前提の上で)ようですから、整合性はどうなっているのでしょうか。 こちらのコメントで初めて気づいたのですが std::valarray<T> は &&, || をオーバーロードしていたんですね。valarrayの場合はT型のコンテナ要素単位で &&, || が処理されるだけですから、短絡評価はもともと問題にならないですね。(T型への要件は「値のように振る舞う型」となっています。)
myoon

2018/12/30 10:17 編集

式テンプレートでも「&&」を使いたければ、構文木作って評価のときに短絡評価すればいいですね。 ありがとうございました。
myoon

2018/12/30 10:29

コメント内であまりにも質問の内容とかけ離れた部分は削除しました。
myoon

2018/12/30 10:48

皆様の回答すべてが私の役に立ちましたが、一番ぴったりして、かつ詳細な回答をいただきましたので、ベストアンサーとさせていただきました。ありがとうございます。
guest

0

しかし、operator bool() があれば、たとえexplicitがついていても、否定演算子!のオペランドの場合は明示的なキャストなしにbool型に変換されます。

私もoperator boolをオーバーロードした場合は、あえてoperator !をオーバーロードすることはしないですね。質問者さん同様、そうしなければならない理由が思いつきません。

ただ、

例えば std::basic_ios では operator!() がオーバーロードされていますが

これに関しては、C++11より前のbasic_iosはexplicit operator boolではなくoperator void*がオーバーライドされていました(返されるnull以外のポインター値には意味がない!)。ポインター値でも暗黙の変換が行われて!演算子が使えますが、直感的に判りづらいので「!演算子も使えますよ」という意味でoperator !もオーバーライドしたのではないでしょうか。それがC++11でoperator boolのオーバーライドに変更された後も残されているだけのような気がします。

投稿2018/12/28 02:24

catsforepaw

総合スコア5938

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

myoon

2018/12/29 09:56

丁寧な回答、ありがとうございます。 std::basic_ios について、以前は operator void* へオーバーロードされていたとは聞いていましたが、c++03より前のc++では、!を付けなければならなかったという他の方の回答がありましたので、やはり歴史的理由のようですね。 オーバーロードする必用がないものをわざわざオーバーロードするのはやはりよくないと思います。 特殊目的で行う場合はあるとしても。 ありがとうございました。
guest

0

Boost::spirit(C++のオーバーロードを駆使して、構文解析器を作る)のような、通常の演算でなく前衛的(あるいは変態的)にオーバーロードを「活用」する場合には、!も1つの演算子として動員することがあります(リファレンス)。

投稿2018/12/27 22:31

maisumakun

総合スコア145123

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

myoon

2018/12/28 10:41

boostまで手を伸ばすとハマってしまう危険を感じて控えてましたが、boostもやはり通らなければならない道のようです。
myoon

2018/12/28 11:25

前衛的(変態的)な使い方をする場合、ADLが余計な仕事をしてしまう危険がありますね。ADL firewall 等の概念があるようなので、必用になったらここで質問するまでもなくググれば分かると思いますgあ。
guest

0

確かに、bool, bit, 整数では!を自分で作る価値はなさそうです。
しかし、複素数の直交ベクタを!として定義することはあるのでは?

x = a + bi ; // i*i = -1 -x => -a - bi ; !x => b - ai ;

投稿2018/12/27 21:54

gm300

総合スコア580

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

myoon

2018/12/28 10:39

回答ありがとうございます。 もちろん、私の意図はbool型の場合ですが、おっしゃるような場合は定義が必用ですね。これはとてもわかり易い例でした。ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問