3

インクリメント演算子とデクリメント演算子の再バージョンとポストバージョンを個別にオーバーロード可能にする理由を見つけようとしています。
私の考えでは、そして私がこれまでに見たあらゆるタイプのクラスのこれらの演算子のすべての実装において、これらは同じ演算子 (= 同じことを行う) であり、呼び出されるタイミングが異なるだけです。C++ の設計者が1 つの演算子を持っていて、値を読み取る前または後に (または、より可能性が高いのは、前または次のシーケンス ポイントで) 必要に応じてコンパイラがそれを呼び出すことは、
私にははるかに論理的に思えます。同等だと思います) ++

したがって、質問は次のとおりです。これらが同じように実装されていない可能性のあるケース/クラスの例を誰かが持っていますか? または、この設計の選択の背後にある理論的根拠を知っている/推測している人はいますか?


質問でテキストを読むよりもコードを見ることを好む人のために、ここに要約があります:

次の 2 行が同じ副作用を持たないのは、どのタイプT(必要なものを表すユーザー定義のクラス) の場合に意味がありますか。

T v;

v++;
++v;

EDIT
以下の @Simple のコメントを引用すると、質問が明確になることを願っています。

コンパイラがコピー自体を実行して事前インクリメントを実行できる場合、言語にポストインクリメント (オーバーロード) がある理由


EDIT 2
質問は明らかに多くの人にとって不明確であるため、別の説明を次に示します。

次の 2 行を考えてみましょう。

b = a++;
b = ++a;

それが 1 つの演算子の場合 (引数のために、演算子 +a+ と呼びます)、最初の行はコンパイラによって次のように変換されます。

b = a;
+a+;

そして2番目に

+a+;
b = a;
4

8 に答える 8

2

プレインクリメントは、ステートメントの残りの部分の前に変数をインクリメントします。たとえば、

x = 2;
y = ++x;

y == 3;
x == 3;

ポストインクリメントはステートメントの残りの後にインクリメントを行いますが、

x = 2;
y = x++;

y == 2;
x == 3;

事前インクリメントはわずかに高速であるため、優先する必要があります。注意すべきことは、両方の演算子が 1 つのステートメントで使用されている場合、動作は未定義であるため、次のようなものです。

x = 5;
x = x++ + ++x;

異なる言語では異なる結果が得られます。

于 2013-11-05T15:56:21.720 に答える
2

post-increment の汎用バージョンをどのように実装しますか?

私は推測する:T operator++(int) { T tmp(*this); ++*this; return tmp; }

私の型がコピーできない場合、またはコピーするのにコストがかかる場合はどうなりますか?

まあ、私は好むだろう:

Proxy operator++(int) { return Proxy(++*this, 1); }

そして、次のようなものがあります:

bool operator==(Proxy const& left, T const& right) {
    return left.value - 1 == right.value;
}

コンパイラがコピー自体を実行して事前インクリメントを実行できる場合、言語にポストインクリメント (オーバーロード) があるのはなぜですか?

コンパイラがコピーを実行できるというあなたの仮定は誤りであり、それが成り立つ場合でもコストがかかりすぎる可能性があるためです。

于 2013-11-05T16:15:17.520 に答える
1

この区別は、複雑な型に対する反復子で重要になります。表現

*it++

イテレータが現在指しているオブジェクトを提供し、イテレータをインクリメントします。通常、イテレータが進んだ後、データがメモリに保持されない場合、前のオブジェクトを返すことは困難になります。これには 2 つのアプローチがあります。

  1. ポストインクリメントにコピーを保持する
  2. 遅ればせながら前進

前者のメソッドは、イテレータのように動作するものを返す必要があります (少なくともoperator*andに関してoperator->は、オブジェクトのコピーの所有権も保持する必要があるため、ポインターにすることはできません。そのため、プロキシが返されます。

struct iterator {
    value_type value;

    struct proxy {
        value_type value;
        value_type &operator*() { return value; }
        value_type *operator->() { return &value; }
    };

    value_type &operator*() { return value; }
    value_type *operator->() { return &value; }
    iterator &operator++(); // actual increment code
    proxy operator++(int) { proxy ret = { value }; ++*this; return ret; }
};

コピーの作成にもコストがかかり、避ける必要がある場合は、インクリメントを遅らせることもできます。

struct iterator {
    value_type value;
    bool needs_increment;

    value_type &operator*() { if(needs_increment) ++*this; return value; }
    value_type *operator->() { if(needs_increment) ++*this; return &value; }
    iterator &operator++(); // actual increment code, resets needs_increment
    value_type *operator++(int) { needs_increment = true; return &value; }
};
于 2013-11-05T16:27:33.137 に答える
0

次の例を見てください

int i = 5;
int x = i++;
cout << i << " " << x;

これは印刷されます

6 5

int i = 5;
int x = ++i;
cout << i << " " << x;

これは印刷されます

6 6

では、何を推測できますか?
post-fix では、 の値がi最初に x に割り当てられ、次にiインクリメントされます
。 pre-fix では、 の値がi最初にインクリメントされ、次に x に割り当てられます。

于 2013-11-05T15:59:59.143 に答える
0

組み込み型に対するこれらの演算子のセマンティクスが異なるためです。どちらもオペランドを変更しますが、式の値は前後のインクリメント/デクリメントで異なります。

int a = 1;
(a++) == 1;
a = 1;
(++a) == 2;

それらを個別にオーバーロードできるようにすることで、戻り値に同様のセマンティクスを作成できます。

于 2013-11-05T15:55:05.600 に答える
0

問題は、(部分)式の評価の順序と副作用の適用時間に関連していると思います。たとえば、C# では、(サブ) 式の評価の順序は決定論的であり、副作用は一度に適用されます。たとえば、次の C# コードを考えてみましょう

int x = 0;
int y = x++ + ++x;

このコードは、C# で動作を定義しています。したがって、インクリメント演算子を 1 つだけ実装でき、コンパイラはそれを適切な方法で使用します。

C++ にはそのような可能性はありません。(サブ) 式の評価の順序は指定されておらず、副作用は一度に適用されません。

于 2013-11-05T16:17:17.610 に答える
-1

これらは、2 つの異なる (関連はあるものの) ことを行うため、2 つの別個のオペレーターです。

プレインクリメント/デクリメントは、変数をインクリメント/デクリメントし、新しい値を返します。

int i = 0;
int j = ++i; // j is now 1

ポストインクリメント/デクリメントは、変数をインクリメント/デクリメントし、古い値を返します。

int i = 0;
int j = i++; // j is now 0

一般に、これらの演算子の実装は次のようになります (一部の型の場合T):

T& T::operator++() // prefix overload
{
    *this = *this + 1;
    return *this;
}

T T::operator++(int) // postfix overload
{
    T prev = *this;
    ++(*this); // call prefix overload
    return prev;
}

ご覧のとおり、プレフィックス オーバーロードは型の追加のコピーを必要としませんが、ポストフィックス バージョンは必要です。

コメントの大部分は、なぜこれが当てはまるのかという質問に集中しているため:

簡単な答えは次のとおりです。C標準がそう言っているからです(そしてC ++はCからそれを継承しました)。

より長い答えは次のとおりです。

++aおよびa++は、特定の関数を呼び出すための簡略表記です。 ++a(特定のタイプのT場合) はT& T::operator++()または にマップされ、 は またはにT& operator++(T&)マップa++されます。すべての演算子と同様に、(プログラマとして) 対応する型に関して必要なことを行うようにそれらを定義できます(注: 一般に、演算子をオーバーロードして何か奇妙なことを行うのは悪い習慣と考えられていますが、標準はあなたを止めませんそうすることから)。一般に、型 (反復子など) を定義する場合は、同様のインターフェイスを提供する (つまり、適切な演算子をオーバーロードする) ことにより、組み込み型 (ポインターなど) の動作と一致させます。ただし、必要に応じて決定できますT T::operator++(int)T operator(T&, int)operator++()二次式を実行しoperator++(int)、フーリエ変換を行います。それらは 2 つの別個の機能であるため、許可されます。operator++(int)で定義されるという前提に基づいてコンパイラが推論することを許可されている場合operator++()、それらは結び付けられます。

C++ の演算子は、関数呼び出しの簡略表記にすぎません。他の観点からいくつかの演算子を実装することは一般的ですが、標準では要求されていないため、コンパイラはそのような仮定を行うことができません。標準で必要な場合は、追跡する必要があると想定される動作がたくさんあります。

さらに、 と の動作は++aCa++からのキャリーオーバーです。いずれかの動作を利用するコードが多数存在し、C++ 標準でそれを変更すると C との互換性が失われます (ただし、 C 標準の変更)。これらの演算子の動作を利用する既存のコードが多数あるため、重大な破壊的変更を行う可能性があります

プリインクリメントの観点からポストインクリメントを実装することは非常に一般的ですが、実際にはこれら 2 つの関数を異なる関数と考える必要があります ( operator==vs operator!=operator<operator>などについて考えるのとほぼ同じです。何かが共通しているからといって、標準はそれを要件にするか、または要件にする必要があります。

于 2013-11-05T16:16:21.463 に答える