Що таке lvalue і rvalue, arm, programming

Якщо Ви деякий час програмували на мові C або на мові C ++, то ймовірно чули про терміни lvalue (читається як "EL-value") і rvalue (читається як "AR-value"), тому що вони іноді з'являються в повідомленнях про помилки компілятора . Є також якийсь шанс, що у Вас немає певного розуміння, що ж це все-таки означає.

З огляду на неоднозначність у визначеннях для lvalue і rvalue серед мовних стандартів, я не підготовлений запропонувати точні визначення. Однак можу пояснити базову основу концепцій, загальну для всіх стандартів.

Що таке lvalue і rvalue, arm, programming

Як часто буває з малозрозумілими концепціями мови, Ви можете задати собі питання - навіщо потрібно піклуватися про знання сенсу lvalue і rvalue? На загальну думку, якщо Ви пишете тільки на C, то можете працювати далі, не розуміючи, що по суті таке lvalues ​​і rvalues. Багато програмістів так і роблять. Але розуміння lvalues ​​і rvalues ​​надає цінну здатність проникнення в суть поведінки вбудованих операторів, і коду, який генерує компілятор для виконання цих операторів. Якщо Ви програмуєте на C ++, то розуміння роботи вбудованих операторів дає обов'язкову основу для якісного написання перезавантажувати операторів (overloaded operators).

Керниган і Річі ввели термін lvalue, щоб відокремити одні вирази від інших. У своїй книзі "C Programming Language" (Prentice-Hall, 1988) вони написали: "Об'єктом є маніпульована область зберігання; lvalue є виразом, що посилаються на об'єкт. Ім'я 'lvalue' відбулося з виразу присвоювання E1 = E2, в якому лівий операнд E1 повинен бути виразом типу lvalue. "

Що таке lvalue і rvalue, arm, programming

Іншими словами, ліві і праві операнди в вираженні присвоювання є самостійними виразами. Для того, щоб присвоювання було допустимим, лівий операнд повинен посилатися на об'єкт, який повинен бути типу lvalue. Правий операнд може бути будь-яким виразом, він не обов'язково повинен мати властивості lvalue. наприклад:

декларує n як об'єкт, що має тип int. Коли Ви використовуєте n у виразі присвоювання, типу:

n є виразом (точніше подвираженіем вираження привласнення), посилаються на об'єкт int. Вираз n є lvalue.

Припустимо, що Ви переставили місцями лівий і правий оператори:

Якщо Ви не колишній програміст мови Фортран, то очевидно це найдурніша річ, яку можна зробити. Присвоєння типу 3 = n намагається змінити значення целочисленной константи. На щастя, компілятори C і C ++ не допустять цього і видадуть помилку. (Прим. Перекладача: оскільки я іноді допускаю помилку, коли ненавмисно пишу замість == знак присвоювання =, то спеціально в операторі перевірки == ставлю зліва константу, щоб компілятор повідомив про помилку присвоювання. Якщо ж зліва буде lvalue, то компілятор не помітить каверзи , і не повідомить про помилку.) Підстава відхилення такої операції - то, що лівий операнд 3 в вираженні не є lvalue. Він є rvalue і не посилається на об'єкт; він просто є деяке значення.

Не знаю, звідки стався термін rvalue. Жоден із стандартів C не використовує його, крім як у виносці, де вказано, що "іноді стандарт описує rvalue як 'значення виразу'".

Специфікація C ++ Standard використовує термін rvalue, побічно його визначаючи в наступному вислові: "Кожен вираз відноситься або до lvalue, або до rvalue.". Таким чином, rvalue - будь-який вираз, яке не є lvalue.

Цифрові літерали, такі як 3 і 3.14159, є rvalue. До rvalue відносяться і символьні літерали, такі як 'a'. Ідентифікатор, який відноситься до об'єкту, є lvalue, але ідентифікатор, який іменує перераховується константу (enumeration constant), є rvalue. наприклад:

Друге присвоювання викличе помилку при компіляції, тому що blue є rvalue.

Хоча Ви не можете використовувати rvalue як lvalue, Ви можете використовувати lvalue як rvalue. Наприклад, визначте:

Ви можете привласнити значення n об'єкту, який позначений через m:

Цей вислів використовує lvalue-вираз n в якості rvalue. Строго кажучи, компілятор виконує те, що стандарт C ++ Standard називає перетворення від lvalue до rvalue (lvalue-to-rvalue conversion), щоб отримати значення, збережене в об'єкті, який позначений через n.

[Lvalue в інших виразах]

Хоча lvalue і rvalue отримали свої імена за своїми ролями (позиції) в виразах привласнення, концепція цих понять застосовується у всіх виразах, навіть в тих, які втягують інші вбудовані оператори.

Наприклад, обидва операнда вбудованого довічного оператора + повинні бути виразами. Очевидно, у цих виразів повинні бути відповідні типи. Після перетворень обидва вирази повинні мати однаковий арифметичний тип, або один вираз має мати тип покажчика, і інше має мати цілий тип. Але будь-яка з них може бути або lvalue або rvalue. таким чином, обидва вирази x + 2 і 2 + x є допустимими.

Хоча операнди бінарного оператора + можуть бути lvalue, результат у нього завжди rvalue. Наприклад, дані цілі об'єкти m і n, і такий вираз призведе до помилки:

Оператор + має більш високий пріоритет, ніж оператор =. Таким чином, вираз присвоювання еквівалентно наступному:

Помилка відбувається тому, що m + 1 є rvalue.

хоча унар вимагає lvalue як операнд, результат його буде rvalue. приклад:

На відміну від унар , Унар * в якості результату видає lvalue. Ненульовий покажчик p завжди вказує на об'єкт, тому * p є lvalue. приклад:

Хоча результат унар * є lvalue, його операнд може бути rvalue, як тут:

[Сховище даних для rvalue]

За базової концепції rvalue є просто значенням, воно не посилається на об'єкт. На практиці rvalue може посилатися на об'єкт. Просто необов'язково, що rvalue посилається на об'єкт. Тому і C, і C ++ наполягають, щоб Ви програмували, як ніби rvalue не посилаються на об'єкти.

Припущення, що rvalue не посилається на об'єкт, дає компіляторам C і C ++ значну свободу в генерації коду для виражень rvalue. Припустимо, що є присвоювання такого виду:

Тут n є цілим (int). Компілятор може згенерувати іменоване сховище даних, ініціалізувати значенням 1, як ніби-то 1 було lvalue. Це згенерувало б код для копіювання з Ініціалізувати сховища в місце зберігання, виділеного для n. На мові асемблера це могло б виглядати так:

В цьому випадку rvalue 1 ніколи не з'являється як об'єкт в просторі даних. Швидше він з'являється як інструкції в просторі коду.

На деяких процесорах найшвидший спосіб помістити значення 1 в об'єкт - очистити його і потім інкрементіровать, наприклад так:

Очищення об'єкта встановлює його вміст в 0. инкрементирование дає 1. І все ж дані, що представляють величини 0 та 1, ніде не з'являються в коді.

Хоча вірно, що rvalue на мові C не посилаються на об'єкти, для мови C ++ це не так. У C ++ rvalue, що мають тип класу, посилаються на об'єкти, але вони все ще не lvalue. Таким чином все, що я вже сказав для rvalue вірно, поки ми не маємо справу з rvalue типу класу.

Хоча lvalue позначають об'єкти, не всі lvalue можуть з'являтися як ліва частина оператора присвоєння. Як відомо, вираз є послідовністю операторів і операндів, які задають якісь обчислення. Обчислення можуть давати в результаті значення, чи виробляти побічні ефекти (side effects). Вираз присвоювання має форму

де e1 і e2 самі по собі вирази. Правий операнд e2 може бути будь-яким виразом, однак лівий операнд повинен бути виразом lvalue, він повинен посилатися на об'єкт (про це говорилося раніше).

Ключове слово const робить базове поняття lvalue неадекватним семантиці виразів. Ми повинні бути в змозі відрізнити один від одного різні види lvalue.

Кваліфікатор const з'являється в декларації, і модифікує тип в декларації, або деяку частину її, наприклад:

Тут декларується об'єкт типу "const int" (постійне ціле). Вираз n посилається на об'єкт, як ніби там немає константи, за винятком того, що n відноситься до такого об'єкта, який програма поміняти не може. Наприклад, присвоювання типу такого видасть помилку компіляції:

Як вираз, що посилається на const object, такий як n, чимось відрізняється від rvalue? Зрештою, якщо переписати ці вирази для літерального цілого числа замість n. то отримаємо ту ж саму помилку:

то тоді наступне присвоювання видасть помилку:

Оскільки cast примушує компілятор не скаржитися на перетворення, то робити щось подібне небажано, інакше це може стати причиною важко знаходять помилок.

Таким чином вираз, яке відноситься до об'єкту const, дійсно є lvalue, а не rvalue. Однак це спеціальний вид lvalue, званий немодіфіціруемих lvalue (non-modifiable lvalue), який не можна використовувати для модифікування об'єкта, на який посилається lvalue. Це на відміну від піддається зміни lvalue (який декларований без const), який можна використовувати для зміни об'єкта, на який посилається lvalue.

Оскільки тепер може впливати фактор наявності ключового слова const, то більше не буде точним називати ліву частину оператора присвоювання, як lvalue. Швидше, його потрібно назвати можливим для зміни lvalue (modifiable lvalue). Фактично, кожен арифметичний оператор присвоювання, такий як + = і * =, вимагає модифікується lvalue як лівого операнда. Для всіх скалярних типів:

за винятком того, що x оцінюється тільки один раз. Оскільки x в цьому арифметичному присвоєнні повинен бути модифікується lvalue, також має бути і в простому привласненні. Чи не кожен оператор, який вимагає операнда lvalue, вимагає саме модифікується lvalue. унарний оператор приймає в якості операнда як modifiable lvalue, так і non-modifiable lvalue. Наприклад, якщо задано:

то m є допустимим виразом, яке поверне тип "покажчик на int", і n буде також допустимим виразом, що повертає результат "покажчик на const int".

[Що ж насправді є немодифікованим]

Раніше я говорив, що non-modifiable lvalue це lvalue, який Ви не можете використовувати, щоб змінити об'єкт. Зауважте, що я не говорив, що non-modifiable lvalue відноситься до об'єкту, який Ви не можете змінити - я сказав, що Ви не можете використовувати lvalue, щоб змінити об'єкт. (Примітка перекладача: як я люблю ці ізвращенскіе головоломки мови C!) Різниця тонке, але тим не менш важливе, як буде показано в наступному прикладі. Припустимо:

Тут ми маємо, що p вказує на n, так що і * p, і просто n є двома різними виразами, що посилаються на один і той же об'єкт. Однак, * p і n мають різні типи. Як було розказано в замітці "What const Really Means" (Що насправді означає const) [1], присвоювання використовує конверсію кваліфікації, щоб перетворити значення типу "покажчик на int" в значення типу "покажчик на const int". Вираз n має тип «не const int". Це модифікуються lvalue, так що Ви можете модифікувати об'єкт, на яке воно вказує:

З іншого боку, p має тип "покажчик на const int", тому * p має тип const int. Вираз * p є немодифікованим lvalue, і Ви не можете використовувати * p для того, щоб модифікувати n (навіть якщо Ви можете використовувати для модифікації вираз n):

Така семантика визначення константи const в C і C ++.

Кожен вираз C і C ++ є або lvalue, або rvalue. Lvalue є виразом, який визначає об'єкт (посилається на нього з покажчиком або без). Кожне lvalue буває, в свою чергу, або модифікується, або немодифікованим. До rvalue відноситься будь-який вираз, яке не є lvalue. Оперативно відмінності між rvalue і lvalue можна звести до тез:

І знову, як уже згадувалося, все це відноситься тільки до rvalue не класова (non-class) типу. Класи в C ++ псують ці поняття ще більше.

1. Lvalues ​​and Rvalues ​​site: embedded.com.
2. Non-modifiable Lvalues ​​site: embedded.com.