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

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

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

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

Q&A

解決済

10回答

25410閲覧

C++の演算子のオーバーロードの悪いところってなんですか?

grovion

総合スコア145

C++

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

13グッド

16クリップ

投稿2017/03/19 10:25

編集2017/03/24 03:25

13

16

質問内容

「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さんにベストアンサーを決定しました。活発な議論がとても楽しかったです。皆様ありがとうございました!

hoshimasa14, tetsukay, miroku, philomagi, mtempa, Chironian, kobaya_c, KaiShoya, yumetodo, hoshi-takanori, 他3名👍を押しています

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

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

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

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

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

grovion

2017/03/19 11:11

その通りです。原文には「殺せ」という表現はありません。ただこの日本語の記事の最初に書いてあるとおり、翻訳ではありませんし、この日本語記事を書いてくださった方は、演算子のオーバーロードに何かしらの問題があると考えてる方だと思います。そのため、質問させていただきました。(引用:逐一流れをなぞってるけど翻訳ではなく読書ノートという位置づけで一つよろしくお願いします(逃げ)。あと日本語割と砕けてる。)
ikedas

2017/03/19 11:19

これ、原文をねじまげていると言われても仕方ないです。大半は原文の日本語抄訳なのに、ところどころ原文にないことがまぎれこませてある。個人的には、論じるに値しないと思いました。
Chironian

2017/03/21 03:50 編集

BAはraccyさんの回答に一票入れます。一番議論が盛り上がってますし、後から来た人が読む際に一番読んで欲しい回答ですから。
grovion

2017/03/24 03:23

ベストアンサーについて:予想を超え、様々な人でこの回答作りあげている質問になりました。そのため、とてもベストアンサーが選びづらかったです。ですが、teratailから催促メールも来ますし、投票の一番多い、faithandbraveさんにベストアンサーを決定しました。活発な議論がとても楽しかったです。皆様ありがとうございました!
guest

回答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
faithandbrave

総合スコア132

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

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

grovion

2017/03/21 14:19

faithandbraveさんご回答ありがとうございます 個人的には、とても興味深い内容が盛り沢山でした。 特に、「短絡評価をハンドリングできない」というのは、何か問題を孕んでそうな感じが直感しました。 もしよかったら、詳しくお願いしたいです。 「短絡評価」は用語は知らなかったですが、「短絡評価」自体は知っていたと思います。 ハンドリングできないというところが、まだピントきません。 `m[x, y]`はタプル(ペア)を使えば、それぽく出来そうですが、言いたいことの本質を解決はできませんよね。 [a, b)は構文解析が難しそうなので、Swiftとかにある(1..10)とか(1..<10)みたいなにすれば、演算子にできそうですね といっても、以下で言われていたとおり、 > 組み込み型と同じことをユーザー定義型でもできるようにすることです ユーザー定義の演算子をC++で定義できない状態では豊かな演算子を作ることはできないですね --- 脱線 ---- 質問内容からは完全にそれていますが、 型安全なI/Oライブラリの設計については、すごく惹かれます。 (というのも最近はHaskell大好き人間になっているので、型安全にして、以下に潜在的なバグの原因を潰せるかというのは、僕のすごい関心事なのです) > printfはフォーマットが文字列になっていてそれがコンパイル時に解決できないことが問題 というのは、ずっと思っていて、頑張って自分でいつも安全なコード書くように心がけるしかない(でも警告でるので、printfはコンパイラで特殊扱いしてるんですかね)感じでした constexprを使った型安全なIO設計みたいです Rustだと優秀なマクロの機能を使って、コンパイル時に型のエラー、摘出可能な出力ができたと思います http://qiita.com/YusukeHosonuma/items/13142ab1518ccab425f4 (Rustはほとんど書いたことありませんが) マクロではない方法での実現にすごく興味があります。 念のために、Cのマクロはぜんぜん違うみたいです。構文木を操れるとか、Scalaのmacroに近いんですかね? マクロ以外の実装だと、変数展開するとかですかね?
faithandbrave

2017/03/21 14:37 編集

短絡評価 (short circuit)について。組み込み演算子としてのoperator||とoperator&&は短絡評価されますが、オーバーロードして定義したoperator||とoperator&&は短絡評価されません。オーバーロードした方はあくまで関数ですので、関数の機能として短絡評価なんてものはなく、必要になることがレアケースなoperator||とoperator&&のためにそのような機能を導入はしませんでした。 詳細は『C++の設計と進化』を読んでください。 m[x, y]は、現在は多くのライブラリが、関数呼び出し演算子で代用しています(Boost.MultiArrayやEigen)。m(x, y)という形式です。しかしそれは苦肉の策であって、代用であることは変わりません。C++のvalarrayやPythonのNumPyなどを見ていると、いまなら初期化子リストを添字演算子のパラメータにするのがよいかもしれません。m[{x, y}] constexprを使った型安全なIO設計については、今のところはBoost.Metaparseががんばっています。コンパイル時の正規表現文字列の検証とかもやっていますので、exampleや発表資料などを見てみるとよいと思います。これはテンプレートという仕組みを使用したものです。テンプレートについての説明はここの質問・回答からは外れるため、別途文献を当たってください。手前味噌ながら、私の著作である書籍『C++テンプレートテクニック』というものもあります。
grovion

2017/03/21 14:49

faithandbraveさん、ご丁寧にご回答ありがとうございます > オーバーロードして定義したoperator||とoperator&&は短絡評価されません。 ということは、左項も右項も両方共評価されてしまうんですね。理解できました。 C++が、遅延評価や名前渡しなどに対応すれば、これもハンドリングできるということになるってことですよね Boost.Metaparseについての情報ありがとうございます こういうことは詳しい方から聞けてとてもありがたいです
faithandbrave

2017/03/21 14:56

> C++が、遅延評価や名前渡しなどに対応すれば、これもハンドリングできるということになるってことですよね その通りです。式の評価を遅延させるだけなら、ユーザー側でラムダ式を渡す方法もありますが、それは演算子側の仕組みではないですね。
guest

0

私の大嫌いな C++ のオーバーロードがあります。

C++

1std::cout << "Hello World!";

この演算子はビットシフトに使うものであって、入出力に使うものではないはずです。

C# の話になりますが、マイクロソフトが言ってくれました。
演算子のオーバーロード使用方法のガイドライン

演算の結果がすぐに明確にわかる場合に、演算子のオーバーロードを使用します。たとえば、ある Time 値を別の Time 値から引いて TimeSpan を取得できるようにすることには意味があります。しかし、or 演算子を使用して 2 つのデータベース クエリの和集合を作成したり、shift を使用してストリームに書き込みを行うことは適切ではありません

投稿2017/03/19 12:34

Zuishin

総合スコア28662

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

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

grovion

2017/03/19 12:50

ご回答ありがとうございます。 確かに、shiftの件、よくわかりました。「cout << "文字列"」は演算というより、書いたときの見た目が、出力してそうな感じになることを想定して、用意した演算子ぽいです。つまりもはやshiftという意味はないですね。「+」をオーバーロードするときは、加算のイメージがある場合が多そうですが... ここまで、来ると質問悪かった気がします。 X「C++の演算子のオーバーロードが悪いところはどこ?」 O 「C++の演算子のオーバーロードはだめで、Scalaの演算子のオーバーロード*では、なぜいい?」 というところかもしれません。 (注意:「殺せ」と書いてある記事はScalaのコードです) (*: 厳密にはScalaでは演算子のオーバーロードとは言わないと思いますが) 僕の認識では、C++より新しい言語は、C++で良くなかったところを改善しているように思えます。 例えば、ひし形継承の問題とかです。 なので、ScalaとかはC++であまり良くなかった演算子のオーバーロードをどのように克服しているのか知りたいです。
Zuishin

2017/03/19 12:54

C++ のオーバーロード自体は悪くありません。 私は、標準のライブラリがこんなオーバーロードを用意したこと、そしてそれが C++ を象徴するものであったことが罪悪だと思います。 おそらく、「C++ ではこんなことができるんだよ」というデモンストレーションだったのでしょうが、これを見て「面白い!」と思った人たちがこのような奇怪なオーバーロードを量産したことが問題だったと思います。
grovion

2017/03/19 12:57

Zuishinさん、お返事感謝します > 奇怪なオーバーロードを量産 「奇怪なオーバーロード」はScalaでも量産できるはずなので、なぜ、Scalaの場合は「殺さずにすむ(笑)」のか不思議が残ります
catsforepaw

2017/03/19 13:02

> std::cout << "Hello World!"; 私もそれ、大嫌いです。やはり、本来の意味から想像もできないような挙動に演算子オーバーロードすべきではないですよね。
Zuishin

2017/03/19 13:07

量産できるのとするのは違います。 繰り返しになりますが、C++ の場合は標準ライブラリが見本として奇怪なオーバーロードを掲げていて、それを手本にした奇怪なオーバーロードが責められることなく大手を振っていました。 その他の言語はそれを反省してそれぞれにガイドラインを掲げています。 誰でも殺人ができます。でも恐れられるのは殺人をする人です。C++ には黒歴史があります。
Zuishin

2017/03/19 13:12

catsforepaw さんもそうですか。私が C++ を知った時、何を見てもこれが一番最初に書いてあるのを見て C でいいやと思ったことがあります。
grovion

2017/03/19 13:18

> 量産できるのとするのは違います。 Zuishinさん言いたいことがよくわかりました C++標準自体が奇怪なオーバーロードを掲げているところが問題ということですか。納得できます。 各言語界で、独特の文化がある感じはします。 勝手な印象ですが、関数型が混じったScalaや純粋関数型のHaskellなどのプログラマさんたちのマナーは、 いい感じですが、C++を書く人たちは色々で、命名規則も、グローバル変数の扱いも、名前空間の汚染も、想定しにくい副作用も...、気にせず、動けば良いコードを書いている方が多い気もします。
raccy

2017/03/19 13:35

Rubyも $stdout << "Hello World!" とできます。 よし、C++と一緒に、Rubyも滅ぼそうぜ!
grovion

2017/03/19 13:48

Rubyも「演算子はメソッドだ」の言語でしたよね $ ruby -e 'p 1.+(2)' 3
episteme

2017/03/21 00:23

streamに対する <<,>> は unix/linux-shell のパイプに由来すんじゃないかしら。 であるなら、最初に滅ぼすべきは cat *.txt >> merge.txt の類かも。
faithandbrave

2017/03/21 17:22 編集

> 1. C++ の場合は標準ライブラリが見本として奇怪なオーバーロードを掲げていて、それを手本にした奇怪なオーバーロードが責められることなく大手を振っていました。 > 2. その他の言語はそれを反省してそれぞれにガイドラインを掲げています。 この2件について、詳細を教えてください。1. について、責められるべきオーバーロードとは具体的にどれでしょうか。2. についてC++からの反省が言及されているところがあったら教えてください。 それと、IOストリームのライブラリについて、私の見解を回答に書きました。IOストリームは標準化前にBjarne氏が個人で書いたものであり、2017年現在としては設計がだいぶ古くなっており、ライブラリの設計技術も向上しています。この回答は大昔のC++の嫌いなところを挙げただけで、現代への言及がないように思います。 それと、マイクロソフトが言ったから正しいわけではありません。C++はマイクロソフトの規約に従うものではありませんし、ライブラリには作者の設計思想があります。他所の規約は自身の考えを作るための元ですので参考にはなりますが、答えではないです。 Rubyについてraccyさんが言及されていますが、Rubyでは配列への要素追加も<<を使います。 https://docs.ruby-lang.org/ja/latest/class/Array.html#I_--3C--3C
faithandbrave

2017/03/21 18:08 編集

> 勝手な印象ですが、関数型が混じったScalaや純粋関数型のHaskellなどのプログラマさんたちのマナーは、 > いい感じですが、C++を書く人たちは色々で、命名規則も、グローバル変数の扱いも、名前空間の汚染も、 > 想定しにくい副作用も...、気にせず、動けば良いコードを書いている方が多い気もします。 人口の違いによるものではないでしょうか。人が多ければそれだけ多様なコードが出てきます。コーディングスタイルがプロジェクトごと、というのは今では少数派な言語だとは思いますが、既存の資産があるところにあとからルール追加はできないでしょう。ただし、最近ではC++ Core GuidelinesというのがC++標準化委員会によって作られています。 特定の言語だからマナーのいいプログラマが多い、というのはまだ優秀な人(アーリーアダプター)しかユーザーが付いていないと考えられる気がします。Javaだからオブジェクト指向でちゃんと設計してるぞ、ということはなく、ひとつのクラスしかない巨大なプロジェクトなんていうのもあります。
guest

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
raccy

総合スコア21737

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

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

Chironian

2017/03/19 16:49

> 対して、Scalaでは暗黙の型変換を定義できるため C++も定義できますよ。(引数を1つ取るコンストラクタやキャスト演算子) でも、C++のメンバ関数はLhsを指定できないです。ScalaのotherがLhsとRhsのどちらともを含むのならばできそうな印象です。 もしそうならdef +(other: ComplexInt) の引き算版が気になってしまいますね。何か対策が用意されているだろうと思いますが。
raccy

2017/03/19 21:12

Scalaは左辺と右辺のどっちもです。そうでなければコンパイルが通りません。 暗黙の型変換にそこまでくわしくないですが、合う物がなければ、変換の材料とマッチしそうなメソッドをコンパイル時に探すという処理らしいので、そこまで問題にならないのかも知れません。
grovion

2017/03/20 01:13 編集

raccyさん、とてもわかりやすい例をありがとうございます この例を見て「確かに!」となりました。足し算なら可換な感じで定義したいので、なるほどなぁと思いました。 > 足し算という処理は一つにまとめることができますので、C++よりはマシであると言えるのではないでしょうか。 確かに、C++だと、1 + aを実現するためには、足し算定義が、一箇所にまとめる方法が思いつきません。 ただ、これは演算子のオーバーロードの話より、暗黙的型変換の強さを見せつけた話のようにも思えました。 (暗黙的型変換が強いからいいと言っているわけでもありません) > ComplexInt operator+(int x, const ComplexInt &y)というグローバル関数を別途定義する必要 というC++の演算子のオーバーロードの問題点を克服する方法が、 暗黙型変換で解決できますってことになってる気がするです。 ------- Chironianさん > def +(other: ComplexInt) の引き算版が気になってしまいますね。 これは、単純に以下の「-」メソッドを1つ定義するだけで、CompelxIntもIntもサポートされることになると思います。 def -(other: ComplexInt) = new ComplexInt(real - other.real, imag - other.imag) 「-」メソッド一つで、Intもサポートされちゃうのは、IntをComplexIntに暗黙的に変えてくれるからです。 例えば、 1 - cmpxをみたら、コンパイラが、 new ComplexIntReal(1) - cmpxのように変換してくれるということでしょう。 他にも、 cmpx + 1なら、 cmpx + new ComplexIntReal(1)のように変換されるでしょう。 (おまけですが、暗黙的型変換は静的にコンパイル時に探されるので、そこそこ安心して使えます) コンパニオンオブジェクト(クラスメイト同名のobject(今回のobject ComplexInt))の中のimplicitは特別にコンパイラが探しに行ってくれるので、グローバルは汚染されずにすんでますね。 でも、個人的には、やっぱり暗黙的型変換は、どこで定義されているか探し回らないといけない感がある気がするので、 可読性は下がっていくような気がします。
grovion

2017/03/20 01:22

あっ、分かった気がします。 というよりちゃんとraccyさんの本意がつかめてなかったかもです。 C++の演算子のオーバーロードの問題点は、 > ComplexInt operator+(int x, const ComplexInt &y)というグローバル関数を別途定義する という定義ができてしまうことにあるのでしょうか? つまり、あたかもintの演算子を追加したかのような見た目になるということです。 例えば、 `std::cout << ....`の「<<」をサポートするためにも、以下のように ostreamにどんどんと演算子定義が増えている感じですし。 std::ostream &operator<<(std::ostream &os, const ComplexInt &ci){...} Scalaの演算子ぽいメソッド(?)では、C++の演算子のオーバーロードのように、 既存の型(またはクラス等)に直接演算子を追加することはできないですし。
yohhoy

2017/03/20 02:32 編集

「演算子オーバーロード」と「グローバル名前空間の汚染」は直接関係ないと思います。通常、演算子オーバーロードの定義は操作対象型と同じ名前空間で行われるか、操作対象クラスのfriend関数として定義されるはずです。
raccy

2017/03/20 03:24

> yohhoyさん 何かの原理主義者が、グローバル空間に関数を追加するなんて、それが標準と同名なんてとんでもないと勘違いしているだけですので、あまり突っ込んで欲しくなかったです。yumetodoさんご言っている通りです。むしろ、Scalaの暗黙的な型変換の方が何をしているのか把握していないと危険だと思っています。
Chironian

2017/03/20 03:56

> def -(other: ComplexInt) = new ComplexInt(real - other.real, imag - other.imag) はまずいと思いますよ。「real - other.real」のotherがLhsとRhsのどちらかに展開される筈です。 1 - a と a - 1の両方ともが、a - 1と解釈されてしまいます。これは困ります。 Scalaの設計者も気がついているでしょうから、何か対策していると思いますが。 raccyさんが、Scalaのメリットとして書かれていることは、Lhsを指定できることだろうと思います。 C++のメンバ関数ではLhsを指定できませんから。標準化委員会(?)はフリー関数で指定できるので不要と判断したということかもです。 > 確かに、C++だと、1 + aを実現するためには、足し算定義が、一箇所にまとめる方法が思いつきません。 クラス外定義なら当然可能ですし、friend関数ならクラス内定義も可能ですよ。 http://www.booran.com/menu/cplus/friend.html
raccy

2017/03/20 04:16

C++が得意なフレンズさんに教えて貰ったので、名前空間もfriendsも足しました。そもそも演算子オーバーロードを使った事って無かった…。C++そんなに書かないですし。
raccy

2017/03/20 04:23

`-`ですが、Scalaはそこに行く前に型変換で同じ型(ComplexInt)になります。`real - (other.real)`であり、両方ともInt型なので、標準の処理がされるだけです。型変換を探すのは該当するメソッドがない場合だけです。
grovion

2017/03/20 04:43

--- もともとの質問から完全にそれている気もしますが --- Chironianさん > 1 - a と a - 1の両方ともが、a - 1と解釈されてしまいます。これは困ります。 僕が勘違いしているだけかもしれませんが、たぶん両方共a - 1に解釈されないとおもいますよ 以下のideoneでじっこうした結果があるので、良かったらみてみてください https://ideone.com/U8X7G2 > クラス外定義なら当然可能ですし、friend関数ならクラス内定義も可能ですよ。 一箇所にまとめられるという意味ですか?まだよくわからないので、もし良かったら補足説明お願いしたいです。 raccyさんのScalaのコードでは * 「+」の定義が一回だけ * Int型をComplexInt型に変換する方法 これがあるため、足し算の定義を一箇所にまとめられます それに対して、C++では3つ書かないと行けないように思えます * Complex + Complex * Complext + Int * Int + Complex
Chironian

2017/03/20 05:08

> 僕が勘違いしているだけかもしれませんが、たぶん両方共a - 1に解釈されないとおもいますよ 以下のideoneでじっこうした結果があるので、良かったらみてみてください https://ideone.com/U8X7G2 うわっ、本当ですね。なんか勘違いしてました。 ということはC++とほとんど差はないと思います。 > 一箇所にまとめられるという意味ですか? 一箇所にまとめて書けると言う意味です。1つにまとめることができると言う意味ではないです。 でも、フレンド関数+暗黙の型変換を使えば1つで済みました。(raccyさんのソース流用) http://melpon.org/wandbox/permlink/PvZWTA2JXg1emLCQ なるほど、ブログの筆者は「フリー関数」ではなく「メソッド」で書けるから「殺さなくてよい」と言っているのですね。やっと理解できました。この2つがそこまで大きな差とは思えませんが、人によってはそのように感じるのかも知れないですね。
grovion

2017/03/20 05:17

Chironianさん本当にお世話になってます 以下のコード読みました Scalaと同じくC++でも暗黙的にintをComplexIntに変換できてますね http://melpon.org/wandbox/permlink/PvZWTA2JXg1emLCQ グローバルについての汚染もなさそうで、すごくうまくいってます
raccy

2017/03/20 07:09 編集

知らない機能が一杯…。Chironianさんのコードから余計な物消して結論にしました。一番すっきり書けたから、C++だけオッケーでいいかと、もう。
guest

0

「Google C++スタイルガイド」の演算子のオーバーロードに関する記述が参考になるかもしれません。Googleでは演算子のオーバーロードはいろいろ欠点があるので使ってはいけないとしているようです。リンク先のブログの筆者も同じことを考えているのかもしれません。

ただし、それが当たり前だと決めつけて否定してしまってはそこで「思考停止」です。デメリットに気をつけつつどう使えばメリットを引き出せるのかを考えることが大事です。

投稿2017/03/19 12:54

catsforepaw

総合スコア5944

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

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

grovion

2017/03/19 13:09

ためになるリンクの提供ありがとうございます Googleは「特殊な状況を除き、演算子をオーバーロードしてはいけません。」なんですね > リンク先のブログの筆者も同じことを考えているのかもしれません。 そうかもしれませんが、Scalaでは演算子のオーバーロードに似たようなことを、ブログの筆者さんは平気だとしてやっているので、C++ではダメで、ScalaだとOKな理由がしりたいところです。
catsforepaw

2017/03/19 13:20

> C++ではダメで、ScalaだとOKな理由がしりたいところです。 私もそれに関しては何度読み返しても判りませんでした。ブログの筆者に聞かないと判らないでしょうね。
grovion

2017/03/19 13:27

やっぱり筆者さんに聞くしかないですかね〜 一番最初に記事を読んだとき、 > これを見て「あっ、C++の演算子オーバーロードだ、殺せ!」となったキミ、ちょっと待ってほしい を読んで、「...」となったキミ と言っているくらいならC++使いの方々なら殺せ!ってなる人が多いのかな?と思ったんですよね。 「演算子のオーバーロードだ殺せ!」ってなりますかね? (過激な表現は冗談交じりなことは理解してます)
catsforepaw

2017/03/19 13:41

> 「演算子のオーバーロードだ殺せ!」ってなりますかね? ブログ内での文体が安定しておらず、時折友達との会話のような口調で書かれていたりするので、その流れでそのような表現になったのだと思います。まぁ、気を引くための演出でしょう。特別気にするほどのことでもありません。
grovion

2017/03/19 13:50

お答えありがとうございます > まぁ、気を引くための演出でしょう。特別気にするほどのことでもありません。 ご意見ありがとうございます 確かに、ブログなので、目を引く文句としては良かったと思いますね
Chironian

2017/03/19 14:01

横から失礼します。 Google Style Guidの訳に「特に代入演算子(operator=)はたちが悪いので避けるべきです。」なんて無茶があります。(コピー代入演算子は普通に書きますから。) 念のため最新版を見ると、160度程見解が変わっているようです。 https://google.github.io/styleguide/cppguide.html#Operator_Overloading あ、operator|の置き換えは嫌がってますね。boostが結構安易に使っている感があるので嫌っているのかも。
grovion

2017/03/19 14:09

Chironianさんお世話になってます。ありがとうございます。 最新版のリンクの提供ためになります。
catsforepaw

2017/03/19 14:09

> 念のため最新版を見ると、160度程見解が変わっているようです。 ご指摘ありがとうございます。 一応そちら(の日本語訳)にも目を通して違いには気づいていたのですが、ブログの日付が2013年だったので、時系列を考えて古い方を出しました。
grovion

2017/03/19 14:12

catsforepawさん、ありがとうございます なるほど、そこまで考慮してということでしたか、すごいです
Chironian

2017/03/19 14:48

おおっ、そこまで見ていたのですか。さすが。
guest

0

こんにちは。

演算子は、その記号自身が意味を持っています。
その意味へ誤解してしまうような定義は「悪」です。退治するべきと思います。

以下は極端な例です。

C++

1int operator+(int lhs, int rhs) 2{ 3 return lhs * rhs; 4}

こんな極端な定義をする人はいないでしょうが、ここまで行かなくても そこそこ誤解を招くような定義をそのブログの筆者は見たことが有るのかもしれませんね。

私自身は見たことはないです。演算子のオーバーロードをするスキルを持っている人が、そこまで愚かであるケースはレアであると信じたいですね。

投稿2017/03/19 11:09

Chironian

総合スコア23272

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

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

grovion

2017/03/19 11:19 編集

ご回答ありがとうございます。 確かに演算子のオーバーロードだと「驚き」を与えてしまう、直感に反するコードが書けるのは、理解できます。 ですが、もうしわけないですがこれでは納得がいきません。 なぜ納得が行かないかというと、それは演算子のオーバーロードに限らずメソッドでも「驚き」がある直感に反するコードが書けるからです。 記号同様に、英単語にも意味を持っています。 例えば、「addItem()というメソッドがitemsを全消しする」挙動を 記述することができます。 これ以外に思い当たることがありましたら、よろしくお願いします
episteme

2017/03/19 11:53 編集

僕もそう思う。 operator殺せというなら int add(int x, int y) { return x * y; } と定義できてしまう関数もろとも皆殺しにせなあかんじゃないですか。 operatorの優先順位が元々の演算子と同じになる(変更できない)のに 不満を覚えることもあるでしょうが、これが変更できたら余計に邪悪 なものになりかねない。
Chironian

2017/03/19 13:29

Zushinさんのコメントみると、実際にオーバーロードを不適切に使いまくった人達が居るのですね。 だから、戒めとして良く語られると言うことではないでしょうか? Scalaの件は、良く分かりません。ブログの作者さんの主張は混乱しているように見えます。 以下の一文は理解できませんでした。演算子がメソッドだからこそオーバーロードできるので。(私がScalaを知らないせいで理解できないだけかも知れませんが。) > 実はね、Scalaでは演算子に見えるものでもすべてメソッドなんだよ! これにより、伝統的なオーバーロード手法よりも一貫的で簡明な演算子記法の取り扱いが可能になっているから安心してほしい。
grovion

2017/03/19 14:02 編集

Chironianさん再度ありがとうございます 一文に関してですが、Scalaは「C++の演算子のオーバーロードより一貫した方法で、演算子ぽくできるよ」と言っているのだと理解してます。 Scalaには演算子はありません。 132 + 15の「+」は演算子ではなく、 132.+(15)です。 意味は、132オブジェクトの「+」メソッドに引数15を渡しているということです。 C++でも、 a.operator+(b); のように、 aが「+演算子をオーバーロード」していれば上の書き方ができるとは思いますが、 C++ではプリミティブはオブジェクトではないので、メソッド呼び出しできないので、以下は多分できないですよね? 132.operator+(15); // エラー 一貫しているというのは、「すべてメソッドだ」という一貫した解釈によって、演算子という概念を廃止したことだと思います。 つまり、演算子とメソッドと分けた考え方ではなく、一貫して「すべてメソッドだ」というわけだと思います。 ----- (脱線します) ---- 話題からそれてしまいますが、 個人的は、正直なところ「すべてメソッドだ」という統一は良くないとも考えています。 理由は演算子は優先順位があり、メソッドにはありません。 そのため、Scalaでは、「記号を含み、中置記法」をしているもの(演算子ぽいもの)は特殊は方法で、優先順位をつけています。その特殊な方法があるため、一貫しているとはいえないのではないかと思ってます。
Chironian

2017/03/19 14:40 編集

> つまり、演算子とメソッドと分けた考え方ではなく、一貫して「すべてメソッドだ」というわけです。 int等の基本型の演算子もオーバーロードできるということですね。なるほど、一貫してますね。 でも、基本型の演算子をオーバーロードできたら何が嬉しいのか良く分かりません。混乱するだけのように感じます。もし、基本型の演算子をオーバーロードしているプログラムがあったら、「あっ、Scalaの演算子オーバーロードだ、殺せ!」と感じると思います。それがどんなオーバーロードであろうと。 優先順位の問題は頭痛いですね。何か「フリー」な演算子があればよかったのにとも思います。 演算子のオーバーロードは、本来の意味を変えてでも使うべきか微妙ですが、メソッド・チェーンをスマートに記述できるので、次善の策としては有りかなと思います。 もし、ostreameのoperator<<がprintだったら、 std::cout << "foo=" << foo; ではなく、 std::cout.print("foo=").print(foo); と書くことになります。個人的には前者の方が「直感的」と感じます。
grovion

2017/03/19 15:09

> 基本型の演算子をオーバーロードできたら何が嬉しいのか良く分かりません。 Scalaには、基本型(?)もなく、すべてオブジェクトで、全部クラスがあります。「すべてがオブジェクト」という点でもScalaは一貫した考え方があるということと考えています。 そのため、JavaのIntegerのようにboxingしなくても、ScalaのIntはパラメータ多層でそのまま使えますし。 intではなく、Intと大文字で始めるのもクラス名だから、一貫してScalaは大文字で始めていますし。 > 「フリー」な演算子があればよかったのにとも思います。 フリーな演算子というのは、自分で好きな記号を演算子にできるということですか? ----- (脱線します) ---- 話題からそれてしまいますが、 確かに、Scalaは色々と一貫していて、洗練されていると感じて、とても素晴らしい言語だと思います。ですが最近は、一貫させ過ぎることは、読み手によってわかりづらくなることがあると思っています。一貫させるということは抽象度を高くすることに通じると思うので、それによってわかりづらいと思われる可能性があると思います。で結局言いたいことは、ぼくはScala信者ではないということですね。 --- 蛇足 --- Scalaの抽象度の高いものの一つに、配列のアクセスなどがあります。 従来のarray[41]のような「[idx]」の書き方はなく、 array(41)のようにScalaでは書きます。 これは配列を関数として見ているということだと思います。(正確には部分関数) String型の配列はstrsは Intを引数にとり、String型を返す関数として考えられます。 Double型の配列doublesは、Intを引数にとり、Double型を返す関数として考えられます。 部分関数であり、完全に関数でない理由は、配列外は未定義になるからです。 Scalaでは[]を廃止して、()だけで配列アクセスも行ってしまう一貫性を持っていますが、 わかり易さからは離れてしまうと思います。 ただ、新しい視点を与えてくれるという意味で、Scalaは良い言語だと思っています。
Chironian

2017/03/19 16:14 編集

基本型(fundamental type)とは、言語のコア仕様で定義された型のことを呼びました。C++ではそのように呼ばれることが多いようです。同様な型はどんな言語でも必ず存在します。 Scalaの場合、基本型もクラスとしての特性を持っているということですね。 そして、基本型の演算子オーバーロードができると言うことは、ユーザが基本型の定義を変更できてしまうということですね。例えば、int型へdoubleを代入する演算子があったとして、デフォルトが切り捨てだったものを四捨五入等へ定義変更できるわけですね。 なるほど、C++の演算子オーバーロードより強い誘惑を受けそうです。 つまり、基本型と言えども、定義が変更されていないことを一々チェックしないと、数式一つ読めないのですね。正直、勘弁して欲しいかも。 これに対して、C++の演算子オーバーロードは元々ないものを定義するだけですから、ないはずのものが使われている時だけチェックすれば良いのでかなり楽ですね。 > フリーな演算子というのは、自分で好きな記号を演算子にできるということですか? 優先順位も含めて自由に定義できる演算子という意味です。 例えば$とか、今使ってないやつを割り当てたら便利かもとちょっと思いました。(ふと思っただけですのであまり突っ込まないで頂けると有り難いです。) 一貫させ過ぎると良くないはその通りと思います。異なるものを同じ表現にすると混乱しますから。 (JavaやC#の参照(≒ポインタ)の代入と値の代入が同じ表現って本当に嫌です。どちらかを:=などにしてくれれば良かったのに。) Scalaは配列アクセスは関数呼び出しと同じ表現なのですか。 基本型の演算子オーバーロードもそうですが、やはり一貫させすぎは良くないですね。
grovion

2017/03/20 00:39

Chironianさん、何度も何度もお返事とても感謝しています ちょっとよくわからないところがあって.... > 例えば、int型へdoubleを代入する演算子があったとして、デフォルトが切り捨てだったものを四捨五入等へ定義変更できるわけですね。 int型へdoubleを代入する演算子って、代入演算子「=」ってことですか? あと、 > そして、基本型の演算子オーバーロードができると言うことは、ユーザが基本型の定義を変更できてしまうということですね。 解釈ミスをしている可能性がありそうですが、 Intの「+」やDoubleの「/」などの挙動は変更できないと思いますので、定義の変更はできないと思います。 「基本型の定義を変更できてしまう」の意味がまだ良くわかりません 例えばRubyならオープンクラスなので、自由にメソッドの上書きができますよね。 例えば、以下の例だと小さい整数の足し算が必ず、10になってしまうみたいなことできちゃいますよね class Fixnum def +(that) 10 end end 1 + 1 // => 10 2 + 17 // => 10 こんな上書きはScalaはできないはずです。
Chironian

2017/03/20 05:18 編集

> int型へdoubleを代入する演算子って、代入演算子「=」ってことですか? その通りです。 > Intの「+」やDoubleの「/」などの挙動は変更できないと思いますので、定義の変更はできないと思います。 おっとそうですか。それができたらダメですよね。 その点では「一貫」していませんが、ここまで一貫するのは一貫し過ぎですので、好ましいと思います。 raccyさんの回答でのコメント群をみて思いました。 Scalaは演算子型のメソッド定義がC++より自由度が高くLhsを指定できるということですね。 もし、def -(lhs: Int)とdef -(rhs: Int)みたいなオーバーロードが可能であれは、C++のように前者をフリー関数にしなければならないことより望ましい一貫性があるのかも知れません。その分、構文が複雑になるので実際の構文を見てみないことには分かりませんが。 【追記】 Scalaを知らないので、def +(other: ComplexInt)を勘違いしてました。raccyさんの回答につけたコメントを参照して下さい。
guest

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

N4367: Comparison in C++

比較関数の提案。

そもそも比較というのは様々ある。同値関係と順序とがあり、同値関係には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

[PDF] P0515R0: Consistent comparison

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

yumetodo

総合スコア5852

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

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

Chironian

2017/03/20 02:07

> C言語のoperatorの挙動を理解するために、operator overload的な考え方で説明する試みは(身内では)成功しています。 なるほど。高級言語(?)しかやってない人に説明するには良い例えですね。 マシン語へ変換された時にはまさにそのような姿になっている筈ですし。 問題は、質問でリンクされているブログの筆者さんが「Scalaは基本型の演算子を再定義できてしまう」ことをC++の演算子オーバーロードより好ましい点と主張している節があることですね。 仕組み的にはyumetodoさんの書いている通り、C++の基本型も演算子オーバーロードで実装されていると見ても決して間違いではなく、再定義を許していないだけなのですから。 そして、私は基本型の再定義禁止には大賛成です。それが本当にできるのなら、Scalaは糞言語と思います。でも、実際にはちゃんと禁止されているのではないかと少し期待してます。 > できることだけは非常に先端的だったりします。 ですね。私はチューリング完全なメタ・プログラミングできるテンプレートにその理由があると思います。 演算子オーバーロードは適切に使えば見栄えが良くなりますが出来ることの本質は大差ないと思います。
yumetodo

2017/03/20 02:47

>Scalaは基本型の演算子を再定義できてしまう なんと・・・。使ったことがない言語なのであれですが、私もそれはどうなんよという思いが。
grovion

2017/03/20 05:19

> 問題は、質問でリンクされているブログの筆者さんが「Scalaは基本型の演算子を再定義できてしまう」 という記述を見つけられないでいます。 リンクというのは、http://d.hatena.ne.jp/xef/20130309/p1 のことですか? Scalaで基本型の演算子を再定義ができるとしたら、由々しき事態です
Chironian

2017/03/20 17:26 編集

すいません、これはredstoneさんの下記説明を私が誤解したものです。 > 一文に関してですが、Scalaは「C++の演算子のオーバーロードより一貫した方法で、演算子ぽくできるよ」と言っているのだと理解してます。 > Scalaには演算子はありません。 > 132 + 15の「+」は演算子ではなく、 > 132.+(15)です。 C++の場合も、クラスでoperator+を定義すれば、a.operator+(15)と書けます。 そして、元のブログの説明の文脈は演算子の再定義でした。 なので、上記説明を、C++とScalaの差は基本型についてoperator+を再定義できるかどうかの差と勘違いしてしまったのです。申し訳ないです。
grovion

2017/03/21 02:03

Chironianさん 誤解を与えるような文で申し訳なかったです
退会済みユーザー

退会済みユーザー

2017/03/21 11:41 編集

失礼ながら、ちょっと重箱の隅を・・・・ >C++erの間では昔から言われている、「メンバ関数を増やすよりもフリー関数を使うべき」という言説の根拠がこれでまた一つ増えた。 ボレロ村上さんがこのように言っていたのは、あくまでも「C++11でconstexprなメンバ関数が暗黙にconst修飾されるせい」であって、C++14が当たり前の今、 >じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆で、 この意見はちょっと極端すぎやしないかと思うわけです。必要な方を使えばいいのでは?
yumetodo

2017/03/21 22:05

まあ、C++11constexprの件はC++14で解決しましたが、そもそものメンバ関数よりフリー関数論は、各クラスに同名のメンバ関数がしばしば定義されるけど(インターフェースの共通化のために)、それは殆どが似たような内部実装になるから、どうにかしよう、しかし継承は重いからだめだ、ならばフリー関数だ、というふうに理解しているのですが。
退会済みユーザー

退会済みユーザー

2017/03/22 03:51

継承が重いとは?? 多重継承やvtbl経由のコストのことをおっしゃってるのだと思いますが、仮想関数は基本的にポリモーフィズムを実現するために使うものだし、コストがあることを理解して使うものだと思います ”仮想でない”メンバ関数、および仮想でない継承でも、基本クラスの演算子をそのまま継承して使えます(コストゼロ) コストに限らずグローバルな関数が良い理由があるならそっちを使えばいいわけで・・・・ グローバルな名前空間を汚染しないため、とかいう信仰は私もどうかと思いますが。 演算子オーバーロードだって、必要なときには使って必要なければ使わなければいいのであって。
Chironian

2017/03/22 05:02

michiru_cppさん。 「メンバ関数を増やすよりもフリー関数を使うべき」の主張の理由をTwitterで聞いたらご本人からEffective C++かどれかの書籍で読んだと思うと言うコメントを頂きました。たまたまこの本を持っていたので調べたら、その主旨が書かれてました。私の別回答に記載してます。 そして、実はEffective C++の字面だけを見ると「friendでないフリー関数の方が良い」としか書いてません。ばっと見る限り、常にその方が良いと書いてあるように見える書き方でした。丸っとオブジェクト指向プログラミングの否定であり得ないですね。内容を良く読めばそうではないことは分かりますので、別回答の「privateメンバへのアクセスが必要ない関数は」は私の方で行間を読みました。
Chironian

2017/03/22 05:02 編集

あう、2重投稿になったので削除。
yumetodo

2017/03/22 06:30

純粋なオブジェクト指向的に言えば、インターフェースを定義して、getter/setterのみ派生クラス(って言うとC++用語だけど)に定義して、それ以外の操作はすべて基底クラスから継承するのが正しいわけですが、そうすると仮想関数のコストがかかります。 そこで考えるわけです、あれ、それべつにフリー関数で良かったんじゃね?と。 ただそれでも何にでも使えるオブジェクト操作関数なんてないわけで、SFINAEハックしてみたけど、これはConcept必要やろ!という話になっているのが現状じゃないでしょうか(Conceptはよ)。 そういう意味で、「メンバ関数を増やすよりもフリー関数を使うべき」はあっていると思います、もはやoperator overloadとは完全に話がそれていますが。
退会済みユーザー

退会済みユーザー

2017/03/22 07:38 編集

>Chironianさん コメントありがとうございます、まぁ単に「ちょっと極端ではないか」と言いたかっただけで、実のところ両方のメリットについてはっきり言えるわけではありませんが・・・ Effective C++は大昔に先輩が持っていたのを読ませてもらった限りで自分では買ってないので今読めないのですが、あの本(というかScott Mayers)は少々押し付けがましい感じがしました。 アンサイクロペディアのC++のページにも、彼について「膨大な量のC++~するべからず集を編纂する。その功績以上に、それほど大量の禁忌が有った事に世界は困惑した。」とか書いてますしw 選択の余地があるということは選択する必要があるというわけで、演算子もグローバルに書くのかメンバに書くのか、どちらにもメリットがあるんだと思います(自分も両方使うし)。 現にそういう「べき」論を主張をしている本人に聞いたら、失礼ながら「Effective C++かどれかの書籍で読んだと思う」程度の認識だったわけですよね。 そういう曖昧な認識を生む「べき論」は危険だと思ったので、ちょっとツッコミ入れたかったのです。 (まぁそれを言えば質問主に対しても何か回答すべきだとは思うのですがw) >yumetodoさん あー、そういう前提でしたか・・・なるほど。 自分の場合、例えばx, y, z, wの要素を持つベクトルクラス(仮にvector4として)を書いたりしてますが(上にある複素数クラスのように、直接メンバを持つタイプ)、これは配列に突っ込んだりするので余計なメンバ(vtbl)があると困るので、仮想関数は一切入れてません。 自分はそのクラスを、次元数をテンプレートにしてるので、{x, y}あるいは{x, y, z}, {x, y, z, w}を持つ基底クラスを継承してます。(そうしないと同じインターフェースを3度も書かなければならないので) 一般的なオブジェクト指向信仰(言葉がアレですが)からすれば有り得ない設計ですが、C++は選択肢の広さからこういう設計も出来るわけで・・・・ 演算子オーバーロードも自分の場合はクラス内に書いてますが(多分グローバルでもいけますが)、仮想関数にする意味がないので非仮想です(vtblは入らない)。多分コストは全く同じですよね。自分の場合は、読みやすさからメンバにしました。そしてグローバルも使ってます(float * vector4などの実装のため)。 あと、こういうのは外部から隠蔽するメンバが全く無いですしね(一部の内部的なメンバ関数やtypedefだけprivate)。 その時その時でベストな選択は違うはず。それで「どっちでもいいじゃん」と言いたかったのです。 あと自分もconcept早く欲しいですw
Chironian

2017/03/22 08:01

michiru_cppさん。 フリー関数とメンバ関数は私も適切と考える方を使えばOKと思います。私の場合は、どちらかというとメンバ関数でできることはメンバ関数を使うと言う判断をしてました。 それに一石を投じてくれたのでyumetodoさんの引用はありがたかったのですよ。 Effective C++は確かに押し付けがましい面はありますね。この件についてもprivateメンバへの依存が悪というわけではないですから、あんなに強く表現するのはどうかなとは私も思いました。 でも、privateメンバへのアクセスが不要な時、privateメンバにアクセスできるメンバ関数で実装するのは良くないという指摘は妥当と思います。 村上さんのコメントの件ですが、理由についてもコメント頂けたのですが、Twitter上でしたので理解できるところまでいかなかったので書かなかっただけです。これは村上さんの認識不足というよりは私の理解力の問題です。
退会済みユーザー

退会済みユーザー

2017/03/22 08:06

あぁー、ボレロ村上さんの話ですか 「C++erの間では昔から言われている、「メンバ関数を増やすよりもフリー関数を使うべき」という言説の根拠がこれでまた一つ増えた。」 ↑これ、その言説を支持している、とは見えないですよ
yumetodo

2017/03/22 08:49

>自分の場合、例えばx, y, z, wの要素を持つベクトルクラス(仮にvector4として)を書いたりしてますが 自分も書いていますが、クラスがクラスしてないな・・・。 https://github.com/Nagarei/DxLibEx/blob/master/dxlibex/basic_types/point3d.hpp >私の場合は、どちらかというとメンバ関数でできることはメンバ関数を使うと言う判断をしてました。 私の場合は、似たようなクラスができない限りはメンバー関数、できたらフリー関数に、という感じでやっています。
Chironian

2017/03/22 08:59

「C++erの間では昔から言われている、「メンバ関数を増やすよりもフリー関数を使うべき」という言説の根拠がこれでまた一つ増えた。」という言葉は、 「メンバ関数を増やすよりもフリー関数を使うべき」を支持していると思いますよ。 「その言説」ってなにか他の言説のことでしょうか? 他になにかありましたっけ?
退会済みユーザー

退会済みユーザー

2017/03/22 09:25 編集

>yumetodoさん >自分も書いていますが なら話は早かったですね。拝見しましたが、そういう非仮想のメンバのみで構成する書き方をされているなら、 なおさら「しかし継承は重いからだめだ」という理由が出て来るのかよくわかりません。継承って重いんですか? 初学者を混乱させるような書き方に私はツッコミたかったのですが。 別にフリー関数を否定するわけじゃないですが、(もう一度書きますが) 「じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆で、」 というのは極端では? >Chironianさん 本人に聞かずに決めつけてませんか? 「C++erの間では昔から言われている、「メンバ関数を増やすよりもフリー関数を使うべき」という”同意できない”言説の根拠がこれでまた一つ増えた。」 と、「同意できない」を付けても自然に見えてしまいます。 個人的には、「そういうよくわからん(どっちでもいい)主張もあるようだが、これでまたそういう偏った主張を正当化する理由が出てきてしまった」という、中立というかそういう宗教論争には関わりたくないスタンスに見えました。 もう一度書きますが、どっちにもメリットありますよね?私おかしなこと言ってますかねぇ。
Chironian

2017/03/22 11:00

既に書いた通り、ご本人に聞いてますよ。まじで意外な主張でしたから。 https://twitter.com/bolero_MURAKAMI/status/843650411957436416 (Theolizer@TheorideTechは私です。他にもやり取りあります。) ----------------------- 質問> 「メンバ関数を増やすよりもフリー関数を使うべき」って見解があるんですね。必ずしもそうでもないような気もしますけど、この理由が書かれているサイト知りませんか? ----------------------- 返信> 端的に言えば、C++におけるジェネリックプログラミングの設計において、xに対して「x.f()というメンバ関数呼び出し」という制約よりも「f(x)という関数呼び出し」という制約のほうが疎結合になるというのが一つの理由です。Boost C++ライブラリの設計でもよく見られます。 ----------------------- 一般に疎結合の方がメンテナンス性は上がるので望ましいのですが、何故にフリー関数の方がメンバ関数より疎結合なのか解らなくて、Effective C++に辿り着いてやっと解ったのです。解ってしまえば単なる見落としでしたが。 > もう一度書きますが、どっちにもメリットありますよね? そう思いますよ。この言説もC++erの間で言われていることですから、オブジェクト指向プログラミングを否定しているわけではないので、メンバ関数も否定していないです。「メンバ関数を増やすな」じゃなくて「メンバ関数を増やす時はそのデメリットにも気を配ろう」だと思います。 Effective C++もぱっとみ、まるでメンバ関数を使うなと受け取れる文で書いてますが、文脈的にその解釈はにありえないし、きちんと読めば理解できるよう必要なことは書いてありました。 「23項 メンバ関数より、メンバでもfriendでもない関数を使おう」 yumetodoさんの真意までは私には分かりませんが、同じロジックが成立するので私は上記のような意味で受け取りました。
退会済みユーザー

退会済みユーザー

2017/03/22 11:41 編集

なるほど、一定の理解は示しているんでしょうね。 自分は別に「オブジェクト指向の観点からメンバの方がいい」などとは言ってません。 ここでいう疎結合というのは、STLの設計に見られるような、コンテナ・イテレータ・アルゴリズムの分離、のような話ですよね? それが”常に望ましい”とお考えですか? 特定の要件(コンセプト)を満たすものは全て適用できる関数、というのはジェネリックプログラミングの観点からは良いことでしょう。 が、常にそれを目指すべき、というのはもはや宗教です。 広く適用されては困るものもあります、例えばx, y, z, wを持つベクトル同士の乗算は、同じくx, y, z, wを持つ四元数に適用されては困ります。 持っているメンバは全く同じなので、何かしら内部の型名などで、「これはベクトルの乗算を使うべきではない」と表明してコンパイル時分岐しなければなりません。この時点で、理想からはかけ離れてしまいます。 このライブラリで書く乗算演算子は、ベクトルなのか四元数なのかそれとも他のものなのか、「ライブラリで想定できない何か」に本質的に対応できないのです。(ベクトルのように振る舞う何か、には対応できても。) 一番楽で、無理な汎用化をしない方法は、四元数がベクトルを継承し、乗算のoperator *だけ上書き(オーバーライドではなく、ただ独自の実装を定義するだけ)です。 (ここで継承を遠慮なく使うのは、乗算以外においてはベクトルと同じように振る舞ってもらう方が望ましい(少なくとも自分の用途では)からです。) 楽だし、何よりクラス定義を見るだけで、ベクトルとは違う乗算を持っていることがわかります。 はっきり言いますが、選択の余地があることについて”常に望ましい”選択など有り得ません。 それは設計意図を明確にすることを放棄して、全て利用者に押し付けることではないですか。 ちなみに自分は手元にEffective C++が無いのでScott Mayersが何と書いたのか細かいことは確認出来ません。 「極論ではないか?」と言いたかっただけです、それに対する反論をお願いしたい。
faithandbrave

2017/03/22 13:49 編集

横から失礼します。ここで言う疎結合は、非侵入的な拡張を目指す場合の設計の話だと思います。クラスのpublicメンバ関数だけで実装できる多くの拡張を想定し、その想定のためのメンバを用意して、クラスを直接いじらない(メンバを拡張しない)プログラマのために、拡張に対して開いた設計にする、というものでしょう。これは、オブジェクト指向における「開放/閉鎖原則」をC++という言語に当てはめた場合の話だと思います。 常にそれを目指すべきかというと、私はそうは思いません。 ライブラリレイヤーとアプリケーションレイヤーでは設計の仕方が違いますし、アプリケーションレイヤー内でもAPIを提供したい相手や、今後のソフトウェアの成長の可能性などを考えた上で、抽象度が高く拡張に閉じたクラスを設計するのはよくあることです。 設計の本の多くは、「あらゆる状況に対応できる設計」を目指します。ライブラリレイヤーは再利用のための領域ですから、とくにそれを目指したほうがよいです。しかし、アプリケーションレイヤーは身内しかコードをいじらない領域であり、外からの拡張に対して閉じていたとしても身内が非侵入的な拡張ではなく侵入的な拡張ができます。もちろん、アプリケーションレイヤー内でも再利用をする場合がありますので、アプリケーションレイヤーだからと言って、非侵入的な設計が不要ということではありません。
Chironian

2017/03/22 13:43

> 「極論ではないか?」と言いたかっただけです、それに対する反論をお願いしたい。 字面だけみると極論と思いますよ。 ですので反論にはならないと思いますが、私の個人的見解を述べておきます。 押し付けるつもりはないので、納得行かない部分は「そのように考える人もいるのだな」くらいに軽く受け取って下さい。 文脈的に極論を言いたい筈がないと私には思えたので、質問したり、ググッたり、文献を当たったりしてその真意を調べてみました。結果、私的には納得の行く回答に辿り着きました。 その御蔭で、恐らく私が作るプログラムのメンテナンス性が多少なりと良くなるだろうと考えています。 私と同様な経験を積む人も居ると思いますので、現状で特に問題があるとは考えていません。 逆に、人によっては字面的な極論を信じ込んで痛い目に合う人もいるかも知れませんね。(その人を仮にボブとしましょう。) しかし、文脈から極論を言いたいわけでないことは明らかなので、ボブがその文の真意を理解する努力をしなかっただけです。それはボブの努力不足と私は感じます。
退会済みユーザー

退会済みユーザー

2017/03/22 14:08

>faithandbraveさん >xに対して「x.f()というメンバ関数呼び出し」という制約よりも「f(x)という関数呼び出し」 とあったので、この場合 std::vector<int> v; v.sort(pred()); ではなく std::sort(v.begin(), v.end(), pred()); であることを疎結合(の一種)と呼ぶのだろうと思ったのでああ書いたのですが、間違いでしょうか。 いずれにせよ、疎結合という用語に詳しくない上、Effective C++が手元に無いので確認できません。 私はChironianさんに対する反論として上の例を出したのですが、反論にはなっているはずです。 >Chironianさん ならば何故横槍を入れてきたのか全く理解できません。 不純な動機ではないか、と疑わざるを得ません。
faithandbrave

2017/03/22 14:25 編集

それも疎結合の一種ではありますね。疎結合の部分はイテレータという中間インタフェースを設けているところですね。 書籍『C++ Coding Standard』とかでHerb Sutterが問題としているのはstd::basic_stringのメンバ関数が多すぎるというものです(とくに検索関係)。これはbasic_string以外の文字列(char配列や外部ライブラリの文字列)に対してもその関数が使えるのにそうしていない、というのが大きな理由でしょう。
faithandbrave

2017/03/22 14:33

念のためですが、ここでの質問は「C++の演算子のオーバーロードの悪いところってなんですか?」であって、「メンバ関数よりも常に非メンバ関数を目指すべきか」は別問題です。より深く議論したい場合は、質問を分けたほうがよいと思います。ここでこの議論をするのは、演算子オーバーロードについて興味を持ってこのページを見る人にとってノイズになります。
Chironian

2017/03/22 14:33

>>じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆で、 > この意見はちょっと極端すぎやしないかと思うわけです。必要な方を使えばいいのでは? に対して、極端に見えますが字面通りに受け取らない人も居ますよって言う情報提供です。 書き方が迂遠過ぎたかもしれません。申し訳ない。 ところで、「不純な動機」というものがなんなのか分かりませんが、michiru_cppさんにもyumetodoさんにも特に負の感情は持って無いですよ。
Chironian

2017/03/22 14:45 編集

faithandbraveさん、確かにそうですね。 michiru_cppさん、既に論点は出尽くしたと思います。私はこの辺で失礼したいと思います。 ところで私自身は「メンバ関数よりも常に非メンバ関数を目指すべきか」を議論している積りはないです。 既に書いたように適切な方を使えば良いという点で恐らくmichiru_cppさんと見解は同じです。 では失礼いたします。
退会済みユーザー

退会済みユーザー

2017/03/22 14:46

>faithandbraveさん そうでしたか、指摘ありがとうございます。 >basic_string以外の文字列(char配列や外部ライブラリの文字列)に対してもその関数が使えるのにそうしていない 一応認識は合っていたようでよかったです。 >ここでこの議論をするのは、演算子オーバーロードについて興味を持ってこのページを見る人にとってノイズになります。 「この意見はちょっと極端すぎやしないかと思うわけです。必要な方を使えばいいのでは?」 と書いた私のコメントもノイズですか。 >Chironianさん Twitterで陰口叩いているようですが、空気読めてなかったのは貴方ですよ。 「偉い人が言ってたんだから間違いないんだ!ケチつけるな!」と私に噛み付いてきたのは貴方です。 私は「デストラクタにはvirtualをつけろ」みたいな極論を初心者が真に受けているのを心配するのと同じ理由で、ちょっと「yumetodoさんに」釘を差したかっただけなのですが、 貴方が偉そうにマウンティングしようとしてきたので議論を始めざるを得なかったんです。 話をややこしくしたのは自分のせいもありますが。 「そのように考える人もいるのだな」くらいに軽く受け取ればよかったのに。 >適切な方が使えば良いという点で恐らくmichiru_cppさんと見解は同じです。 うん、だからなんで口挟んできたん? 権威を笠に着るプログラマって・・・・・・・ねぇ。 私もこれで失礼します。不純な動機で口を挟む人とは全く議論にならないので。 以前もこんなことありましたよね・・・ホント勘弁してください。
Chironian

2017/03/22 15:38 編集

1点看過できない誤解があるようなので情報提供しておきます。 > Twitterで陰口叩いているようですが、 もしかしてこれですか?→https://twitter.com/TheorideTech/status/844550313826648064 引用先を見て頂ければ分かりますが、これは「マサカリが飛んでくる」の語源の話ですよ。 マサカリは「間違いの指摘」くらいの意味で理解してたから、あのような負の意味があることに驚いたのです。
faithandbrave

2017/03/22 16:11 編集

第三者として意見させていただくと、yumetodoさんもChironianさんも、publicなTwitterのタイムラインでは(私が見えるところでは)、michiru_cppさんに対するものと思われる陰口は、一切見つけられませんでした。 Chironianさんの意見を、おそらく書籍の内容や、ボレロさん個人からの回答を鵜呑みにしているという理由から「権威を笠に着る」とおっしゃっているようですが、Chironianさんが「質問したり、ググッたり、文献を当たったりしてその真意を調べてみました。結果、私的には納得の行く回答に辿り着きました。」とおっしゃっているように、いろいろな意見を拾った上で自分なりの考えを得た結果を共有されています。 それと、この場合はmichiru_cppさんの方が、Chironianさんの名誉を傷つける発言をされていると思います。Twitterで陰口を叩いているというのは誤解であったとしても、「権威を笠に着るプログラマ」や「偉そうにマウンティング」というのは侮辱にあたりますのでご注意ください。
yumetodo

2017/03/22 22:17

あれぇ、話がおかしな方向に・・・。 >継承って重いんですか? 仮想関数のコストは多くの場合看過できないコストがかかります。 だからつい昨日もQiitaに http://qiita.com/Kogia_sima/items/6de2bb4423b5eb93639b なんていう、タグですディスパッチしようみたいな記事が投稿されるくらいで。 なんでもかんでも継承で書こうとするのは、少なくとも私の友人の間では、Java系出身者によく見られる行為ですが、 いいや、ちょっとまて、ここ、C++だから、Javaじゃねーから と思うわけです。(Javaの最近の動向は知りません。) > じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆で、 というのは確かによく聞く話ですし、私自身もそうすることでうまく行く例が多かったので(うえで出ているように粗結合になる)、最初に書いた時にScalaは全部メソッドだからいい、みたいな言説に対し、いや、C++では違うんよ、と言うことが言いたかっただけです。なんで権威主義とかいう話になっているのか、私の日本語力では理解できなかったのですが。 個別具体的な事例に対して、常にこの格言?が真だと言っているわけではありません。 >私は「デストラクタにはvirtualをつけろ」みたいな極論を初心者が真に受けているのを心配するのと同じ理由で、ちょっと「yumetodoさんに」釘を差したかっただけなのですが、 オブジェクト指向をまっとうに適用するには、仮想関数を使う必要もあるだろう、という程度の話です。
退会済みユーザー

退会済みユーザー

2017/03/23 01:34

>yumetodoさん 途中からコメ欄汚しになって申し訳ないです。 >あー、そういう前提でしたか・・・なるほど。 と書きましたが、そういうポリモーフィズムな話はしてないんです。 仮想関数のコストも私が書きましたし、継承が重いというのもこれまた誤解を招くのでつっこんだだけです(継承自体に仮想関数のコストは全く関係ありません)。 >タグですディスパッチしようみたいな記事が投稿されるくらいで。 確かfaithandbrave(高橋晶)さんとエピステーメー氏が書いた本(多分相当前です)に紹介されてたので、失礼ながら私は知ってます。 >いいや、ちょっとまて、ここ、C++だから、Javaじゃねーから そのお気持ちはよくわかります。 >なんで権威主義とかいう話になっているのか この辺は、Chironianさんとのやり取りでの話なので気にしないで下さい。 >オブジェクト指向をまっとうに適用するには、仮想関数を使う必要もあるだろう そういう話ではない、という説明のために例を書いたんですが・・・・ コメ欄汚しになったのは悪いと思いますが、最初の私のコメントから何故こんなに話がややこしくなるのか正直全く理解できません。 これで私が100%悪いことにされるのは納得いかないなぁ。 >Chironianさん >もしかしてこれですか? はい。陰口と断定したのは私が悪いかもしれませんが、タイムリーすぎて誤解せざるを得ませんでした。
Chironian

2017/03/24 00:56 編集

こんにちは。 昨日は一日所要で居なかったのですが、何が不味かったのか再度私の言動を見直してみました。 見直してみると私は大人気ないですね。しつこくしすぎました。 redstoneさん、yumetodoさん、折角の良いスレッドを無関係な話題で埋めてしまって申し訳ないです。 またfaithandbraveさん。折角割り込んで頂いたのに無にしてしまって申し訳ないです。 技術の話はしつこくて良いと思いますが、方針については人それぞれですので、しつこくするとダメですね。反省です。 最後にmichiru_cppさん。もう読まれていないと思いますが、退会されたのですね。遠くからですが今後のご活躍をお祈り申し上げます。
退会済みユーザー

退会済みユーザー

2017/03/24 19:10

>Chironianさん あのさぁ・・・・・ 「大人気なかった」とか、どう見ても謝ってるようには見えないですよ 私は「この意見はちょっと極端すぎやしないかと思うわけです。必要な方を使えばいいのでは?」と書いたけど、 この指摘は 「議 論 の 余 地 が 無 い」 事なの。 yumetodoさんにしてもそうだけど、「確かにそうですね」で終わってたはずの話。 私自身、大した指摘をしたつもりはない。だから柔らかく言ったんです。 それに対してあなたは代理戦争的に入ってきたの。 読み返したのにわからなかったの? 別に議論するのは構わないけど、正しいか間違ってるかを追求せずに、あなたは上から目線で講釈を垂れるばかりで、論破されても認めない。 (「書籍に書いてるから」、「ボレロ氏の言葉だから」、という考えからそうなってたのは分かるけど) 「オブジェクト指向的にメンバにすべき」だなどと私は一言も言ってないのに、勝手にそういう前提を作っていたでしょ? コストの無い(多態性不要な)クラスでも、広範に演算子を適用させるべきでない(オーバーロードか何かで結局分けなければならない)場合などザラにある、 (言い換えれば、「そういうクラスの演算子をどこに書くかについて、解決すべき問題など無い(=疎結合とか関係ない)」、あくまで私が出した例の場合。) こんな例をいちいち挙げざるを得なかったのは、あなたが私の主張を勝手に低く見積もったからです。 私の反論でそれにすぐ気付いたはずなのに、認めずに言い返してきたわけで。 >フリー関数とメンバ関数は私も適切と考える方を使えばOKと思います。 その上でこんなエクスキューズ入れるのは、明らかにケンカ売ってます。 私が怒るのも、「動機が不純」と言われるのも当然じゃないですかね。 「大人げなかった」とか「痛い目に合う人もいるかも知れませんね。」とか上から言ってますが、何様ですか。 確か私はあなたにコンパイル時条件分岐のやり方を教えたはず。(私と違って相当ここを利用してるようなので忘れてるだろうけど) 私も教えずに、あなたが痛い目に合うのを黙って見ていた方が良かったですかね? 頭にきて人格攻撃的な書き方をしたのは本当に悪かったです。それについては謝罪しておきます。 >faithandbraveさん 私も色んな本のお世話になったので技術系のライターはリスペクトしてたつもりなんですが、正直ちょっと幻滅しました。 というのも、 http://qiita.com/yumetodo/items/a843bd542106215bbc84 この記事に対して 「こういう言動がホントに初心者に迷惑かけてることに気づいてないんだろうな。敬遠されるような事をして悦に入ってるだけ。」 と突っ込んでる人を以前見かけました。 (というかyumetodoさんの記事であることにさっき気付いて吹いた・・・w) 最初はその記事を「まぁ冗談で言ってるだけだろう」と思ったので、そこまで言うこともないのでは・・・と思いましたが、 ここでのお三方の論調を見て確信しました。 まさにその人の指摘の通りです。 「言語というのは手段であって目的であってはならない」というのは、BjarneStroustrupもD&Eで言ってたはずです。 「侮辱にあたりますのでご注意ください。」という、私への警告だけで終わらせて良かったんですかね。理解不能です。 確かTwitterで「C++への風評被害を防ぐためにだけ回答する」と仰ってたと思いますが、やってることは真逆じゃないですか?? faithandbraveさんに反論する意味は無いだろうとも思いますが、説得力が足りないだろうから一応反論します。(前提が長くなって申し訳ないですが) >オブジェクト指向における「開放/閉鎖原則」をC++という言語に当てはめた場合の話だと思います。 私の出した例には当てはまらないですよね。私が誤解していると言いたかったようですが、認識としては合っていたはずだし、反論として成立してたはずです。 私が出した例の場合、想定していないタイプの数体系には対応できません。 対応しようとしても結局、「何のように振る舞うのか」は表明しなければいけません。 計算内容が全く違うので。 演算子を共通にしたければ、継承関係を持たせるか、SFINAEならテンプレートにして constexpr bool IsVector()とかIsScalar()とかIsQuaternion()とか使うんでしょうか? もうその時点で終わってます。 (それなら演算子を使わない方がマシです。VectorMultiply()とかQuaternionMultiply()とか。・・・あれあれ?w) ベクトルやら複素数やらの色んな数には色んな演算と色んな関数が有り得ますよね。 それらのサービスを提供して便利に(=ユーザーの開発コストを削減)しようとすればするほど、 無理な汎用化をすれば 「一番大変な部分の実装をユーザーに押し付ける」 ことになりませんか? この例の場合、想定してない数体系は、(ライブラリから&ユーザーから)使用されうる全ての演算&関数を自分で実装してもらうことになるんです。しかも直交してません、原理的に依存しまくりです。 こんなクラスの演算子を、クラスに対して疎結合にしてどうするんですか。いやマジで。 自分がやった限り、式テンプレートにして速度を追求しつつも「○○のように振る舞う型」を許可するまでが限度でした。 それ以上の無理な汎用化をするなら、使い物になりません。 一般的な数値演算ライブラリでも、組み込み型ではなくユーザーが作った数値クラスにまで対応できるものなど、見たことがありません。あなたは見たことがあるのかと。 (世界のどこかにはあるかも知れませんが、不便で開発効率の悪いライブラリだと思いますよ、必然的に。) あるんなら、「Chironianさんやyumetodoさんの主張は極端でない、お前が悪い」と言われてもわかりますが。 (まぁ、説明不足や用語に対する私の無理解で、Chironianさんやyumetodoさんにもわかりにくかったとは思いますが・・・) 「ちょっと極端では?」と言ったのは、一応経験上から出た言葉です。ナメないで頂きたい。 private云々の話もありましたが、STLやboostを見ても隠すべき(というか公開すると責任放棄になる)ものは隠してるでしょう。 Chironianさんに対する反論として、私の言ったことはおかしかったですか? もう一度言いますが、「どっちでもいいじゃん」。 一応Chironianさんとyumetodoさんに言っておきますが、「私もそう思いますよ」とか「同意します」とか言わないで下さいね。 だったら「確かにそうですね」で終わってたはずなんで。 他人(ScottMeyersとか)の主張を勝手に代弁する必要は無かったはずです。代理戦争は絶対ケンカになります。 ほんと何で「確かにそうですね」で終わってくれなかったのか・・・・。 (蛇足ですが、Modern C++ Design(和訳)でScott Meyersの推薦文を読んだとき、「あれ、この人こんなことも気付いてなかったんか・・・・」と思った部分があります。 言語の知識自体は当然私の方が今でも全然足りてないでしょうし、今Effective C++を読んでも勉強になるんでしょうが、C++専門のライターでさえエアポケット的に知らないことくらいあるでしょう。 盲信せずに、主体的に判断すべきでは?) ちなみに、 >ライブラリレイヤーとアプリケーションレイヤーでは設計の仕方が違いますし 私は小規模ながら、企業でゲームのレンダリングやシェーダ周りのライブラリを実務で作ってた人間(もう引退して個人ですが)ですので、あなたに言われるまでもありません。 >ライブラリレイヤーは再利用のための領域ですから、とくにそれを目指したほうがよいです。 さっき書いた内容からその先を言えば、最も汎用的なライブラリとは「何も書かないこと」でしょう。 「何を強制し、何を強制しないか」を(速度や効率・汎用性が必要か、開発コストがどれだけ掛けられるかなどのトレードオフで)選ぶのは、ライブラリ設計者が背負うべき責任というものです。 それを背負わず(ライブラリの)ユーザーに押し付けるのは、ただの給料泥棒です。 あと、faithandbraveさんは「技術書なんだから、『あらゆる状況に対応できる設計』を目指すのは当然」と仰りたかった部分もあるのだと思いますが 多くの初心者に、不要な「べき論」の連鎖を生み出したという点で、Scott Meyersには罪があるかもしれませんよ。 それを鵜呑みにした意見が、「極端じゃね?」と軽く突っ込まれて、「本に書いてあるんだから」「みんなが言ってるから(みんなって誰?)」という反論をしたのでは、同じ罪を何倍にもして、初心者に多大な悪影響を与えようとしていると言わざるを得ません。 言語は道具でしょう。好きに使えばいいのであって、あらゆる指針は生産性や効率を上げるためのポジティブなものでしょう。 ネガティブな「べき論」を重ねて自縄自縛に陥るのは、言語コミュニティの崩壊を招くだけだと思います。 (C++って、「知らない事がその人を不幸にしない」言語じゃなかったんですか?) 過去にJavaのC++への悪口に散々振り回されたのに、まさにミイラ取りがミイラです。 ほんと最初に挙げたQiitaの記事に突っ込んだ人の言う通りだと思います。 (失礼を承知で)幻滅した、と言った理由はそれです。 昔、「C++の継承は(Javaと比べて)汚いから、テンプレートで代用しよう」などという意味不明でアホな風潮が生まれましたよね。 私も当時まだ若かったので、そういう言説に困惑させられました(結局そういう手法にはならなかったですが。真似すると実現できないことだらけになると、さすがに気付いたので)。 騙された人(私のようなアホ含め)は一部だけかもしれませんが・・・・多くの初心者が誤解し、時間を無駄にしたと思います。違いますか? あのパターンは未だに続いてるんですね。 悲しいことです。 (誤解しないで欲しいのですが、質問者に対して私が何も書いてないのは、同意してるんじゃありません。) めちゃくちゃ長文になって申し訳ないですが・・・読んでいただけたとしたら幸いです。 ですが、コメントは結構です。お時間取らせるのもアレだし、私ももう面倒くさいので。
yumetodo

2017/03/25 05:19

http://qiita.com/yumetodo/items/a843bd542106215bbc84 の記事は、現代のC++があまりにも難解になっているのでおおよそ仕様をすべて把握し、C++の言語としての特性を、コーディングを実際にするユーザーがうまく利用して「楽をする」ことができなくなっているな、と思ったので、記事冒頭のツイートをきっかけに書いたものです。 >「こういう言動がホントに初心者に迷惑かけてることに気づいてないんだろうな。敬遠されるような事をして悦に入ってるだけ。」 というコメントは当時私も見ましたが、ある意味狙い通りです。 よっぽど記事中に引用しようかと思いましたが、無駄にview数が伸びてたので炎上しそうで、「抜き出すとまさかりが飛んできそうなので各自見てください。」と書くにとどめましたが、 このコメントを見てくれた人はどのくらいいたんだろうか。 だって、現代見かけるC++のコードを書くために覚えることがあまりにも多すぎるじゃないですか。 実際のところ私は継承もオーバーロードも理解しきれていませんし。 で、本来、プログラミング言語そのものを相当知らないと水準のコードが書けないというのはおかしいはずです。たかが道具なのに。 ところがC++の場合、言語仕様を半ば悪用したトリッキーなコードがデファクトスタンダードになっているがために、言語そのものを相当勉強しないといけない。 せめてコンパイルエラーがわかりやすければまだ良かったかもしれないがそれすらないからユーザーはライブラリの実装を読んで理解しないといけない(Conceptはよ)。 少なくとも私は、みなさんと違ってC++11から入門した人間ですが、当時江添さんの本の虫の記事を必死に読み漁り(もちろんググりながらなのでfaithandbraveさんはじめ多くの人の記事もよみました)、 やっとこのわけわかんないコートはどうしてこれ以上わかりやすくかけないのか、おぼろげに理解できるようになるまで約半年、本当に大変でした。なんせC++にはまともな入門本すらないですからね。 現状のC++の存続意義は過去の資産の活用と速度追求の2点に絞られます。速度を追求しないのであればほかにいくらでも適当な、かんたんな言語はあるはずです(C#とかRust?)。 逆に言えば速度追求のためならなんでも許される、そういう空気を私は感じています。これがますますC++を難解にさせている原因かもしれません。一番最初に >できることだけは非常に先端的だったりします。 という言葉を引用したように、C++は色々できてしまいますからね、なんでも。 [C++11]lambda、[C++11]ravlue reference、[C++14]constexpr、[C++17]template classの実引数推定や[はよこい]Conceptなど改善がないわけではないですが、 少なくとも私は今後輩にC++を勉強してみたら?とは言えません。実際JavaScriptとRustを勧めてます。 今のC++界隈の状況で、C++入門したら、無駄に時間が消し飛んで振り回されるだけです。まじで今のC++おかしいんじゃねーの?私はC++が好きだから使い続けるけど。 まあしかし、 >というかyumetodoさんの記事であることにさっき気付いて吹いた と言われる時点であの記事はちょっと広まりすぎたなぁ。 なんでああいうランク分けがC++において(のみ)成立して、またあれだけview数があって(さっき確認したら22458views!?) >対抗記事とか他言語版記事とか出ないかな(チラッ と煽ってなお他の言語版の記事が出ない理由は何なのかまでちゃんと伝わっているとは思えないし。 まあせめてあの記事を見てC++から離れた初心者がいれば、すこしは意味があったのかな、上述の内容が伝わらなくても。あの記事書いたときはそこまで思ってなかったけど。 --- >なおさら「しかし継承は重いからだめだ」という理由が出て来るのかよくわかりません。継承って重いんですか? に対して変化球的に仮想関数の話を返したために、 >継承自体に仮想関数のコストは全く関係ありません という返信をいただき、内心苦笑していました。 もちろん継承そのものじゃなくて仮想関数がコストがかかると言うのはもちろん承知していたのですが、 まっとうにオブジェクト指向すると仮想関数の使用が避けられない場合もあるよね、そのコストは速度追求の観点から許容されないこともある、 だからC++は他の言語と違ってフリー関数を多用するんだ 結果他の言語でオブジェクト指向するのとはちょっと違った形にC++では落ち着く ということを再確認すると同時に、そもそもの質問主様に >じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆 という自説を補強して、raccyさんが2017/03/20 12:24にかかれていることとほぼ同じ意図で釘を刺しておきたかっただけです。 一瞬全部メソッドでできないからC++は糞みたいな言説を見た気がしたので(それこそC++への風評被害なので)、 極端な言説を打ち消すには、同じく極端である「じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆」を持ち出すのが手っ取り早かったからです。 まあしかし、継承そのものが重いと誤解を与えてしまったのは意図するところではありませんでした。 --- >2017/03/21 20:41 >>じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆で、 >この意見はちょっと極端すぎやしないかと思うわけです。必要な方を使えばいいのでは? >2017/03/25 04:10 >一応Chironianさんとyumetodoさんに言っておきますが、「私もそう思いますよ」とか「同意します」とか言わないで下さいね。 >だったら「確かにそうですね」で終わってたはずなんで。 確かにそうですねですまなかったのは、2017/03/22 07:05時点ではどういう意図で「必要な方を使えばいい」を言っているのか理解できていなくて、 じゃあその「必要な方」ってなによ?となっていたからです。そのあとは「確かにそうですね」なんて言葉では収まらないくらい話が膨らんでいたので言わなかったわけですが。 >もう一度言いますが、「どっちでもいいじゃん」。 わたしの言説もおなじ思いから出発していたことが伝われば幸いです。全部メソッドでできないからC++は糞だなんて冷静に考えておかしいだろ、書きやすい方でかけばいいじゃん、そんなの。 傾向論として、迷ったときはC++ではフリー関数にするほうがスッキリすることが多いというだけで(すっきりしやすい理由はfaithandbraveさんが言及の通り)。 --- >一般的な数値演算ライブラリでも、組み込み型ではなくユーザーが作った数値クラスにまで対応できるものなど、見たことがありません。 個人的にはBoost.Geometryを見たときに、それに近い狂気を感じました。 --- >ですが、コメントは結構です。お時間取らせるのもアレだし、私ももう面倒くさいので。 あ・・・。これ見る前に返信を書き始めてしまった。うーん、せっかく書いたし投稿だけするか。
guest

0

有意義な議論ができて楽しかったです。
補足で少し調べたことがあるので、折角ですので簡単に書いておきます。

じゃあフリー関数が悪くてメソッドが正義かというと、実は真逆

これについて、Effective C++ 第3版の「23項 メンバ関数より、メンバでもfriendでもない関数を使おう」に解説がありました。
メンバ関数はprivateメンバにアクセスできる。ということは、そのメンバ関数はカプセル化を弱めてしまう。なので、privateメンバへのアクセスが必要ない関数は「メンバでもfriendでもない関数」を使おうと言う主旨と理解しました。

グローバル変数はスコープが広いから、無闇に使うのは良くないですね。
それと同様、privateメンバにアクセスできるメンバ関数もprivateメンバをアクセスできる場所を広げると言う意味でスコープを広げるので、無闇に使うのは良くないです。見落としがちですが大事な視点だなと思いました。(私も見落としてました。)

グローバル空間の汚染は悪か?

次に、フリー関数はグローバル空間を「汚染」するのか?という疑問があります。
C++の関数名はマングリングされているので、引数の型名もマングリングされた関数名に含まれます。
これは、名前空間名も同様です。
であれば、固有のクラスを引数に取る関数がグローバル空間を「汚染」しているのかと言うと、そうではないと思います。グローバル空間で当該クラスと無関係に作った識別子とぶつかる心配はありませんから。

int型や標準ライブラリで定義されている型を引数に取る「一般的な名称」のグローバル関数を定義することはグローバル空間の汚染に該当すると思います。汎用なライブラリで避けるべきはこちらですね。

投稿2017/03/21 03:49

Chironian

総合スコア23272

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

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

grovion

2017/03/21 14:26

同感です 僕もすごく楽しかったです こういう話ができるのはすごく有意義ですね それで、もうこの質問も終わりかな...と思っていたら、新しい方々からのご回答を いただいたので、どうやらこの議論はつづきそうですね
yumetodo

2017/03/21 22:09

>メンバ関数はprivateメンバにアクセスできる あーたしかに。 変更操作がどこから来るのかという可読性が減ると言われればまあ同意できる。
faithandbrave

2017/03/22 02:28

yohhoyさんも指摘されていましたが、演算子オーバーロードとグローバル空間の汚染というのは別な問題ではないでしょうか。演算子オーバーロードはユーザー定義型と同じ名前空間で定義しますので、演算子オーバーロードがグローバル名前空間で定義する何らかのルールや慣習などはないはずです。
Chironian

2017/03/22 09:01 編集

faithandbraveさん。 ああ、ごめんなさい。当初の疑問は解決しているし、省略してしまいました。すっとばしすぎですね。 質問主のredsotneさんの疑問は、演算子オーバーロード自体の得失と言うよりはscalaは良くてc++では悪い演算子オーバーロードって何?でした。つまり、http://d.hatena.ne.jp/xef/20130309/p1 の主張の根拠は一般的なのか?だと思います。 その結論は、Scalaは(1+a)のようなケースでも「メンバ関数」による演算子オーバーロードで対応できるが、C++は「フリー関数」で定義しないと無理だからでは?です。 つまり「フリー関数」での実装は「メンバ関数」での実装に劣るとの仮定があるわけです。 しかし、「フリー関数を使う方が良い」との見解や「friend関数はグローバルを汚さない」との見解がでたのでちょっと調べてみました。(friend関数自体は定義位置の名前空間に入るのでグローバル空間で定義されたクラス内で定義するとグローバル空間に入りますね。メンバ関数と異なりクラス・スコープではないですから。) フリー関数の方が本当に良いのか?→friendでないフリー関数のことだったので非該当。 下手に書くとグローバルを汚すフリー関数での実装は良くないのか?→普通に書いても事実上汚さない( ぶつからない)ので問題なし。 ならば、元のブログの主張は更に根拠に乏しいですねって話でした。
guest

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
myoon

総合スコア100

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

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

0

自分の中でホットな話題だったので、私見を述べさせていただきます。
自分は演算子オーバーロード反対派です。理由としては、「プログラム言語の元々の仕様」と「プログラマが定義した仕様」の境界が曖昧になる、というのが一番大きいです。

ここでいう「プログラム言語の元々の仕様」とは四則演算や変数への代入などのもともと言語の仕様のことを指します。一方、「ユーザーが定義した仕様」は、プログラム言語の機能を組み合わせて、プログラマがその組み合わせに意味を与えたものを指します。これは、クラスや関数が持っている役割です。

演算子オーバーロードはこの2つの分類を曖昧にします。例えばC++において、構造体Aに対する変数x,yが定義されているします。この時、以下のコード
x = y;
はこの部分を見ただけでは、「プログラム言語の元々の仕様」によって、ビットコピーされているのか、それとも演算子オーバーロードによって「ユーザーが定義した仕様」が呼び出されているのか分かりません。

私はこれこそが問題だと思っていて、この区別さえつけばよいと思ってます。
例えば、質問の背景にあったScalaのコード、
x add y
addがプログラム言語の仕様にない(予約語にない)ということが分かれば、「ユーザーが定義した仕様」だということが一目でわかります。なので、演算子オーバーロードを使うときは演算子の先頭に$を付けて、
$=, $+
みたいにできる言語があればいいのになー、とか思ってます。

自分の意見を述べるだけになってしまってすみません。今回質問に沿う形でまとめれば、私の意見は
「どちらの言語の仕様も、上記の2つを区別できないので、どちらも大差なく悪い」
ということになります。

投稿2017/03/21 10:04

teck124

総合スコア8

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

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

grovion

2017/03/21 13:58

teck124さんご回答感謝します。 `x = y;`の件よくわかります。確かにオーバーロードされいてるか、いないかいつもの気を配っていけない感じが負担なですよね > なので、演算子オーバーロードを使うときは演算子の先頭に$を付けて... これについては、プログラマにより賛否両論が別れるところになると思います。 なぜかというと、これは関数名についても同じように言えると思うからです。 つまり、「$」をつけることは以下のように言っているのと同じようなことだと思うのです。 自分で作った関数は、 myAdd() myMulti() にしたほうが、C++標準のなのか自分で作ったかはっきりするから「my」をつけるといいのになぁ。 「関数と演算子は違うよ」と思われるかもしれませんが、 yumetodaさんも言っていましたが > C++のoperator overloadとは、ちょっと特殊な関数に過ぎません。 (慣れなければ、コードを読むときに、演算子を見たときも関数と同じように頭を使う認識を変えるということかもしれません) それで、「$」をつける、「my」をつけるなどの話を進めていくと、 名前空間をつかってライブラリごとに区別を付くけたくなってくると思われます。 (ちょっとこれは大げさですね) こんな感じで、 complex::add(1, cmpx) complex::multi(cmpx, 3) // (C++でこういう記法ができるかは知りません) 1 complex::+ cmpx cmpx cmplex::* 3 or { use namespace complex; 1 + cmpx cmpx * 3 } リンクを貼れないのが残念ですが、 以前、yahooの知恵袋か、OkWaveかで、 関数のプレフィックスとして「my」をつけたらもっとわかりやすいのにという質問を 読んだことをあります。 これに対して、回答者は「僕たちは言語を拡張しているという認識でプログラムを書いている」というような内容を書いていました。(だから、標準ライブラリで使われいそうな関数名を使うことは普通のことだと言う内容だったかと思います) おそらくプログラマの認識の違いだと思います。 ------ 脱線 ------ でも、本当は、僕自体はteck124さんの意見にかなり賛成です。 やはり、初見でコードを見たときに、これ標準にあるやつ?それとも自分で作ったの?ってなることは すごく多いです。言語をよくしれば、段々と標準にあるものと、ないものの把握も進みますが、新しく学ぶ言語だと多々あります。あと、新しいライブラリを複数使ったりすると、参考に公式やブログなどみてても、所見が辛いことはよくありますよね。
faithandbrave

2017/03/21 14:10

記号の演算子に限らない中置記法を取り入れるのであれば、「$で始まらない演算子は将来の言語仕様のために予約する」のように決め打ちにしてしまうのはよいと思います。C++ではリテラル演算子がそのように制限されていて、アンダースコアで始まらないリテラル演算子は将来の標準のために予約されています。
yumetodo

2017/03/21 22:08

しかし、User Defiend Opertorとなるとますますコンパイラの実装が・・・。
Zuishin

2018/12/11 22:13

標準の演算子を使わないなら、演算子のオーバーロードでなく関数で事足りると思います。
guest

0

boostライブラリの一部に見られるような演算子オーバライドで成り立っているような物もあります。(「 | 」(OR演算子)をパイプとして使っているとか)
多くの人に直感的に受け入れられるものであれば良いのですが、そうでないようなものは混乱を招くだけなので、基本的には演算子のオーバーライドは慎重であるべきだと思います。

投稿2017/03/21 08:18

PineMatsu

総合スコア3579

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

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

faithandbrave

2017/03/21 13:37 編集

誤字 s/オーバーライド/オーバーロード/ この回答には、例示と根拠が不足していると感じました。まず、 PineMatsu さんが受け入れられると考えるものとしてどのようなものがあるか、受け入れられないと考えるものとしてどのようなものがあるかが不明瞭です。 それと、「多くの人に直感的に受け入れられる」ための直感とはどのようなもので、多くの人というのがどのような人の集合なのかが不明瞭です。たとえば直感というのが、「専門分野に対して知識を持たない非有識者から見て意味を理解できる」というものだとしたら、そんな条件に合致する演算子を定義するのは困難でしょう。類似の問題として可読性が議論されるときに軽視されやすいものとして、ドキュメントがあると思います。ドキュメントを読んで理解しやすい、設計論拠 (Design rationale)に賛同できる、というのも私は受け入れられる条件にできます。 「直感」とは曖昧なものだと思います。 私の意見は回答として別途投稿していますので、「オーバーロードに慎重になるべき」というのは同感です。しかし、「必要でないのにオーバーロードしている」というのが遊びではないコードでどれくらいあるのか疑問です。本当に必要になったからオーバーロードしているものは、それなりに理由があってやっているでしょう。
PineMatsu

2017/03/21 20:01

ささっと書いてしまったので書き間違いてましたね。訂正ありがとうございます。 確かに曖昧です。ただ、何となく生理的に受け入れがたいなと感じることってあると思います。その正体を見極めるのも大事かもしれませんが。 すべてが論理的に割り切れればいいのですが。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.37%

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

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

質問する

関連した質問