web-dev-qa-db-ja.com

演算子のオーバーロードに対する引数がわかりません

私は Joelの記事の1つ を読んだだけです。

一般的に、私はそれを認めなければなりません私は物事を隠す言語機能が少し怖いです。コードを見ると

_i = j * 5;
_

…Cでは、少なくともjは5倍され、結果はiに格納されます。

しかし、C++で同じコードスニペットが表示された場合、何もわかりません。何もない。 C++で実際に何が起こっているのかを知る唯一の方法は、iとjの型を見つけることです。それは、jが_operator*_をオーバーロードした型である可能性があり、乗算しようとするとひどく機知に富んだことを行うためです。

(Emphasis mine。)物事を隠す言語機能が怖いですか?どうして怖がるの? (abstractionとも呼ばれます)を非表示にすることは、オブジェクト指向プログラミングの重要なアイデアの1つではありませんか?メソッドa.foo(b)を呼び出すたびに、それが何をするのかわかりません。 abのタイプは何か、どこかで宣言される可能性があるものを見つける必要があります。では、オブジェクト指向プログラミングは、プログラマから隠しすぎているため、廃止する必要がありますか?

また、_j * 5_は、演算子のオーバーロードをサポートしない言語で記述する必要があるj.multiply(5)とどのように異なりますか?繰り返しますが、jのタイプを調べてmultiplyメソッド内を覗く必要があります。なぜなら、loおよびbeholdは、jmultiplyメソッドはひどく気の利いた何かをします。

「ムアハハ、私はメソッドにmultiplyという名前の邪悪なプログラマーですが、実際に行うことは完全に不明瞭で直感的ではなく、乗算を行うこととはまったく関係がありません。」それは、プログラミング言語を設計するときに考慮する必要があるシナリオですか?次に、誤解を招く可能性があるという理由で、プログラミング言語の識別子を破棄する必要があります。

メソッドの機能を知りたい場合は、ドキュメントを一瞥するか、実装内をのぞいてみてください。オペレーターのオーバーロードは単なる構文上の砂糖であり、それによってゲームがどのように変化するかはわかりません。

教えてください。

83
fredoverflow

抽象化はコードを「隠す」ので、内部の仕組みを気にする必要はありませんし、多くの場合それらを変更することもできませんが、意図はそれを見ないようにすることではありませんでした。オペレーターについての仮定を行うだけで、ジョエルが言ったように、それはどこにあってもかまいません。オーバーロードされたすべての演算子を特定の場所に確立する必要があるプログラミング機能があると、それを見つけるのに役立つ場合がありますが、それを使用する方が簡単かどうかはわかりません。

データを削除するGet_Some_Dataと呼ばれる関数よりも、乗算に似ていない何かをする*とは思わない。

32
JeffO

私見、オペレーターのオーバーロードなどの言語機能により、プログラマーはより強力になります。そして、誰もが知っているように、大きな力には大きな責任が伴います。より多くのパワーを与える機能は、足元で自分を撃つためのより多くの方法も提供し、明らかに、慎重に使用する必要があります。

たとえば、_+_または_*_の_class Matrix_または_class Complex_演算子をオーバーロードすることは完全に理にかなっています。誰もがそれが何を意味するかを即座に知るでしょう。一方、私にとって、_+_が文字列の連結を意味するという事実は、Javaが言語の一部としてこれを行い、STLが_std::string_演算子のオーバーロードを使用します。

演算子のオーバーロードによりコードがより明確になる別の良い例は、C++のスマートポインターです。スマートポインターをできるだけ通常のポインターのように動作させたいので、単項_*_および_->_演算子をオーバーロードすることは完全に理にかなっています。

本質的に、演算子のオーバーロードは、関数に名前を付けるための別の方法にすぎません。また、関数の命名規則があります。名前はわかりやすくする必要があり、関数の機能がすぐにわかります。同じ正確なルールが演算子のオーバーロードに適用されます。

19
Dima

Haskellでは、「+」、「-」、「*」、「/」などは単なる(中置)関数です。

「4 plus 2」のように、中置関数に「plus」という名前を付けますか?なぜそうしないのですか? 「プラス」関数に「+」という名前を付けますか?何故なの。

いわゆる「演算子」の問題は、ほとんどが数学演算に似ていて、それらを解釈する方法が多くないため、そのようなメソッド/関数/演算子が何をするかについて高い期待があると私は思います。

編集:私のポイントをより明確にしました

9

私が見た他の答えに基づいて、私は、オペレーターのオーバーロードに対する本当の異論は、すぐに明白なコードへの欲求であると結論付けることができるだけです。

これは2つの理由で悲劇的です:

  1. その論理的な結論を踏まえると、コードがすぐに明らかであるという原則は、私たち全員がまだCOBOLでコーディングしていることになります。
  2. すぐにわかるコードからは学べません。コードのしくみについて少し時間をかけて考えれば、意味のあるコードから学ぶことができます。
7
Larry Coleman

ややそう思う。

multiply(j,5)と書くと、jはスカラーまたは行列型になる可能性があり、jが何であるかに応じて、multiply()は多少複雑になります。ただし、オーバーロードの概念を完全に放棄する場合は、関数の名前をmultiply_scalar()またはmultiply_matrix()にする必要があります。

私たちの多くが一方的な方法を好むコードと、ほとんどの人がもう一方を好むコードがあります。ただし、ほとんどのコードは、この2つの極端な中間点にあります。あなたが何を好むかはあなたの背景と個人的な好みに依存します。

5
sbi

オペレーターの過負荷に関する2つの問題が発生しています。

  1. オーバーロードは、プログラマーが意図していない場合でも、オペレーターのセマンティクスを変更します。たとえば、&&||、または,をオーバーロードすると、これらの演算子の組み込みバリアント(および短絡動作)によって暗示されるシーケンスポイントが失われます論理演算子の)。このため、言語で許可されている場合でも、これらの演算子をオーバーロードしない方がよいでしょう。
  2. 一部の人々は、オペレーターのオーバーロードをこのような素晴らしい機能と見なし、適切なソリューションではない場合でも、どこでもそれを使用し始めます。これにより、他の人々は他の方向に過剰に反応し、オペレーターの過負荷の使用に対して警告します。私はどちらのグループにも同意しませんが、中立的な立場を取ります。演算子のオーバーロードは、次の場合にのみ控えめに使用する必要があります。
    • 過負荷のオペレーターは、ドメインの専門家とソフトウェアの専門家の両方にとって自然な意味を持っています。これら2つのグループがオペレーターの自然な意味に同意しない場合は、過負荷にしないでください。
    • 関係する型については、演算子に自然な意味はなく、直接のコンテキスト(同じ式が望ましいが、数行以内)は常に演算子の意味を明確にします。このカテゴリの例は、ストリームのoperator<<です。

私の個人的な経験に基づくと、Java複数のメソッドを許可する方法ですが、演算子のオーバーロードは許可していません。つまり、演算子を見るといつでも正確にが何をするかがわかります。

*が奇妙なコードを呼び出すかどうかを確認する必要はありませんが、それが乗算であることを知っており、Java言語仕様で定義されている方法とまったく同じように動作します。これは、プログラマーが定義したすべての改札を見つける代わりに、実際の動作に集中できます。

つまり、演算子の過負荷を禁止することはwriterではなくreaderにとってメリットであり、プログラムの保守が容易になります!

3
user1249

_a * b_のオーバーロードとmultiply(a,b)の呼び出しの違いの1つは、後者を簡単にgrepできることです。 multiply関数が異なる型に対してオーバーロードされていない場合は、abの型を追跡することなく、関数が何をするのかを正確に知ることができます。

Linus Torvaldsには、演算子のオーバーロードについて 興味深い引数 があります。 Linuxカーネル開発のように、ほとんどの変更がパッチを介して電子メールで送信される場合、各変更の前後の数行のコンテキストのみでパッチが何を行うかをメンテナーが理解できることが重要です。関数と演算子がオーバーロードされていない場合、変更されたファイルを調べてすべての型を調べ、オーバーロードされた演算子を確認する必要がないため、コンテキストに依存しない方法でパッチをより簡単に読み取ることができます。

3
Scott Wales

あなたがジョエルの隠蔽についての議論を嫌いだと私は完全に理解しています。私もダメ。組み込みの数値型のようなものや、たとえば行列のような独自のものに対しては、「+」を使用する方が実際にはるかに優れています。これは、 '。multiply()'の代わりに '*'を使用して2つの行列を乗算できるので、きちんとしていてエレガントです。結局、どちらの場合も同じ種類の抽象化が得られます。

ここで害になるのは、コードの読みやすさです。実際のケースでは、学術的な行列乗算の例ではありません。特にあなたの言語が最初に言語コアに存在しない演算子を定義することを許可する場合、例えば=:=。この時点で多くの追加の質問が発生します。あのいまいましいオペレーターは何ですか?そのことの優先順位は何ですか?関連性とは何ですか? a =:= b =:= c本当に実行されましたか?

これは、演算子のオーバーロードに対する反対論です。まだ納得できませんか?優先ルールを確認すると、10秒もかかりませんでしたか?では、さらに進みましょう。

演算子のオーバーロードを許可する言語(たとえば、名前が 'S'で始まる人気のある言語)を使い始めると、ライブラリデザイナーが演算子をオーバーライドすることを好むことがすぐにわかります。もちろん、それらは十分に教育されており、ベストプラクティスに従っています(ここでは皮肉なことはありません)。それらを個別に見ると、すべてのAPIは完全に意味をなします。

次に、1つのコードでオーバーロードする演算子を多用するいくつかのAPIを使用する必要があると想像してください。またはさらに良い-あなたはそのようないくつかのレガシーコードを読む必要があります。これは、オペレーターのオーバーロードが本当に厄介なときです。基本的に、1つの場所に多くのオーバーロードされた演算子がある場合、それらはすぐにプログラムコード内の他の英数字以外の文字と混ざり始めます。それらは、実際には演算子ではなく、ブロックやスコープのようなものを定義するいくつかのより基本的な言語文法要素である英数字以外の文字と混ざり合い、フロー制御ステートメントを形作り、またはメタものを示します。視覚的な混乱を理解するには、メガネを置き、目を10cm近づけてLCDディスプレイに近づける必要があります。

2
akosicki

それは期待を超えることと関係があるのではないかと思います。私はC++に慣れていて、言語によって完全に指示されない演算子の動作に慣れていて、演算子が奇妙なことをしても驚かないでしょう。その機能を持たない言語に慣れていて、C++コードを見ると、他の言語からの期待に沿うものであり、オーバーロードされた演算子が何かおかしなことをしていることに気付くと、驚くほど驚くかもしれません。

個人的には違いがあると思います。言語の組み込み構文の動作を変更できる場合、その理由はわかりにくくなります。メタプログラミングを許可しない言語は構文的には強力ではありませんが、概念的には理解が簡単です。

2
Joeri Sebrechts

数学演算子のオーバーロードは、C++でのオペレーターのオーバーロードの問題ではないと思います。式のコンテキスト(つまり、型)に依存してはならないオーバーロード演算子は「悪」だと思います。例えば。オーバーロード,[ ]( )->->*newdeleteまたは単項*。あなたは決して変更してはならないそれらのオペレーターからの一定の期待を持っています。

2
Allon Guralnek

一般に、直感的でない方法で演算子のオーバーロードを使用することは避けます。つまり、数値クラスがある場合、*のオーバーロードは許容されます(推奨されます)。しかし、もし私がクラスEmployeeを持っているなら、オーバーロードすることは何をしますか*つまり、直観的な方法で演算子をオーバーロードし、読みやすく理解しやすくします。

許容/推奨:

class Complex
{
public:
    double r;
    double i;

    Complex operator*(const Compex& rhs)
    {
        Complex result;
        result.r = (r * rhs.r) - (i * rhs.i);
        result.i = (r * rhs.i) + (i * rhs.r);
        return result;
    }
};

受け付けできません:

class Employee
{
public:
    std::string name;
    std::string address;
    std::string phone_number;

    Employee operator* (const Employee& e)
    {
        // what the hell do I do here??
    }
};
1
Zac Howland

スペルアウトされたメソッドと比較すると、演算子は短くなりますが、括弧は不要です。括弧はタイプするのに比較的不便です。そして、あなたはそれらのバランスをとる必要があります。全体として、メソッド呼び出しには、演算子と比較して3文字のプレーンノイズが必要です。このため、演算子の使用は非常に魅力的です。
なぜ他の人もこれをしたいのですか:cout << "Hello world"

オーバーロードの問題は、ほとんどのプログラマーが信じられないほど怠惰であり、ほとんどのプログラマーがそうする余裕がないということです。

C++プログラマーをオペレーターのオーバーロードの悪用に駆り立てているのは、その存在ではなく、メソッド呼び出しを実行するためのより適切な方法がないことです。そして、人々はそれが可能であるだけでなく、それが行われているので、オペレーターのオーバーロードを恐れているだけではありません。
たとえば、RubyとScalaでは、演算子のオーバーロードを恐れていないことに注意してください。演算子の使用はメソッドよりも実際には短くないという事実は別として、Ruby は演算子のオーバーロード を賢明な最小値、Scala を使用すると、独自の演算子 を宣言できるため、衝突を簡単に回避できます。

1
back2dos

ここですでに述べたことに加えて、演算子のオーバーロードに対するもう1つの議論があります。実際、_+_と書けば、何かを何かに追加することを意味するのは明らかです。しかし、これは常に当てはまるわけではありません。

C++自体は、そのようなケースの素晴らしい例を提供します。 _stream << 1_はどのように読み取られることになっていますか?ストリームは左に1シフトしましたか? C++の<<もストリームに書き込むことを明示的に知らない限り、これはまったく明白ではありません。ただし、この操作がメソッドとして実装された場合、正しい開発者はo.leftShift(1)を作成せず、o.write(1)のようになります。

要するに、オペレーターのオーバーロードを使用不可にすることで、この言語はプログラマーに操作の名前を考えさせます。選択した名前が完全ではない場合でも、記号よりも名前を誤って解釈することは困難です。

1
Malcolm

演算子のオーバーロードが怖いのは、_*_が単に「乗算」を意味するのではなく、foo.multiply(bar)のようなメソッドが少なくとも即座に考えられないプログラマが多数いるためですそのプログラマーが誰かがカスタムの乗算メソッドを書いたことを指摘します。その時点で彼らはなぜだろうと思い、調査に行くでしょう。

私は2つの引数を取り、一方から他方に値を適用してブール値を返す「CompareValues」と呼ばれるメソッドを作成する高位の立場にある「優れたプログラマ」と協力してきました。または、他の3つのオブジェクトのデータベースに移動し、値を取得し、計算を行い、thisを変更してデータベースに保存する「LoadTheValues」というメソッド。

私がそのようなタイプのプログラマーと一緒にチームで作業している場合、私は彼らが取り組んだことを調査することを即座に知っています。彼らが演算子をオーバーロードした場合、私は彼らがそうしたと思い込んで見に行く以外に、彼らがそれをしたことを知る方法はまったくありません。

完璧な世界、または完璧なプログラマーがいるチームでは、演算子のオーバーロードはおそらく素晴らしいツールです。私はまだ完璧なプログラマーのチームに取り組んでいないので、それが怖いのはそのためです。

0
James P. Wright