質問内容
「C++の演算子のオーバーロードは悪いのところ」はどこですか?
質問の背景
なんで、そもそもC++の演算子のオーバーロード悪いと思っているかというと、
以下のサイトで、
これを見て「あっ、C++の演算子オーバーロードだ、殺せ!」となったキミ、ちょっと待ってほしい。実はね...
http://d.hatena.ne.jp/xef/20130309/p1
という記述を前に見たことがあったからです。
笑い混じりに「殺せ」と使っている様子をみると、C++演算子の演算子のオーバーロードに多少問題があるのだろうなと思いました。
問題になるケースをC++のコードで、書いていただけるとありがたいです。
(追記)ベストアンサーについて
(追記)2017/03/21 11:08
書き込んでくれた皆様、
書き込もうとしてやっぱりやめってしまった方、
この質問について真剣に考えてくださった方、
ありがとうございます
ベストアンサーをどうしたらいいか困っています
誰か一人がというより、様々な方々の議論で、答えが作られていると思います
。なにか方法があればお願いしたいです。
(追記)結論:悪なのか?
あくまでも、個人的な結論ですが、そこまで悪ということはなさそうだと思いました。
質問をする前は、もっと明らかな問題点が回答していただけるのかと思いましたが、そこまで決定的な悪があるようには思いませんでした。
(追記)独断と偏見でご回答まとめ
(追記) 2017/03/21 11:59
まとめる理由
回答とたくさんのコメントがあるので、
このページを初見した方が話を把握しづらいと思いますので、
個人的にまとめます。
グローバル空間の汚染は悪か?
グローバル空間を汚すことは行儀が悪いコードと言われます
これに対して、yumetodoさんが必ずしもそうではないと教えていただきました
じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆
とも教えていただき、説得力のある記事など詳しいことはyumetodoさんのご回答にかかれています。
比較演算子についてもありましたが、演算子のオーバーロード自体の問題ではないのかなぁと思い、悪だという決定打にはなりませんでした。
問題のあるコードのご提示と改善
raccyさんはC++演算子のオーバーロードでいかが問題となるものを
具体的なコードでご提示していただきました。
- グローバルの汚染がある
- 足し算の定義が一箇所にならない
様々な議論の末、Chironianさんが上記の問題を解決するコードを提示していただきました。
実際にこのページ内に動作するコードが掲載されています。
詳しくは、raccyさんのご回答の先頭のコード(内容が変更されるかもしれません)
または、こちらのChironianさんが作ってくださったリンクをご確認ください。
http://melpon.org/wandbox/permlink/PvZWTA2JXg1emLCQ
(厳密にはChironianさんのリンクのコードとraccyさんとの回答内にあるコードは少し異なります)
C++標準が良くないものを全面に出している?
(見出しの表現が適切じゃないかもです)
Zuishinさんのご回答とコメントでなるほどと思いました。
std::cout << ...
について、
この演算子はビットシフトに使うものであって、入出力に使うものではないはずです。
これを説得付ける資料として、Microsoftが以下のように言っているようです。
(詳しくはZuishinさんのご回答内にリンクなどがあります)
shift を使用してストリームに書き込みを行うことは適切ではありません。
C++の演算子のオーバーロードの問題点は、
Zuishinさんのコメントを引用すると、
繰り返しになりますが、C++ の場合は標準ライブラリが見本として奇怪なオーバーロードを掲げていて、それを手本にした奇怪なオーバーロードが責められることなく大手を振っていました。
ということです。これはすごく納得しました。
「できるのとやるは違う」ということを教えていただきました。
確かにC++標準が作り出すコーディングスタイルが問題というのは、とても問題だと感じました。なぜなら、C++プログラマはその演算子のオーバーロードを使う機会が増えますし、そのスタイルに影響を受けるからです(ここは自分の意見です)。
Googleのコーディングスタイル
catsforepawさんが、ご提供してくださいました
この記事は、この質問の内にあるブログ記事の年に合わせて、古いものを提供してくださってます。
ここでは、
特殊な状況を除き、演算子をオーバーロードしてはいけません。
とだけ書いてあり、演算子のオーバーロードに対して良くないと感じを受けます。
最新のものは、Chironianさんがご提供してくださり、
念のため最新版を見ると、160度程見解が変わっているようです。
と言われているように、だいぶ変わっています。
yumetodoさんの言葉を借りれば、
別に嫌わずにかわいがってあげようよ、と思う、今日このごろです。
ということでしょうか(英語ですし、まだちゃんと読んでません...)。
https://google.github.io/styleguide/cppguide.html#Operator_Overloading
この質問に、
考えてくださった方、
ご回答してくださった方、
回答しようとしたが、やめてしまった方(結構ありますよね笑)、
皆様、ありがとうございます。
追記 - ベストアンサーについて
2017/03/24 12時24分
ベストアンサーについて:予想を超え、様々な人でこの回答作りあげている質問になりました。そのため、とてもベストアンサーが選びづらかったです。ですが、teratailから催促メールも来ますし、投票の一番多い、faithandbraveさんにベストアンサーを決定しました。活発な議論がとても楽しかったです。皆様ありがとうございました!
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/19 11:11
2017/03/19 11:19
2017/03/21 03:50 編集
2017/03/24 03:23
回答10件
0
ベストアンサー
オーバーロードは本来、意味論が同じものをまとめるためにあります。そういう理由で、operator<<
をビットシフト以外の意味で使用するのを忌避するのは理解できます。
operator<<
を採用したのは、『C++の設計と進化』の「8.3.1 ストリームI/Oライブラリ」に記載されているように、UNIXのI/Oリダイレクト演算子にあやかったものです。
しかし、より技術的な理由は、型安全な可変引数がなかったことによるものでしょう。C++11段階であれば可変引数テンプレート、constexpr、リテラル演算子がありますので、operator<<
に頼ることのない、より使いやすいI/Oのライブラリを設計できます。つまり、現在のI/O標準ライブラリについては、単に設計が古いです。具体的には、printf
はフォーマットが文字列になっていてそれがコンパイル時に解決できないことが問題ですので、フォーマット文字列をコンパイル時に検証し、かつパラメータ数が可変個受け取れればいい、というものです。
演算子オーバーロードの設計思想は、組み込み型と同じことをユーザー定義型でもできるようにすることです。そのため、組み込み型でできないカスタムの演算子というのは定義できないようになっています(HaskellやScalaと違って)。基本的にはこれに従い、そのときの言語機能で、その枠からはみ出ざるを得ない状況でのみ独自の名前空間内で独自の意味を持たせることになるでしょう。
演算子オーバーロードは、a.Add(b)
をa + b
のように変換し、「冗長」という問題を解決するためにも使用できます。既存の意味から外れる演算子オーバーロードは、主な目的は冗長さ問題の解決にあります。
演算子オーバーロードの制限や不利な点は、以下のようなところです:
- 規定の個数を超えるオペランドを扱えない
たとえば、配列においてar[i]
はできますが、行列型に対してm[x, y]
のような指定はできません
たとえば、浮動小数点数の等値比較においてa == b
という2つのパラメータしか扱えないため、誤差をどこまで許容するか、のようなパラメータは指定できません - ADLに依存してしまう
- 短絡評価をハンドリングできない (
operator||
、operator&&
) - 組み込み型の演算子と意味論に制限されてしまう
C++以外の分野(たとえば数学)で広く採用されている演算子があり、それを採用できない
例として、累乗を扱う**
演算子や、論理学で「PならばQ」を表す→
演算子、ディレクトリ区切りの/
、区間を表す[a, b)
のようなカッコなど、やりたいことは無数にあります
演算子に独自の意味を持たせるためのルール付けが何かしらあれば、特定分野のコードがより書きやすくなるでしょう
##参照
投稿2017/03/21 08:08
編集2017/03/21 08:13総合スコア132
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/21 14:19
2017/03/21 14:37 編集
2017/03/21 14:49
2017/03/21 14:56
0
私の大嫌いな C++ のオーバーロードがあります。
C++
1std::cout << "Hello World!";
この演算子はビットシフトに使うものであって、入出力に使うものではないはずです。
C# の話になりますが、マイクロソフトが言ってくれました。
演算子のオーバーロード使用方法のガイドライン
演算の結果がすぐに明確にわかる場合に、演算子のオーバーロードを使用します。たとえば、ある Time 値を別の Time 値から引いて TimeSpan を取得できるようにすることには意味があります。しかし、or 演算子を使用して 2 つのデータベース クエリの和集合を作成したり、shift を使用してストリームに書き込みを行うことは適切ではありません。
投稿2017/03/19 12:34
総合スコア28662
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/19 12:50
2017/03/19 12:54
2017/03/19 12:57
2017/03/19 13:02
2017/03/19 13:07
2017/03/19 13:12
2017/03/19 13:18
2017/03/19 13:35
2017/03/19 13:48
2017/03/21 00:23
2017/03/21 17:22 編集
2017/03/21 18:08 編集
0
Chironianさんのコメントからの結論
C++
1#include <iostream> 2class ComplexInt 3{ 4private: 5 int real; 6 int imag; 7 8public: 9 ComplexInt(int real, int imag = 0) : real(real), imag(imag) {} 10 friend ComplexInt operator+(const ComplexInt &x, const ComplexInt &y) 11 { 12 return ComplexInt(x.real + y.real, x.imag + y.imag); 13 } 14 friend std::ostream &operator<<(std::ostream &os, const ComplexInt &ci) 15 { 16 os << ci.real; 17 if (ci.imag >= 0) { 18 os << "+"; 19 } 20 os << ci.imag << "i"; 21 return os; 22 } 23}; 24 25 26int main() 27{ 28 const ComplexInt a(2, 3); 29 const ComplexInt b(1, -5); 30 std::cout << a << std::endl; 31 std::cout << b << std::endl; 32 std::cout << (a + b) << std::endl; 33 std::cout << (a + 1) << std::endl; 34 std::cout << (1 + a) << std::endl; 35 return 0; 36}
何も問題なし、以上。
以下、駄文。
C++がだめな理由を考えました。
サンプルとして、整数しか扱えなくて足し算しかできない複素数のクラスを作ってみました。
C++
1#include <iostream> 2class ComplexInt 3{ 4private: 5 int real; 6 int imag; 7 8public: 9 ComplexInt(int real, int imag) : real(real), imag(imag) {} 10 ComplexInt operator+(const ComplexInt &other) const 11 { 12 return ComplexInt(real + other.real, imag + other.imag); 13 } 14 ComplexInt operator+(int other) const 15 { 16 return ComplexInt(real + other, imag); 17 } 18 std::string toString() const 19 { 20 std::string str(""); 21 str += std::to_string(real); 22 if (imag >= 0) { 23 str += "+"; 24 } 25 str += std::to_string(imag) += "i"; 26 return str; 27 } 28 int getReal() const { return real; } 29 int getImag() const { return imag; } 30}; 31 32std::ostream &operator<<(std::ostream &os, const ComplexInt &ci) 33{ 34 os << ci.toString(); 35 return os; 36} 37ComplexInt operator+(int x, const ComplexInt &y) 38{ 39 return ComplexInt(x, 0) + y; 40} 41 42int main() 43{ 44 const ComplexInt a(2, 3); 45 const ComplexInt b(1, -5); 46 std::cout << a << std::endl; 47 std::cout << b << std::endl; 48 std::cout << (a + b) << std::endl; 49 std::cout << (a + 1) << std::endl; 50 std::cout << (1 + a) << std::endl; 51 return 0; 52}
Scalaは演算子がメソッドだ!と言っていますが、C++も演算子はメソッド相当であるメンバー関数として定義できます。その部分に違いはありません。しかし、メンバー関数として定義した場合は、1 + a
のような整数が前に来るような足し算には対応できません。そのため、ComplexInt operator+(int x, const ComplexInt &y)
というグローバル関数を別途定義する必要が出てきます。
グローバル空間を汚すことは行儀が悪いコードと言われます(つまり、Cだと関数一個も作れないと言うこと)。しかし、C++の演算子オーバーロードではグローバル関数として定義せざるをえない場合があります※。
※ 名前空間は汚す必要ない!って話なので、名前空間を汚さないの後から追記しています。
対して、Scalaでは左辺も含めた暗黙の型変換を定義できるため、次のようになります。
Scala
1class ComplexInt(val real: Int, val imag: Int) { 2 override def toString = 3 real + (if (imag >= 0) "+" else "") + imag + "i" 4 def +(other: ComplexInt) = 5 new ComplexInt(real + other.real, imag + other.imag) 6} 7 8object ComplexInt { 9 implicit def intToCemplexInt(i: Int) = new ComplexInt(i, 0) 10} 11 12object Main { 13 def main(args: Array[String]) { 14 val a = new ComplexInt(2, 3) 15 val b = new ComplexInt(1, -5) 16 println(a) 17 println(b) 18 println(a + b) 19 println(a + 1) 20 println(1 + a) 21 } 22}
+
の定義が統一できるし、型変換も一つの型に一つのメソッドだけです。が、暗黙の型変換はオススメしない機能らしく警告が出ます。そこで、次のように書き換えます。
Scala
1class ComplexInt(val real: Int, val imag: Int) { 2 override def toString = 3 real + (if (imag >= 0) "+" else "") + imag + "i" 4 def +(other: ComplexInt) = 5 new ComplexInt(real + other.real, imag + other.imag) 6} 7 8object ComplexInt { 9 implicit class ComplexIntReal(real: Int) extends ComplexInt(real, 0) {} 10} 11 12object Main { 13 def main(args: Array[String]) { 14 val a = new ComplexInt(2, 3) 15 val b = new ComplexInt(1, -5) 16 println(a) 17 println(b) 18 println(a + b) 19 println(a + 1) 20 println(1 + a) 21 } 22}
ぱっと見、C++とは違ってグローバル関数を使ってないですので、グローバルを汚すと言うことはしていないように思えます。しかし、暗黙の型変換も暗黙のクラスもグローバルに影響を与えています。**どっちにしろグローバルを汚します。**ただ、足し算という処理は一つにまとめることができますので、C++よりはマシであると言えるのではないでしょうか。
おまけで、Rubyを見てみましょう。
Ruby
1# frozen_string_literal: true 2class ComplexInt 3 attr_reader :real, :imag 4 def initialize(real, imag) 5 @real = real.to_i 6 @imag = imag.to_i 7 end 8 9 def to_s 10 "#{@real}#{@imag >= 0 ? '+' : ''}#{@imag}i" 11 end 12 13 def coerce(other) 14 if other.is_a?(Integer) 15 [ComplexInt.new(other, 0), self] 16 else 17 super 18 end 19 end 20 21 def +(other) 22 case other 23 when ComplexInt 24 ComplexInt.new(@real + other.real, @imag + other.imag) 25 when Integer 26 ComplexInt.new(@real + other, @imag) 27 else 28 x, y = a.coerce(self) 29 x + y 30 end 31 end 32end 33 34if $0 == __FILE__ 35 a = ComplexInt.new(2, 3) 36 b = ComplexInt.new(1, -5) 37 puts a 38 puts b 39 puts a + b 40 puts a + 1 41 puts 1 + a 42end
RubyにはNumeric#coerceという仕組みがあります。これは、自分が知らない型なら相手のcoerceを呼び出して型をあわせようとする動作です。Numeric、つまり、数値をあらわす型のみですが、この仕組みにより、任意の数値のような型を追加することができます。実際にBigDecimal(任意精度十進数小数点数)やMatrix(行列)ではこの仕組みを使って、通常のInteger(整数)と足し算などができるようになっています。
この仕組みの利点は、グローバルを汚さないということです。他の型と動作が変わったわけではありません。しかし、欠点として、もともとの定義がcoerceを考慮していなければなりません。実質使えるのは数値のみです。
名前空間を汚さなくても大丈夫だそうです。
C++
1#include <iostream> 2namespace ci { 3 class ComplexInt 4 { 5 private: 6 int real; 7 int imag; 8 9 public: 10 ComplexInt(int real, int imag) : real(real), imag(imag) {} 11 ComplexInt operator+(const ComplexInt &other) const 12 { 13 return ComplexInt(real + other.real, imag + other.imag); 14 } 15 ComplexInt operator+(int other) const 16 { 17 return ComplexInt(real + other, imag); 18 } 19 std::string toString() const 20 { 21 std::string str(""); 22 str += std::to_string(real); 23 if (imag >= 0) { 24 str += "+"; 25 } 26 str += std::to_string(imag) += "i"; 27 return str; 28 } 29 int getReal() const { return real; } 30 int getImag() const { return imag; } 31 }; 32 33 std::ostream &operator<<(std::ostream &os, const ComplexInt &ci) 34 { 35 os << ci.toString(); 36 return os; 37 } 38 ComplexInt operator+(int x, const ComplexInt &y) 39 { 40 return ComplexInt(x, 0) + y; 41 } 42} 43 44int main() 45{ 46 const ci::ComplexInt a(2, 3); 47 const ci::ComplexInt b(1, -5); 48 std::cout << a << std::endl; 49 std::cout << b << std::endl; 50 std::cout << (a + b) << std::endl; 51 std::cout << (a + 1) << std::endl; 52 std::cout << (1 + a) << std::endl; 53 return 0; 54}
では、これで一安心だねっていうかと思うと、ちょっと使っただけで、グローバルに動作を影響を与えています。あ、Scalaと暗黙の型変換を検索するところと検索の仕方だけは同じ所に戻っただけのようです。
ぶっちゃけ、C++とScalaでは、単にScalaは暗黙の型変換とか使えば、実装が少なくて済むので楽だよね、程度しか無いと思います。ただ、C++の方が細かく実装できる分、コストがー、コストがー、と言っている深淵を覗くことが大好きな人達には好まれるでしょう。
C++で何か嫌だと思う点の一つは、メンバー関数として実装できないことだと思います※。オブジェクト指向原理主義者からみると、メソッドでない奴は駄目な奴だと勘違いしているのかも知れません。Scalaの暗黙の型変換もさほど変わらないと思うのですが、objectのメソッドだからいいとか言い出すのでしょうか…。
※ friendsだとできるそうです。追記しています。
あと、たぶん、何か嫌だと思ってしまうのは、標準型に新たなメンバー関数を追加しているように見えると言うことではないでしょうか。じゃあ、ScalaやRubyはどうなのかというと、確かに、元々の標準型に何かを追加している訳ではありませんが、一歩離れれば、やっていることはさほどかわりありません。やり方が違うだけで目くじら立てるのはどうなのかとは思ってしまいます。
最後にHaskellを見てみましょう。Haskellのa + b
は(+)(a)(b)
です。他の関数と同じようにオーバーロードできます。オブジェクト指向なんかにして、無理にメソッドとかメンバー関数とか、そんなことをするより、全部関数にしておけば良かったのかも知れません。
君はC++が得意なフレンズなんだね。
C++
1#include <iostream> 2class ComplexInt 3{ 4private: 5 int real; 6 int imag; 7 8public: 9 ComplexInt(int real, int imag) : real(real), imag(imag) {} 10 ComplexInt operator+(const ComplexInt &other) const 11 { 12 return ComplexInt(real + other.real, imag + other.imag); 13 } 14 ComplexInt operator+(int other) const 15 { 16 return ComplexInt(real + other, imag); 17 } 18 std::string toString() const 19 { 20 std::string str(""); 21 str += std::to_string(real); 22 if (imag >= 0) { 23 str += "+"; 24 } 25 str += std::to_string(imag) += "i"; 26 return str; 27 } 28 int getReal() const { return real; } 29 int getImag() const { return imag; } 30 friend std::ostream &operator<<(std::ostream &os, const ComplexInt &ci) 31 { 32 os << ci.toString(); 33 return os; 34 } 35 friend ComplexInt operator+(int x, const ComplexInt &y) 36 { 37 return ComplexInt(x, 0) + y; 38 } 39}; 40 41 42int main() 43{ 44 const ComplexInt a(2, 3); 45 const ComplexInt b(1, -5); 46 std::cout << a << std::endl; 47 std::cout << b << std::endl; 48 std::cout << (a + b) << std::endl; 49 std::cout << (a + 1) << std::endl; 50 std::cout << (1 + a) << std::endl; 51 return 0; 52}
メンバー関数にできたけど、これこれで、ありなんだそうです。C++書くのつかれた。
投稿2017/03/19 16:28
編集2017/03/20 07:10総合スコア21737
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/19 16:49
2017/03/19 21:12
2017/03/20 01:13 編集
2017/03/20 01:22
2017/03/20 02:32 編集
2017/03/20 03:24
2017/03/20 03:56
2017/03/20 04:16
2017/03/20 04:23
2017/03/20 04:43
2017/03/20 05:08
2017/03/20 05:17
2017/03/20 07:09 編集
0
「Google C++スタイルガイド」の演算子のオーバーロードに関する記述が参考になるかもしれません。Googleでは演算子のオーバーロードはいろいろ欠点があるので使ってはいけないとしているようです。リンク先のブログの筆者も同じことを考えているのかもしれません。
ただし、それが当たり前だと決めつけて否定してしまってはそこで「思考停止」です。デメリットに気をつけつつどう使えばメリットを引き出せるのかを考えることが大事です。
投稿2017/03/19 12:54
総合スコア5944
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/19 13:09
2017/03/19 13:20
2017/03/19 13:27
2017/03/19 13:41
2017/03/19 13:50
2017/03/19 14:01
2017/03/19 14:09
2017/03/19 14:09
2017/03/19 14:12
2017/03/19 14:48
0
こんにちは。
演算子は、その記号自身が意味を持っています。
その意味へ誤解してしまうような定義は「悪」です。退治するべきと思います。
以下は極端な例です。
C++
1int operator+(int lhs, int rhs) 2{ 3 return lhs * rhs; 4}
こんな極端な定義をする人はいないでしょうが、ここまで行かなくても そこそこ誤解を招くような定義をそのブログの筆者は見たことが有るのかもしれませんね。
私自身は見たことはないです。演算子のオーバーロードをするスキルを持っている人が、そこまで愚かであるケースはレアであると信じたいですね。
投稿2017/03/19 11:09
総合スコア23272
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/19 11:19 編集
2017/03/19 11:53 編集
2017/03/19 13:29
2017/03/19 14:02 編集
2017/03/19 14:40 編集
2017/03/19 15:09
2017/03/19 16:14 編集
2017/03/20 00:39
2017/03/20 05:18 編集
0
個人的にはoperator overloadはありったけ悪用している人なので、operator overloadが悪く言われるのは納得行かないですね。
グローバル空間を汚すことは行儀が悪いコードと言われます
という話も出ているようですが、別にArgument Dependent Look-Upがあるので、名前空間に押し込むこともできますし、template classのoperator overloadにはSFINAEを併用することでoverload resolutionの曖昧さは消せます。
じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆で、
http://boleros.hateblo.jp/entry/20130604/1370364968
C++erの間では昔から言われている、「メンバ関数を増やすよりもフリー関数を使うべき」という言説の根拠がこれでまた一つ増えた。
C++は概ねフリー関数にするほうがスッキリする言語です。
operator overloadが良くない的な言説で前に見かけたのは
https://cpplover.blogspot.jp/2015/05/tech-8-c.html
operator overloadするとoperatorの実行コストが見積もれない、というのがあります。
が、江添さんが論破しているように、私も懐疑的です。それって普通の関数/メンバ関数でも同じじゃないかな?という意味で。
私が先日operator overloadを悪用するライブラリとして、文字列を分割するものを作りました。
C++でPStade.Oven(pipeline)風味なstringのsplitを作ってみた
http://qiita.com/yumetodo/items/bf2bc5c1d49d5aec3efa
cpp
1#include "../include/string_split.hpp" 2#include <iostream> 3int main() 4{ 5 std::string s = "arikitari na world!"; 6 const auto s_1 = s | split(' ')[1];//"na" 7 std::string s2 = "123,421,113"; 8 const auto s_2 = s2 | split(',') >> [](const std::string& s) { 9 return std::stoi(s); 10 };//[123,421,113] 11 s2 | split(',') >> [](std::string&& s) { 12 std::cout << s << std::endl; 13 }; 14 /*stdout: 15 123 16 421 17 113 18 */ 19 return 0; 20}
operator[]
> operator>>
> operator|
の順番を利用して、遅延評価的ななにかを実現しています。
C++には遅延評価が存在しないので、operator overloadはその実現のための強力なツールです。
他の言語はともかく、C++のoperator overloadとは、ちょっと特殊な関数に過ぎません。
C言語のoperatorの挙動を理解するために、operator overload的な考え方で説明する試みは(身内では)成功しています。
関数の創世から深淵まで駆け抜ける関数とはなんぞや講座 - Qiita#演算子を関数のように解釈してみよう
で、operator overloadを賞賛しまくったわけですが、問題がないわけではありません。
先程、operator overloadを利用して遅延評価を実現したライブラリを紹介しましたが、これは利点であると同時に欠点でもあります。
const auto s_1 = s | split(' ')[1];//"na"
のようなコードが先程ありましたが、実際に文字列の分割処理をしているのは、operator |
です。
ですが、見かけ上、split関数がそれをやっているように見えてしまいます。
もちろんどんなC/C++の初心者本にも乗っている、演算子の優先順位表を見れば一目瞭然ではありますが、どれだけの人があの表を暗記しているかと言われると、疑問符です。
次はもっと大きな問題です。
ここで比較演算子 operator<
/ operator<=
/ operator>
/ operator>=
を取り上げます。
https://cpplover.blogspot.jp/2015/02/isoiec-jtc1sc22wg21-papers-2015-pdf.html
比較関数の提案。
そもそも比較というのは様々ある。同値関係と順序とがあり、同値関係にはequivalenceとequalityがあり、順序には、partial ordering, weak ordering, total orderingが存在する。例えば、通常のソートにはweak orderingが必要で、メモ化にはweak orderingとequalityが必要だ。
三種類の異なる順序を、operator <ひとつで扱うという既存の仕組みがそもそも間違っていたのだ。そこで、これら三種類の順序比較をテンプレート関数として標準ライブラリに追加してはどうか。
一口に比較と言ってもいろいろな比較があるのにもかかわらず、operator<
一つで評価する、という現状があります。これはやはり問題ではないか(どのorderingか実装を読まないとわからない問題)ということで、解決に向けて幾つかの提案が出ています。
https://cpplover.blogspot.jp/2017/02/c-p0501r0-p0549r0.html
operator <=>の提案。
これまで、様々な種類の比較(strong/weak/partial orderingやequality)について、それぞれstrong_order_less_thanとかweak_order_less_than
operator <=>はthree-way comparisonを提供する。
a <=> bを評価した結果の値rは、a < bの場合 r < 0。a > bの場合r > 0。a == bの場合r == 0となる。
operator <=>によって、比較の種類の問題が解決できる。戻り値の型によって種類を表せばよい。例えばある型がstrong orderingをサポートしている場合は、以下のように書く
まあいろいろ見てきましたが、個人的にはoperator overloadはどんどん使っていいと思います。C++が古い言語にもかかわらず(少なくともJavaよりは古い)、
http://qiita.com/omochimetaru/items/621f1ef62b9798ee5ff5
できることだけは非常に先端的だったりします。
なんて言われる原因は、templateとoperator overlaodで殴れるからだと思っています。
別に嫌わずにかわいがってあげようよ、と思う、今日このごろです。
投稿2017/03/20 00:05
総合スコア5852
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/20 02:07
2017/03/20 02:47
2017/03/20 05:19
2017/03/20 17:26 編集
2017/03/21 02:03
2017/03/21 09:08
退会済みユーザー
2017/03/21 11:41 編集
2017/03/21 22:05
退会済みユーザー
2017/03/22 03:51
2017/03/22 05:02
2017/03/22 05:02 編集
2017/03/22 06:30
退会済みユーザー
2017/03/22 07:38 編集
2017/03/22 08:01
退会済みユーザー
2017/03/22 08:06
2017/03/22 08:49
2017/03/22 08:59
退会済みユーザー
2017/03/22 09:25 編集
2017/03/22 11:00
退会済みユーザー
2017/03/22 11:41 編集
2017/03/22 13:49 編集
2017/03/22 13:43
退会済みユーザー
2017/03/22 14:08
2017/03/22 14:25 編集
2017/03/22 14:33
2017/03/22 14:33
2017/03/22 14:45 編集
退会済みユーザー
2017/03/22 14:46
2017/03/22 15:38 編集
2017/03/22 16:11 編集
2017/03/22 22:17
退会済みユーザー
2017/03/23 01:34
2017/03/24 00:56 編集
退会済みユーザー
2017/03/24 19:10
2017/03/25 05:19
0
有意義な議論ができて楽しかったです。
補足で少し調べたことがあるので、折角ですので簡単に書いておきます。
じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆
これについて、Effective C++ 第3版の「23項 メンバ関数より、メンバでもfriendでもない関数を使おう」に解説がありました。
メンバ関数はprivateメンバにアクセスできる。ということは、そのメンバ関数はカプセル化を弱めてしまう。なので、privateメンバへのアクセスが必要ない関数は「メンバでもfriendでもない関数」を使おうと言う主旨と理解しました。
グローバル変数はスコープが広いから、無闇に使うのは良くないですね。
それと同様、privateメンバにアクセスできるメンバ関数もprivateメンバをアクセスできる場所を広げると言う意味でスコープを広げるので、無闇に使うのは良くないです。見落としがちですが大事な視点だなと思いました。(私も見落としてました。)
グローバル空間の汚染は悪か?
次に、フリー関数はグローバル空間を「汚染」するのか?という疑問があります。
C++の関数名はマングリングされているので、引数の型名もマングリングされた関数名に含まれます。
これは、名前空間名も同様です。
であれば、固有のクラスを引数に取る関数がグローバル空間を「汚染」しているのかと言うと、そうではないと思います。グローバル空間で当該クラスと無関係に作った識別子とぶつかる心配はありませんから。
int型や標準ライブラリで定義されている型を引数に取る「一般的な名称」のグローバル関数を定義することはグローバル空間の汚染に該当すると思います。汎用なライブラリで避けるべきはこちらですね。
投稿2017/03/21 03:49
総合スコア23272
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/21 14:26
2017/03/21 22:09
2017/03/22 02:28
2017/03/22 09:01 編集
0
C++はよく知らないのでこんな難しい議論を完全に理解はできないですが。
C言語では、私は(値が0,1のどちらかとは限らない)変数xをブール値に返還するとき !!x と書いたことが多かったです(整数型やポインタ型に対して)が、C++ではせっかくboolが使えるのに、コンパイラは「「!」がオーバーロードされているかもしれない」と最適化してくれなさそうなところが気がかりです。
削除したかったのですが、削除できない仕様だそうなので。
!!x ではなく x != 0 がよい書き方ですね。xがbool型なら x != false 、xがポインタ型なら x != nullptr でしょうが。
投稿2018/12/04 11:47
編集2018/12/11 18:48総合スコア100
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
自分の中でホットな話題だったので、私見を述べさせていただきます。
自分は演算子オーバーロード反対派です。理由としては、「プログラム言語の元々の仕様」と「プログラマが定義した仕様」の境界が曖昧になる、というのが一番大きいです。
ここでいう「プログラム言語の元々の仕様」とは四則演算や変数への代入などのもともと言語の仕様のことを指します。一方、「ユーザーが定義した仕様」は、プログラム言語の機能を組み合わせて、プログラマがその組み合わせに意味を与えたものを指します。これは、クラスや関数が持っている役割です。
演算子オーバーロードはこの2つの分類を曖昧にします。例えばC++において、構造体A
に対する変数x,y
が定義されているします。この時、以下のコード
x = y;
はこの部分を見ただけでは、「プログラム言語の元々の仕様」によって、ビットコピーされているのか、それとも演算子オーバーロードによって「ユーザーが定義した仕様」が呼び出されているのか分かりません。
私はこれこそが問題だと思っていて、この区別さえつけばよいと思ってます。
例えば、質問の背景にあったScalaのコード、
x add y
はadd
がプログラム言語の仕様にない(予約語にない)ということが分かれば、「ユーザーが定義した仕様」だということが一目でわかります。なので、演算子オーバーロードを使うときは演算子の先頭に$
を付けて、
$=, $+
みたいにできる言語があればいいのになー、とか思ってます。
自分の意見を述べるだけになってしまってすみません。今回質問に沿う形でまとめれば、私の意見は
「どちらの言語の仕様も、上記の2つを区別できないので、どちらも大差なく悪い」
ということになります。
投稿2017/03/21 10:04
総合スコア8
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/21 13:58
2017/03/21 14:10
2017/03/21 22:08
2018/12/11 22:13
0
boostライブラリの一部に見られるような演算子オーバライドで成り立っているような物もあります。(「 | 」(OR演算子)をパイプとして使っているとか)
多くの人に直感的に受け入れられるものであれば良いのですが、そうでないようなものは混乱を招くだけなので、基本的には演算子のオーバーライドは慎重であるべきだと思います。
投稿2017/03/21 08:18
総合スコア3579
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/03/21 13:37 編集
2017/03/21 20:01
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。