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

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

ただいまの
回答率

89.65%

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

解決済

回答 10

投稿 編集

  • 評価
  • クリップ 13
  • VIEW 13K+

grovion

score 143

 質問内容

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • ikedas

    2017/03/19 20:19

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

    キャンセル

  • Chironian

    2017/03/21 12:17 編集

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

    キャンセル

  • grovion

    2017/03/24 12:23

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

    キャンセル

回答 10

checkベストアンサー

+13

オーバーロードは本来、意味論が同じものをまとめるためにあります。そういう理由で、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 23: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に近いんですかね?

    マクロ以外の実装だと、変数展開するとかですかね?




    キャンセル

  • 2017/03/21 23: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++テンプレートテクニック』というものもあります。

    キャンセル

  • 2017/03/21 23:49

    faithandbraveさん、ご丁寧にご回答ありがとうございます

    > オーバーロードして定義したoperator||とoperator&&は短絡評価されません。
    ということは、左項も右項も両方共評価されてしまうんですね。理解できました。
    C++が、遅延評価や名前渡しなどに対応すれば、これもハンドリングできるということになるってことですよね

    Boost.Metaparseについての情報ありがとうございます
    こういうことは詳しい方から聞けてとてもありがたいです

    キャンセル

  • 2017/03/21 23:56

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

    キャンセル

+7

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

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


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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/03/21 09:23

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

    キャンセル

  • 2017/03/22 01:30 編集

    > 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

    キャンセル

  • 2017/03/22 03:08 編集

    > 勝手な印象ですが、関数型が混じったScalaや純粋関数型のHaskellなどのプログラマさんたちのマナーは、
    > いい感じですが、C++を書く人たちは色々で、命名規則も、グローバル変数の扱いも、名前空間の汚染も、
    > 想定しにくい副作用も...、気にせず、動けば良いコードを書いている方が多い気もします。

    人口の違いによるものではないでしょうか。人が多ければそれだけ多様なコードが出てきます。コーディングスタイルがプロジェクトごと、というのは今では少数派な言語だとは思いますが、既存の資産があるところにあとからルール追加はできないでしょう。ただし、最近ではC++ Core GuidelinesというのがC++標準化委員会によって作られています。
    特定の言語だからマナーのいいプログラマが多い、というのはまだ優秀な人(アーリーアダプター)しかユーザーが付いていないと考えられる気がします。Javaだからオブジェクト指向でちゃんと設計してるぞ、ということはなく、ひとつのクラスしかない巨大なプロジェクトなんていうのもあります。

    キャンセル

+3

こんにちは。

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

以下は極端な例です。

int operator+(int lhs, int rhs)
{
    return lhs * rhs;
}

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/03/20 01:11 編集

    基本型(fundamental type)とは、言語のコア仕様で定義された型のことを呼びました。C++ではそのように呼ばれることが多いようです。同様な型はどんな言語でも必ず存在します。

    Scalaの場合、基本型もクラスとしての特性を持っているということですね。
    そして、基本型の演算子オーバーロードができると言うことは、ユーザが基本型の定義を変更できてしまうということですね。例えば、int型へdoubleを代入する演算子があったとして、デフォルトが切り捨てだったものを四捨五入等へ定義変更できるわけですね。
    なるほど、C++の演算子オーバーロードより強い誘惑を受けそうです。
    つまり、基本型と言えども、定義が変更されていないことを一々チェックしないと、数式一つ読めないのですね。正直、勘弁して欲しいかも。
    これに対して、C++の演算子オーバーロードは元々ないものを定義するだけですから、ないはずのものが使われている時だけチェックすれば良いのでかなり楽ですね。

    > フリーな演算子というのは、自分で好きな記号を演算子にできるということですか?

    優先順位も含めて自由に定義できる演算子という意味です。
    例えば$とか、今使ってないやつを割り当てたら便利かもとちょっと思いました。(ふと思っただけですのであまり突っ込まないで頂けると有り難いです。)

    一貫させ過ぎると良くないはその通りと思います。異なるものを同じ表現にすると混乱しますから。
    (JavaやC#の参照(≒ポインタ)の代入と値の代入が同じ表現って本当に嫌です。どちらかを:=などにしてくれれば良かったのに。)
    Scalaは配列アクセスは関数呼び出しと同じ表現なのですか。 基本型の演算子オーバーロードもそうですが、やはり一貫させすぎは良くないですね。

    キャンセル

  • 2017/03/20 09:39

    Chironianさん、何度も何度もお返事とても感謝しています

    ちょっとよくわからないところがあって....

    > 例えば、int型へdoubleを代入する演算子があったとして、デフォルトが切り捨てだったものを四捨五入等へ定義変更できるわけですね。

    int型へdoubleを代入する演算子って、代入演算子「=」ってことですか?

    あと、
    > そして、基本型の演算子オーバーロードができると言うことは、ユーザが基本型の定義を変更できてしまうということですね。
    解釈ミスをしている可能性がありそうですが、
    Intの「+」やDoubleの「/」などの挙動は変更できないと思いますので、定義の変更はできないと思います。
    「基本型の定義を変更できてしまう」の意味がまだ良くわかりません

    例えばRubyならオープンクラスなので、自由にメソッドの上書きができますよね。
    例えば、以下の例だと小さい整数の足し算が必ず、10になってしまうみたいなことできちゃいますよね
    class Fixnum
    def +(that)
    10
    end
    end

    1 + 1 // => 10
    2 + 17 // => 10
    こんな上書きはScalaはできないはずです。

    キャンセル

  • 2017/03/20 13:37 編集

    > int型へdoubleを代入する演算子って、代入演算子「=」ってことですか?

    その通りです。

    > Intの「+」やDoubleの「/」などの挙動は変更できないと思いますので、定義の変更はできないと思います。

    おっとそうですか。それができたらダメですよね。
    その点では「一貫」していませんが、ここまで一貫するのは一貫し過ぎですので、好ましいと思います。

    raccyさんの回答でのコメント群をみて思いました。
    Scalaは演算子型のメソッド定義がC++より自由度が高くLhsを指定できるということですね。

    もし、def -(lhs: Int)とdef -(rhs: Int)みたいなオーバーロードが可能であれは、C++のように前者をフリー関数にしなければならないことより望ましい一貫性があるのかも知れません。その分、構文が複雑になるので実際の構文を見てみないことには分かりませんが。

    【追記】
    Scalaを知らないので、def +(other: ComplexInt)を勘違いしてました。raccyさんの回答につけたコメントを参照して下さい。

    キャンセル

+3

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/03/19 23:09

    > 念のため最新版を見ると、160度程見解が変わっているようです。

    ご指摘ありがとうございます。
    一応そちら(の日本語訳)にも目を通して違いには気づいていたのですが、ブログの日付が2013年だったので、時系列を考えて古い方を出しました。

    キャンセル

  • 2017/03/19 23:12

    catsforepawさん、ありがとうございます

    なるほど、そこまで考慮してということでしたか、すごいです

    キャンセル

  • 2017/03/19 23:48

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

    キャンセル

+2

Chironianさんのコメントからの結論

#include <iostream>
class ComplexInt
{
private:
    int real;
    int imag;

public:
    ComplexInt(int real, int imag = 0) : real(real), imag(imag) {}
    friend ComplexInt operator+(const ComplexInt &x, const ComplexInt &y)
    {
        return ComplexInt(x.real + y.real, x.imag + y.imag);
    }
    friend std::ostream &operator<<(std::ostream &os, const ComplexInt &ci)
    {
        os << ci.real;
        if (ci.imag >= 0) {
            os << "+";
        }
        os << ci.imag << "i";
        return os;
    }
};


int main()
{
    const ComplexInt a(2, 3);
    const ComplexInt b(1, -5);
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    std::cout << (a + b) << std::endl;
    std::cout << (a + 1) << std::endl;
    std::cout << (1 + a) << std::endl;
    return 0;
}

何も問題なし、以上。

以下、駄文。


C++がだめな理由を考えました。


サンプルとして、整数しか扱えなくて足し算しかできない複素数のクラスを作ってみました。

#include <iostream>
class ComplexInt
{
private:
    int real;
    int imag;

public:
    ComplexInt(int real, int imag) : real(real), imag(imag) {}
    ComplexInt operator+(const ComplexInt &other) const
    {
        return ComplexInt(real + other.real, imag + other.imag);
    }
    ComplexInt operator+(int other) const
    {
        return ComplexInt(real + other, imag);
    }
    std::string toString() const
    {
        std::string str("");
        str += std::to_string(real);
        if (imag >= 0) {
            str += "+";
        }
        str += std::to_string(imag) += "i";
        return str;
    }
    int getReal() const { return real; }
    int getImag() const { return imag; }
};

std::ostream &operator<<(std::ostream &os, const ComplexInt &ci)
{
    os << ci.toString();
    return os;
}
ComplexInt operator+(int x, const ComplexInt &y)
{
    return ComplexInt(x, 0) + y;
}

int main()
{
    const ComplexInt a(2, 3);
    const ComplexInt b(1, -5);
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    std::cout << (a + b) << std::endl;
    std::cout << (a + 1) << std::endl;
    std::cout << (1 + a) << std::endl;
    return 0;
}

Scalaは演算子がメソッドだ!と言っていますが、C++も演算子はメソッド相当であるメンバー関数として定義できます。その部分に違いはありません。しかし、メンバー関数として定義した場合は、1 + aのような整数が前に来るような足し算には対応できません。そのため、ComplexInt operator+(int x, const ComplexInt &y)というグローバル関数を別途定義する必要が出てきます。

グローバル空間を汚すことは行儀が悪いコードと言われます(つまり、Cだと関数一個も作れないと言うこと)。しかし、C++の演算子オーバーロードではグローバル関数として定義せざるをえない場合があります※。

※ 名前空間は汚す必要ない!って話なので、名前空間を汚さないの後から追記しています。

対して、Scalaでは左辺も含めた暗黙の型変換を定義できるため、次のようになります。

class ComplexInt(val real: Int, val imag: Int) {
  override def toString =
    real + (if (imag >= 0) "+" else "") + imag + "i"
  def +(other: ComplexInt) =
    new ComplexInt(real + other.real, imag + other.imag)
}

object ComplexInt {
  implicit def intToCemplexInt(i: Int) = new ComplexInt(i, 0)
}

object Main {
  def main(args: Array[String]) {
    val a = new ComplexInt(2, 3)
    val b = new ComplexInt(1, -5)
    println(a)
    println(b)
    println(a + b)
    println(a + 1)
    println(1 + a)
  }
}

+の定義が統一できるし、型変換も一つの型に一つのメソッドだけです。が、暗黙の型変換はオススメしない機能らしく警告が出ます。そこで、次のように書き換えます。

class ComplexInt(val real: Int, val imag: Int) {
  override def toString =
    real + (if (imag >= 0) "+" else "") + imag + "i"
  def +(other: ComplexInt) =
    new ComplexInt(real + other.real, imag + other.imag)
}

object ComplexInt {
  implicit class ComplexIntReal(real: Int) extends ComplexInt(real, 0) {}
}

object Main {
  def main(args: Array[String]) {
    val a = new ComplexInt(2, 3)
    val b = new ComplexInt(1, -5)
    println(a)
    println(b)
    println(a + b)
    println(a + 1)
    println(1 + a)
  }
}

ぱっと見、C++とは違ってグローバル関数を使ってないですので、グローバルを汚すと言うことはしていないように思えます。しかし、暗黙の型変換も暗黙のクラスもグローバルに影響を与えています。どっちにしろグローバルを汚します。ただ、足し算という処理は一つにまとめることができますので、C++よりはマシであると言えるのではないでしょうか。

おまけで、Rubyを見てみましょう。

# frozen_string_literal: true
class ComplexInt
  attr_reader :real, :imag
  def initialize(real, imag)
    @real = real.to_i
    @imag = imag.to_i
  end

  def to_s
    "#{@real}#{@imag >= 0 ? '+' : ''}#{@imag}i"
  end

  def coerce(other)
    if other.is_a?(Integer)
      [ComplexInt.new(other, 0), self]
    else
      super
    end
  end

  def +(other)
    case other
    when ComplexInt
      ComplexInt.new(@real + other.real, @imag + other.imag)
    when Integer
      ComplexInt.new(@real + other, @imag)
    else
      x, y = a.coerce(self)
      x + y
    end
  end
end

if $0 == __FILE__
  a = ComplexInt.new(2, 3)
  b = ComplexInt.new(1, -5)
  puts a
  puts b
  puts a + b
  puts a + 1
  puts 1 + a
end

RubyにはNumeric#coerceという仕組みがあります。これは、自分が知らない型なら相手のcoerceを呼び出して型をあわせようとする動作です。Numeric、つまり、数値をあらわす型のみですが、この仕組みにより、任意の数値のような型を追加することができます。実際にBigDecimal(任意精度十進数小数点数)やMatrix(行列)ではこの仕組みを使って、通常のInteger(整数)と足し算などができるようになっています。

この仕組みの利点は、グローバルを汚さないということです。他の型と動作が変わったわけではありません。しかし、欠点として、もともとの定義がcoerceを考慮していなければなりません。実質使えるのは数値のみです。


名前空間を汚さなくても大丈夫だそうです。

#include <iostream>
namespace ci {
    class ComplexInt
    {
    private:
        int real;
        int imag;

    public:
        ComplexInt(int real, int imag) : real(real), imag(imag) {}
        ComplexInt operator+(const ComplexInt &other) const
        {
            return ComplexInt(real + other.real, imag + other.imag);
        }
        ComplexInt operator+(int other) const
        {
            return ComplexInt(real + other, imag);
        }
        std::string toString() const
        {
            std::string str("");
            str += std::to_string(real);
            if (imag >= 0) {
                str += "+";
            }
            str += std::to_string(imag) += "i";
            return str;
        }
        int getReal() const { return real; }
        int getImag() const { return imag; }
    };

    std::ostream &operator<<(std::ostream &os, const ComplexInt &ci)
    {
        os << ci.toString();
        return os;
    }
    ComplexInt operator+(int x, const ComplexInt &y)
    {
        return ComplexInt(x, 0) + y;
    }
}

int main()
{
    const ci::ComplexInt a(2, 3);
    const ci::ComplexInt b(1, -5);
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    std::cout << (a + b) << std::endl;
    std::cout << (a + 1) << std::endl;
    std::cout << (1 + a) << std::endl;
    return 0;
}

では、これで一安心だねっていうかと思うと、ちょっと使っただけで、グローバルに動作を影響を与えています。あ、Scalaと暗黙の型変換を検索するところと検索の仕方だけは同じ所に戻っただけのようです。


ぶっちゃけ、C++とScalaでは、単にScalaは暗黙の型変換とか使えば、実装が少なくて済むので楽だよね、程度しか無いと思います。ただ、C++の方が細かく実装できる分、コストがー、コストがー、と言っている深淵を覗くことが大好きな人達には好まれるでしょう。

C++で何か嫌だと思う点の一つは、メンバー関数として実装できないことだと思います※。オブジェクト指向原理主義者からみると、メソッドでない奴は駄目な奴だと勘違いしているのかも知れません。Scalaの暗黙の型変換もさほど変わらないと思うのですが、objectのメソッドだからいいとか言い出すのでしょうか…。

※ friendsだとできるそうです。追記しています。

あと、たぶん、何か嫌だと思ってしまうのは、標準型に新たなメンバー関数を追加しているように見えると言うことではないでしょうか。じゃあ、ScalaやRubyはどうなのかというと、確かに、元々の標準型に何かを追加している訳ではありませんが、一歩離れれば、やっていることはさほどかわりありません。やり方が違うだけで目くじら立てるのはどうなのかとは思ってしまいます。

最後にHaskellを見てみましょう。Haskellのa + b(+)(a)(b)です。他の関数と同じようにオーバーロードできます。オブジェクト指向なんかにして、無理にメソッドとかメンバー関数とか、そんなことをするより、全部関数にしておけば良かったのかも知れません。


君はC++が得意なフレンズなんだね。

#include <iostream>
class ComplexInt
{
private:
    int real;
    int imag;

public:
    ComplexInt(int real, int imag) : real(real), imag(imag) {}
    ComplexInt operator+(const ComplexInt &other) const
    {
        return ComplexInt(real + other.real, imag + other.imag);
    }
    ComplexInt operator+(int other) const
    {
        return ComplexInt(real + other, imag);
    }
    std::string toString() const
    {
        std::string str("");
        str += std::to_string(real);
        if (imag >= 0) {
            str += "+";
        }
        str += std::to_string(imag) += "i";
        return str;
    }
    int getReal() const { return real; }
    int getImag() const { return imag; }
    friend std::ostream &operator<<(std::ostream &os, const ComplexInt &ci)
    {
        os << ci.toString();
        return os;
    }
    friend ComplexInt operator+(int x, const ComplexInt &y)
    {
        return ComplexInt(x, 0) + y;
    }
};


int main()
{
    const ComplexInt a(2, 3);
    const ComplexInt b(1, -5);
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    std::cout << (a + b) << std::endl;
    std::cout << (a + 1) << std::endl;
    std::cout << (1 + a) << std::endl;
    return 0;
}

メンバー関数にできたけど、これこれで、ありなんだそうです。C++書くのつかれた。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/03/20 14:08

    > 僕が勘違いしているだけかもしれませんが、たぶん両方共a - 1に解釈されないとおもいますよ
    以下のideoneでじっこうした結果があるので、良かったらみてみてください
    https://ideone.com/U8X7G2

    うわっ、本当ですね。なんか勘違いしてました。
    ということはC++とほとんど差はないと思います。

    > 一箇所にまとめられるという意味ですか?

    一箇所にまとめて書けると言う意味です。1つにまとめることができると言う意味ではないです。
    でも、フレンド関数+暗黙の型変換を使えば1つで済みました。(raccyさんのソース流用)
    http://melpon.org/wandbox/permlink/PvZWTA2JXg1emLCQ

    なるほど、ブログの筆者は「フリー関数」ではなく「メソッド」で書けるから「殺さなくてよい」と言っているのですね。やっと理解できました。この2つがそこまで大きな差とは思えませんが、人によってはそのように感じるのかも知れないですね。

    キャンセル

  • 2017/03/20 14:17

    Chironianさん本当にお世話になってます

    以下のコード読みました
    Scalaと同じくC++でも暗黙的にintをComplexIntに変換できてますね
    http://melpon.org/wandbox/permlink/PvZWTA2JXg1emLCQ

    グローバルについての汚染もなさそうで、すごくうまくいってます

    キャンセル

  • 2017/03/20 16:07 編集

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

    キャンセル

+2

個人的には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

#include "../include/string_split.hpp"
#include <iostream>
int main()
{
    std::string s = "arikitari na world!";
    const auto s_1 = s | split(' ')[1];//"na"
    std::string s2 = "123,421,113";
    const auto s_2 = s2 | split(',') >> [](const std::string& s) {
        return std::stoi(s);
    };//[123,421,113]
    s2 | split(',') >> [](std::string&& s) {
        std::cout << s << std::endl;
    };
    /*stdout:
    123
    421
    113
    */
    return 0;
}

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/24 02:23 編集

    こんにちは。

    昨日は一日所要で居なかったのですが、何が不味かったのか再度私の言動を見直してみました。
    見直してみると私は大人気ないですね。しつこくしすぎました。
    redstoneさん、yumetodoさん、折角の良いスレッドを無関係な話題で埋めてしまって申し訳ないです。
    またfaithandbraveさん。折角割り込んで頂いたのに無にしてしまって申し訳ないです。

    技術の話はしつこくて良いと思いますが、方針については人それぞれですので、しつこくするとダメですね。反省です。

    最後にmichiru_cppさん。もう読まれていないと思いますが、退会されたのですね。遠くからですが今後のご活躍をお祈り申し上げます。

    キャンセル

  • 2017/03/25 04: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と比べて)汚いから、テンプレートで代用しよう」などという意味不明でアホな風潮が生まれましたよね。
    私も当時まだ若かったので、そういう言説に困惑させられました(結局そういう手法にはならなかったですが。真似すると実現できないことだらけになると、さすがに気付いたので)。
    騙された人(私のようなアホ含め)は一部だけかもしれませんが・・・・多くの初心者が誤解し、時間を無駄にしたと思います。違いますか?
    あのパターンは未だに続いてるんですね。
    悲しいことです。

    (誤解しないで欲しいのですが、質問者に対して私が何も書いてないのは、同意してるんじゃありません。)


    めちゃくちゃ長文になって申し訳ないですが・・・読んでいただけたとしたら幸いです。
    ですが、コメントは結構です。お時間取らせるのもアレだし、私ももう面倒くさいので。

    キャンセル

  • 2017/03/25 14: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を見たときに、それに近い狂気を感じました。

    ---

    >ですが、コメントは結構です。お時間取らせるのもアレだし、私ももう面倒くさいので。

    あ・・・。これ見る前に返信を書き始めてしまった。うーん、せっかく書いたし投稿だけするか。

    キャンセル

+1

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

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

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

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

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/03/21 23:26

    同感です 僕もすごく楽しかったです
    こういう話ができるのはすごく有意義ですね

    それで、もうこの質問も終わりかな...と思っていたら、新しい方々からのご回答を
    いただいたので、どうやらこの議論はつづきそうですね

    キャンセル

  • 2017/03/22 07:09

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

    キャンセル

  • 2017/03/22 11:28

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

    キャンセル

  • 2017/03/22 13:46 編集

    faithandbraveさん。

    ああ、ごめんなさい。当初の疑問は解決しているし、省略してしまいました。すっとばしすぎですね。

    質問主のredsotneさんの疑問は、演算子オーバーロード自体の得失と言うよりはscalaは良くてc++では悪い演算子オーバーロードって何?でした。つまり、http://d.hatena.ne.jp/xef/20130309/p1 の主張の根拠は一般的なのか?だと思います。

    その結論は、Scalaは(1+a)のようなケースでも「メンバ関数」による演算子オーバーロードで対応できるが、C++は「フリー関数」で定義しないと無理だからでは?です。
    つまり「フリー関数」での実装は「メンバ関数」での実装に劣るとの仮定があるわけです。

    しかし、「フリー関数を使う方が良い」との見解や「friend関数はグローバルを汚さない」との見解がでたのでちょっと調べてみました。(friend関数自体は定義位置の名前空間に入るのでグローバル空間で定義されたクラス内で定義するとグローバル空間に入りますね。メンバ関数と異なりクラス・スコープではないですから。)

    フリー関数の方が本当に良いのか?→friendでないフリー関数のことだったので非該当。
    下手に書くとグローバルを汚すフリー関数での実装は良くないのか?→普通に書いても事実上汚さない(
    ぶつからない)ので問題なし。
    ならば、元のブログの主張は更に根拠に乏しいですねって話でした。

    キャンセル

0

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/03/21 22:35 編集

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

    私の意見は回答として別途投稿していますので、「オーバーロードに慎重になるべき」というのは同感です。しかし、「必要でないのにオーバーロードしている」というのが遊びではないコードでどれくらいあるのか疑問です。本当に必要になったからオーバーロードしているものは、それなりに理由があってやっているでしょう。

    キャンセル

  • 2017/03/22 05:01

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

    キャンセル

0

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

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

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/03/21 22: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さんの意見にかなり賛成です。
    やはり、初見でコードを見たときに、これ標準にあるやつ?それとも自分で作ったの?ってなることは
    すごく多いです。言語をよくしれば、段々と標準にあるものと、ないものの把握も進みますが、新しく学ぶ言語だと多々あります。あと、新しいライブラリを複数使ったりすると、参考に公式やブログなどみてても、所見が辛いことはよくありますよね。

    キャンセル

  • 2017/03/21 23:10

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

    キャンセル

  • 2017/03/22 07:08

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

    キャンセル

  • 2018/12/12 07:13

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

    キャンセル

0

C++はよく知らないのでこんな難しい議論を完全に理解はできないですが。
C言語では、私は(値が0,1のどちらかとは限らない)変数xをブール値に返還するとき !!x と書いたことが多かったです(整数型やポインタ型に対して)が、C++ではせっかくboolが使えるのに、コンパイラは「「!」がオーバーロードされているかもしれない」と最適化してくれなさそうなところが気がかりです。

削除したかったのですが、削除できない仕様だそうなので。

!!x ではなく x != 0 がよい書き方ですね。xがbool型なら x != false 、xがポインタ型なら x != nullptr でしょうが。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

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

  • ただいまの回答率 89.65%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る