CおよびC ++の左辺値と右辺値について

こんにちは、Habr! Eli Benderskyによる記事の翻訳、CおよびC ++の左辺値と右辺値の理解を紹介します



翻訳者から: C / C ++の左辺値と右辺値に関する興味深い記事の翻訳に注目します。 このトピックは新しいものではありませんが、これらの概念を知るのに遅すぎることはありません。 この記事は、初心者、またはC(または他の言語)からC ++に切り替えるプログラマーを対象としています。 したがって、詳細な咀wingに備えてください。 興味があれば、猫にようこそ。



左辺値右辺 という用語は、C / C ++でプログラミングするときによく出くわすものではありません。また、それが満たされたときに、それらの意味がすぐには明らかになりません。 それらに遭遇する可能性が最も高い場所は、コンパイラメッセージです。 たとえば、 gcc



コンパイラで次のコードをコンパイルする場合:



 int foo() { return 2; } int main() { foo() = 2; return 0; }
      
      





次のものを奪います:



 test.c: In function 'main': test.c:8:5: error: lvalue required as left operand of assignment
      
      





私はこのコードが少し手に負えないことに同意します、そして、あなたがこのような何かを書くことはありそうにありませんが、エラーメッセージはlvalueに言及します。これは、C / C ++チュートリアルではあまり見られない用語です。 次のコードをg++



コンパイルするときの別の例を示しg++







 int& foo() { return 2; }
      
      





次のエラーが表示されます。



 testcpp.cpp: In function 'int& foo()': testcpp.cpp:5:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
      
      





繰り返しますが、神秘的な右辺値はエラーメッセージに記載されています。 CおよびC ++で左辺値右辺 として理解されるものは何ですか? これがこの記事のトピックです。



簡単な定義



最初に、単純化された形式で左辺値右辺 の定義を意図的に与えます。 将来的には、これらの概念は虫眼鏡の下で検討されます。



lvalue(ロケーター値)は、メモリ内の識別可能な場所を占有するオブジェクトです(たとえば、アドレスを持っています)。



右辺値は例外によって定義され、式は左辺値または右辺 値のいずれかであることを示します。 したがって、 左辺値の定義は、 右辺値がメモリ内の識別可能な場所を占有するオブジェクトではない式であることを意味します。



基本的な例



上記で定義された用語は少し曖昧に見えるかもしれません。 したがって、いくつかの簡単な説明例をすぐに検討する価値があります。 整数変数を扱っているとします:



 int var; var = 4;
      
      





代入演算子は左辺に左辺値を期待し、 var



は左辺値です。これは、識別可能なメモリ位置を持つオブジェクトであるためです。 一方、次のスペルはエラーにつながります。



 4 = var; // ERROR! (var + 1) = 4; // ERROR!
      
      





定数4



も式var + 1



も左辺値ではありません

(自動的に右辺値になります)。 どちらもメモリ内の特定の場所を持たない式の一時的な結果であるため、これらは左辺値ではありません(つまり、計算の間、いくつかの一時レジスタに存在する可能性があります)。 したがって、この場合の割り当てには意味的な意味はありません。 言い換えれば、適切な場所はありません。



これで、最初のコードフラグメントのエラーメッセージの意味が明確になります。 foo



は一時的な値を返します。これは右辺値です。 割り当ての試みは間違いです。 つまり、コードfoo() = 2;



foo() = 2;



、コンパイラは、代入演算子の左側に左辺値が必要であることを報告します。



ただし、関数呼び出し結果へのすべての割り当てが誤っているわけではありません。 たとえば、C ++で参照を使用すると、これが可能になります。



 int globalvar = 20; int& foo() { return globalvar; } int main() { foo() = 10; return 0; }
      
      





ここで、 foo



はリンクを返します。 これは左辺値です。つまり、値を与えることができます。 一般に、C ++では、関数呼び出しの結果として左辺値を返す機能は、オーバーロードされた演算子を実装するために不可欠です。 例として、検索結果に基づいてアクセスを実装するクラスで演算子[]



オーバーロードしましょう。 たとえば、 std::map







 std::map<int, float> mymap; mymap[10] = 5.6;
      
      





mymap[10]



割り当ては、 std::map::operator[]



が非定数のオーバーロードであるため、値を割り当てることができるリンクを返すため、機能します。



可変左辺値



最初、 左辺値の概念がCで導入されたとき、それは文字通り「代入演算子の左側に適用可能な式」を意味していました。 ただし、後にISO Cがconst



キーワードを追加したとき、この定義をさらに開発する必要がありました。 確かに:



 const int a = 10; // 'a' - lvalue a = 10; //       !
      
      





したがって、すべての左辺値に値を割り当てることができるわけではありません。 呼び出すことができるのは、変更可能な左辺値です。 正式には、C99標準は可変左辺値を次のように定義しています。

[...]型が配列ではなく、不完全ではなく、 const



指定子を持たないlvalueは、 const



指定子を持つ構造体またはフィールドを含むユニオン(含まれる集約およびユニオンに再帰的にネストされたフィールドも含む)ではありません。


左辺値と右辺値間の変換



比Fig的に言えば、オブジェクトの値を操作する言語構成体には、引数として右辺値が必要です。 たとえば、バイナリの「+」演算子は引数として2つの右辺値を取り、右辺値も返します。



 int a = 1; // a - lvalue int b = 2; // b - lvalue int c = a + b; // '+'  rvalue,  a  b   rvalue //  rvalue    
      
      





前に見たように、 a



b



どちらも左辺値です。 したがって、3行目では、暗黙的に左辺値から右辺値への変換が行われます。 配列でも関数でもなく、不完全な型でもないすべての左辺値は、右辺値に変換できます。



他の方法で変換するのはどうですか? 右辺値を左辺値に変換することは可能ですか? もちろん違います! これは、定義されている左辺値の本質に違反します(暗黙的な変換がないため、右辺値が期待される場所では右辺値を使用できません)。



これは、右辺値から明示的に左辺値を取得できないという意味ではありません。 たとえば、単項演算子 '*'(逆参照)は引数として右辺値を取りますが、結果として左辺値を返します。 次の有効なコードを検討してください。



 int arr[] = {1, 2}; int* p = &arr[0]; *(p + 1) = 10; // OK: p + 1 rvalue,  *(p + 1)  lvalue
      
      





逆に、単項演算子 '&'(アドレス)は引数として左辺値を取り、右辺値を生成します。



 int var = 10; int* bad_addr = &(var + 1); // :  lvalue    '&' int* addr = &var; // : var - lvalue &var = 40; // :  lvalue    //  
      
      





「&」文字は、C ++でわずかに異なる役割を果たします-参照型を定義できます。 「左辺値への参照」と呼ばれます。 無効な右辺値から左辺値への変換が必要になるため、左辺値への非定数参照には右辺値を割り当てることができません。



 std::string& sref = std::string(); // :   //    'std::string&' // rvalue  'std::string'
      
      





左辺値への定数参照には右辺値割り当てることができます 。 それらは定数であるため、参照によって値を変更することはできません。そのため、右辺値を変更する問題は単に存在しません。 このプロパティにより、基本的なC ++イディオムの1つが可能になります-定数参照によって関数引数として値を許可し、一時オブジェクトの不必要なコピーと作成を回避します。



CV固有の右辺値



左辺値から右辺値への変換に関するC ++標準の部分(C ++ 11標準ドラフトの4.1章)を注意深く読んだ場合、次のことがわかります。

機能的でない型Tまたは配列である左辺値(3.10)は、右辺値に変換できます。 [...] Tがクラスでない場合、右辺値の型はT型のcv-unspecifiedバージョンです。それ以外の場合、右辺値の型はTです。

では、「cv-unspecified」とはどういう意味ですか? CV指定子は、 constおよびvolatile型指定子を記述するために使用される用語です。



3.9.3章から:

cv-unspecifiedの完全または不完全なオブジェクト型またはvoid型(3.9)である各型には、それぞれ3つのcv指定バージョンがあります。const指定子を持つ型、volatile指定子を持つ型、およびconst volatile指定子を持つ型です。 [...] CV固有のタイプとCVの未指定のタイプは異なりますが、それらは同じアイデアとアライメント要件を持っています。

しかし、これはすべて右辺値にどのように関係していますか? Cでは、右辺値にcv指定型が含まれることはありません。 これは左辺値プロパティです。 ただし、C ++では、クラスの右辺値をcvで指定できます。これは、 int



などの組み込み型には適用されません。 例を考えてみましょう:



 #include <iostream> class A { public: void foo() const { std::cout << "A::foo() const\n"; } void foo() { std::cout << "A::foo()\n"; } }; A bar() { return A(); } const A cbar() { return A(); } int main() { bar().foo(); //  foo cbar().foo(); //  foo const }
      
      





main



関数の2行目は、 foo() const



メソッドをcbar



ます。 cbar



は、 cbar



const A



とは異なるconst A



型のオブジェクトを返すためcbar



A



これはまさに、上記の標準からの抜粋の最後の文で意図されていたものです。 ところで、 cbar



が返す値は右辺値であることに注意してください。 これは、cvで指定された右辺値が動作している例です。



右辺値へのリンク(C ++ 11)



右辺値への参照と転送セマンティクスの関連概念 、C ++ 11言語に追加された最も強力なツールの1つです。 このトピックに関する詳細な説明は、この控えめな記事の範囲を超えています(「右辺値参照」をグーグルで検索するだけで多くの資料を見つけることができます。ここに、役立つリソースを紹介します。 これこれ、 特にこのリソース)。単純な例です。なぜなら、この章は、左辺値と右辺値を理解することで、自明でない言語の概念を議論する能力がどのように広がるかを示す最も適切な場所だと思うからです。



記事のかなりの半分が、左辺値と右辺値の主な違いの1つは左辺値を変更できるが右辺値は変更できないという事実の説明に費やされました。 まあ、C ++ 11はこの違いに1つの重要な機能を追加し、右辺値への参照を持たせることで、場合によってはそれらを変更できます。



例として、整数の動的配列の最も単純な実装を検討してください。 この章のトピックに関連するメソッドのみを見てみましょう。



 class Intvec { public: explicit Intvec(size_t num = 0) : m_size(num), m_data(new int[m_size]) { log("constructor"); } ~Intvec() { log("destructor"); if (m_data) { delete[] m_data; m_data = 0; } } Intvec(const Intvec& other) : m_size(other.m_size), m_data(new int[m_size]) { log("copy constructor"); for (size_t i = 0; i < m_size; ++i) m_data[i] = other.m_data[i]; } Intvec& operator=(const Intvec& other) { log("copy assignment operator"); Intvec tmp(other); std::swap(m_size, tmp.m_size); std::swap(m_data, tmp.m_data); return *this; } private: void log(const char* msg) { cout << "[" << this << "] " << msg << "\n"; } size_t m_size; int* m_data; };
      
      





したがって、ここには通常のコンストラクタとデストラクタ、コピーコンストラクタと代入演算子があります(これは例外に対する安定性の観点からコピー代入演算子の標準的な実装です。例外がどこかで発生した場合、初期化されていないメモリを持つ中間状態)。 それらはすべてロギング機能を使用しているため、実際に呼び出されるタイミングを把握できます。



v1



の内容をv2



コピーする簡単なコードを実行してみましょう。



 Intvec v1(20); Intvec v2; cout << "assigning lvalue...\n"; v2 = v1; cout << "ended assigning lvalue...\n";
      
      





そして、これが私たちが見るものです:

 assigning lvalue... [0x28fef8] copy assignment operator [0x28fec8] copy constructor [0x28fec8] destructor ended assigning lvalue...
      
      





これは、代入演算子の内部で起こることを正確に反映しているため、完全に論理的です。 しかし、 v2



右辺値を割り当てたいとしましょう:



 cout << "assigning rvalue...\n"; v2 = Intvec(33); cout << "ended assigning rvalue...\n";
      
      





ここでは新しく作成したベクターにのみ値を割り当てていますが、これは一時的な右辺値を作成してv2



割り当てる一般的なケースのデモの1つです(たとえば、関数がベクターを返す場合に発生する可能性があります)。 画面に表示されるものは次のとおりです。



 assigning rvalue... [0x28ff08] constructor [0x28fef8] copy assignment operator [0x28fec8] copy constructor [0x28fec8] destructor [0x28ff08] destructor ended assigning rvalue...
      
      





わあ! とても面倒です。 特に、一時オブジェクトを作成して削除するには、デストラクタを使用したコンストラクタ呼び出しの追加ペアが必要でした。 コピー割り当て演算子の内部で別の一時オブジェクトが作成および削除されるため、これは悲しいことです。 無料で追加の作業を行います。



しかし、違います! C ++ 11は、右辺値へのリンクを提供します。この値を使用して、「転送セマンティクス」、特に「転送代入演算子」を実装できます(現在、私は常にoperator=



copy assignment operatorと呼ばれています。重要)。 別のoperator=



IntVec



追加しましょう。



 Intvec& operator=(Intvec&& other) { log("move assignment operator"); std::swap(m_size, other.m_size); std::swap(m_data, other.m_data); return *this; }
      
      





double aspersandは、 右辺値へ参照です。 それはそれが約束することを正確に意味します-それは呼び出し後に破壊される右辺値へのリンクを与えます。 この事実を使用して、右辺値の内部を単純に「こっそり」することができます-とにかくそれらを必要としません! 画面に表示される内容は次のとおりです。



 assigning rvalue... [0x28ff08] constructor [0x28fef8] move assignment operator [0x28ff08] destructor ended assigning rvalue...
      
      





ご覧のとおり、右辺値がv2



割り当てられているため、新しい移植可能な代入演算子が呼び出されます。 Intvec(33)



介して作成された一時オブジェクトには、コンストラクターとデストラクターの呼び出しが依然として必要です。 ただし、代入演算子内の別の一時オブジェクトは不要になりました。 演算子はそれ自体で内部右辺値バッファを変更するだけであるため、右辺値デストラクタはオブジェクト自体のバッファを削除します。これは使用されなくなります。 純粋に!



この例は、転送とセマンティックスへのリンクのセマンティクスの氷山の一角にすぎないことをもう一度指摘しておきます。 ご想像のとおり、これは多くの特別なケースとパズルを含む複雑なトピックです。 私は、C ++での左辺値と右辺値の違いの非常に興味深いアプリケーションを示すことだけを試みました。 コンパイラーは明らかにそれらを区別し、コンパイル時に正しいコンストラクターを呼び出すことに注意します。



おわりに



エラー値でコンパイラの不可解な専門用語としてそれらを省略して、右辺値と左辺値の違いを考えずに多くのC ++コードを書くことができます。 ただし、この記事で説明しようとしたように、このトピックのより良いコマンドは、特定のC ++コンストラクトのより深い理解を提供し、C ++標準の一部と言語専門家間の議論をよりアクセスしやすくします。



C ++ 11標準では、C ++ 11が右辺値参照と転送セマンティクスの概念を導入するため、このトピックはさらに重要です。 言語の新しい機能を本当に理解するには、右辺値と左辺値の厳密な理解が必要です。



All Articles