++i が左辺値であり、i++ でないのはなぜですか?
11 に答える
ポストインクリメントとプレインクリメントの機能的な違いに取り組んでいる人もいます。
左辺値に関する限り、変数i++
を参照しないため、に割り当てることはできません。計算値を指します。
割り当てに関しては、次の両方が同じ意味で意味をなさない:
i++ = 5;
i + 0 = 5;
pre-increment は、一時的なコピーではなく、インクリメントされた変数への参照を返すため++i
、左辺値です。
パフォーマンス上の理由からプリインクリメントを優先することは、int よりもかなり重い (STL などの) イテレータ オブジェクトのようなものをインクリメントする場合に特に有効です。
++i
別の回答者が既に指摘したように、左辺値が参照に渡される理由はそれです。
int v = 0;
int const & rcv = ++v; // would work if ++v is an rvalue too
int & rv = ++v; // would not work if ++v is an rvalue
2 番目の規則の理由は、参照が const への参照である場合に、リテラルを使用して参照を初期化できるようにするためです。
void taking_refc(int const& v);
taking_refc(10); // valid, 10 is an rvalue though!
なぜ右辺値を導入するのですか? これらの用語は、これら 2 つの状況の言語ルールを構築するときに出てきます。
- ロケーター値が必要です。これは、読み取り可能な値を含む場所を表します。
- 式の値を表現したい。
上記の 2 つのポイントは、C99 標準から取られています。これには、次の非常に役立つ脚注が含まれています。
[ ''lvalue'' という名前は、最初は代入式 E1 = E2 に由来します。ここで、左側のオペランド E1 は (変更可能な) 左辺値である必要があります。オブジェクトの「ロケーター値」を表していると考えたほうがよいでしょう。「右辺値」と呼ばれることがあるものは、この国際標準では「式の値」として記述されています。]
ロケーター値はlvalueと呼ばれ、その場所を評価した結果の値はrvalueと呼ばれます。それは、C++ 標準 (左辺値から右辺値への変換について話している) に従っても正しいです。
4.1/2: 左辺値によって示されるオブジェクトに含まれる値は、右辺値の結果です。
結論
上記のセマンティクスを使用すると、i++
が左辺値ではなく右辺値である理由が明らかになりました。返された式はもう存在しi
ない (インクリメントされている!) ため、関心のあるのは値だけです。によって返された値を変更してi++
も意味がありません。なぜなら、その値を再度読み取ることができる場所がないからです。したがって、標準はそれが右辺値であると述べているため、const への参照にのみバインドできます。
ただし、対照的に、 によって返される式++i
は の場所 (左辺値) ですi
。in のように、左辺値から右辺値への変換を引き起こすと、そこからint a = ++i;
値が読み取られます。または、それへの参照ポイントを作成し、後で値を読み取ることもできます: int &a = ++i;
.
右辺値が生成される他の機会にも注意してください。たとえば、すべての一時変数は右辺値、バイナリ/単項の + およびマイナスの結果、参照ではないすべての戻り値式です。これらの式はすべて、名前付きオブジェクトにはありませんが、むしろ値のみを運びます。もちろん、これらの値は、定数ではないオブジェクトによってバックアップできます。
rvalue references
次の C++ バージョンには、nonconst を指していても右辺値にバインドできる、いわゆるものが含まれます。理論的根拠は、これらの匿名オブジェクトからリソースを「盗む」ことができ、それを行うコピーを回避できるようにすることです。Object&
プレフィックス ++ ( を返す) とポストフィックス ++ ( を返す)をオーバーロードしたクラス型を想定するとObject
、次のようにすると最初にコピーが発生し、2 番目のケースでは右辺値からリソースが盗まれます。
Object o1(++a); // lvalue => can't steal. It will deep copy.
Object o2(a++); // rvalue => steal resources (like just swapping pointers)
++i
多くの人が左辺値がどのようになっているのかを説明しているようですが、特にCが左辺値としてどちらも許可していないという事実に照らして、C++標準委員会がこの機能を導入した理由は説明していません。comp.std.c ++に関するこの議論から、そのアドレスを取得したり、参照に割り当てたりできるようになっているようです。ChristianBauの投稿から抜粋したコードサンプル:
int i; extern void f(int * p); extern void g(int&p); f(&++ i); / *違法なCですが、Cプログラマー この機能を見逃したことはありません*/ g(++ i); / *C++プログラマーはこれが合法であることを望んでいます*/ g(i ++); /*合法的なC++ではなく、 この意味のあるセマンティクスを与える*/
ちなみに、組み込み型の場合は、シーケンスポイント間で2回変更されるため、未定義動作を呼び出すi
などの代入ステートメントがあります。++i = 10
i
コンパイルしようとすると左辺値エラーが発生します
i++ = 2;
しかし、私がそれを変更したときではありません
++i = 2;
これは、前置演算子 (++i) が i の値を変更してから i を返すため、引き続き代入できるためです。後置演算子 (i++) は i の値を変更しますが、代入演算子では変更できない古い値の一時コピーを返します。
元の質問への回答:
forループのように、ステートメント内でインクリメント演算子を単独で使用することについて話している場合、実際には違いはありません。ポストインクリメントはそれ自体をインクリメントして一時的な値を返す必要があるため、プリインクリメントの方が効率的であるように見えますが、コンパイラはこの違いを最適化します。
for(int i=0; i<limit; i++)
...
と同じです
for(int i=0; i<limit; ++i)
...
操作の戻り値をより大きなステートメントの一部として使用している場合、状況は少し複雑になります。
2つの簡単なステートメントでも
int i = 0;
int a = i++;
と
int i = 0;
int a = ++i;
異なっています。複数演算子ステートメントの一部としてどのインクリメント演算子を使用するかは、意図する動作によって異なります。要するに、1つだけを選択することはできません。両方を理解する必要があります。
POD プリインクリメント:
プレインクリメントは、オブジェクトが式の前にインクリメントされたかのように動作し、この式でそれが発生したかのように使用できる必要があります。したがって、C++ 標準委員会は、左辺値としても使用できると判断しました。
POD 投稿の増分:
ポストインクリメントは、POD オブジェクトをインクリメントし、式で使用するコピーを返す必要があります (n2521 セクション 5.2.6 を参照)。コピーは実際には変数ではないため、左辺値は意味がありません。
オブジェクト:
オブジェクトのプリインクリメントとポストインクリメントは、オブジェクトのメソッドを呼び出す手段を提供する言語の単なる構文糖衣です。したがって、技術的には、オブジェクトは言語の標準的な動作によって制限されるのではなく、メソッド呼び出しによって課される制限によってのみ制限されます。
これらのオブジェクトの動作を POD オブジェクトの動作に反映させるのは、これらのメソッドの実装者次第です (必須ではありませんが、期待されています)。
オブジェクトのプリインクリメント:
ここでの要件 (期待される動作) は、オブジェクトがインクリメントされ (オブジェクトに依存することを意味します)、メソッドが変更可能な値を返し、インクリメントが発生した後に元のオブジェクトのように見えることです (このステートメントの前にインクリメントが発生したかのように)。
これを行うのは簡単で、メソッドがそれ自体への参照を返すことだけが必要です。参照は左辺値であるため、期待どおりに動作します。
オブジェクトのポストインクリメント:
ここでの要件 (予想される動作) は、オブジェクトがインクリメントされ (プレインクリメントと同じ方法で)、返される値が古い値のように見え、変更できない (左辺値のように動作しないようにするため) ことです。 .
Non-Mutable:
これを行うには、オブジェクトを返す必要があります。オブジェクトが式内で使用されている場合、一時変数にコピー構築されます。一時変数は const であるため、可変ではなく、期待どおりに動作します。
古い値のように見えます:
これは、変更を加える前に (おそらくコピー コンストラクターを使用して) 元の値のコピーを作成することで簡単に実現できます。コピーはディープ コピーである必要があります。そうしないと、元の変更がコピーに影響を与えるため、オブジェクトを使用する式に関連して状態が変化します。
プリインクリメントと同じ方法:
同じ動作を得るために、プリインクリメントに関してポストインクリメントを実装するのがおそらく最善です。
class Node // Simple Example
{
/*
* Pre-Increment:
* To make the result non-mutable return an object
*/
Node operator++(int)
{
Node result(*this); // Make a copy
operator++(); // Define Post increment in terms of Pre-Increment
return result; // return the copy (which looks like the original)
}
/*
* Post-Increment:
* To make the result an l-value return a reference to this object
*/
Node& operator++()
{
/*
* Update the state appropriatetly */
return *this;
}
};
LValueについて
(
C
およびたとえば Perl) では、LValues も++i
そうでもありませんi++
。では
C++
、i++
ではなく、LValue で++i
はあります。++i
は と同等でi += 1
、これは と同等i = i + 1
です。
その結果、まだ同じオブジェクトを扱っていることになりますi
。
次のように表示できます。int i = 0; ++i = 3; // is understood as i = i + 1; // i now equals 1 i = 3;
i++
一方、次のように表示できます。最初に の値を
使用し、次にオブジェクトをインクリメントします。i
i
int i = 0; i++ = 3; // would be understood as 0 = 3 // Wrong! i = i + 1;
(編集:しみのある最初の試みの後に更新されました)。
たぶん、これはポストインクリメントが実装される方法と関係があります。おそらくそれはこのようなものです:
- 元の値のコピーをメモリに作成します
- 元の変数をインクリメントします
- コピーを返す
コピーは変数でも動的に割り当てられたメモリへの参照でもないため、l値にすることはできません。
主な違いは、i++ はプレインクリメント値を返すのに対し、++i はポストインクリメント値を返すことです。i++ を使用する非常にやむを得ない理由がない限り、通常は ++i を使用します。つまり、プレインクリメント値が本当に必要な場合です。
私見では、「++i」形式を使用することをお勧めします。整数または他の POD を比較する場合、プリインクリメントとポストインクリメントの違いは実際には測定できませんが、「i++」を使用するときに作成して返す必要がある追加のオブジェクトコピーは、オブジェクトが非常に高価な場合、パフォーマンスに大きな影響を与える可能性があります。コピーするか、頻繁にインクリメントします。
ところで、同じステートメント内の同じ変数に対して複数のインクリメント演算子を使用することは避けてください。少なくともCでは、「シーケンスポイントはどこにあるのか」と未定義の操作順序の混乱に陥ります。その一部はJava nd C#でクリーンアップされたと思います。
C#:
public void test(int n)
{
Console.WriteLine(n++);
Console.WriteLine(++n);
}
/* Output:
n
n+2
*/