12.2 — Kategorie wartości (lwartości i rwartości)

Zanim porozmawiamy o naszym pierwszym typie złożonym (odniesienia do lwartości), zboczymy z drogi i porozmawiamy o tym, czym jest lvalue jest.

W lekcji 1.10 -- Wprowadzenie do wyrażeń, zdefiniowaliśmy wyrażenie jako „kombinację literały, zmienne, operatory i wywołania funkcji, które można wykonać w celu uzyskania pojedynczej wartości”.

Na przykład:

#include <iostream>

int main()
{
    std::cout << 2 + 3 << '\n'; // The expression 2 + 3 produces the value 5

    return 0;
}

W powyższym programie wyrażenie 2 + 3 jest oceniane w celu uzyskania wartości 5, która jest następnie wydawana na konsolę.

W lekcji 6.4 — Operatory zwiększania/zmniejszania i skutki uboczne zauważyliśmy również, że wyrażenia mogą powodować skutki uboczne, które trwają dłużej niż wyrażenie:

#include <iostream>

int main()
{
    int x { 5 };
    ++x; // This expression statement has the side-effect of incrementing x
    std::cout << x << '\n'; // prints 6

    return 0;
}

W powyższym programie wyrażenie ++x zwiększa wartość of x, a wartość ta pozostaje zmieniona nawet po zakończeniu obliczania wyrażenia.

Oprócz tworzenia wartości i skutków ubocznych, wyrażenia mogą robić jeszcze jedną rzecz: mogą oceniać obiekty lub funkcje. Za chwilę zajmiemy się tym szerzej.

Właściwości wyrażenia

Aby określić, w jaki sposób wyrażenia powinny być oceniane i gdzie można ich używać, wszystkie wyrażenia w C++ mają dwie właściwości: typ i kategorię wartości.

Typ wyrażenia

Typ wyrażenia jest równoważny typowi wartości, obiektu lub funkcji, która wynika z ocenianego wyrażenia. Na przykład:

int main()
{
    auto v1 { 12 / 4 }; // int / int => int
    auto v2 { 12.0 / 4 }; // double / int => double

    return 0;
}

For v1, kompilator ustali (w czasie kompilacji), że dzielenie na dwa int operandy da wynik int , więc int jest typem tego wyrażenia. Na podstawie wnioskowania o typie, int zostanie wówczas użyty jako typ v1.

For v2, kompilator ustali (w czasie kompilacji), że dzielenie za pomocą argumentu double i argumentu int da wynik double . Pamiętaj, że operatory arytmetyczne muszą mieć operandy pasujących typów, zatem w tym przypadku operand int jest konwertowany na a double i wykonywane jest dzielenie zmiennoprzecinkowe. Zatem double jest typem tego wyrażenia.

Kompilator może użyć typu wyrażenia, aby określić, czy wyrażenie jest prawidłowe w danym kontekście. Przykładowo:

#include <iostream>

void print(int x)
{
    std::cout << x << '\n';
}

int main()
{
    print("foo"); // error: print() was expecting an int argument, we tried to pass in a string literal

    return 0;
}

W powyższym programie funkcja print(int) oczekuje parametru int . Jednak typ wyrażenia, które przekazujemy (literał ciągu "foo") nie jest zgodny i nie można znaleźć żadnej konwersji. W rezultacie pojawia się błąd kompilacji.

Zauważ, że typ wyrażenia musi być możliwy do określenia w czasie kompilacji (w przeciwnym razie sprawdzanie typu i dedukcja typu nie zadziałałyby) -- jednakże wartość wyrażenia może zostać określona w czasie kompilacji (jeśli wyrażenie to constexpr) lub w czasie wykonywania (jeśli wyrażenie nie jest constexpr).

Kategoria wartości wyrażenia

Rozważmy teraz następujący program:

int main()
{
    int x{};

    x = 5; // valid: we can assign 5 to x
    5 = x; // error: can not assign value of x to literal value 5

    return 0;
}

Jedna z tych instrukcji przypisania jest prawidłowa (przypisanie wartość 5 do zmiennej x), a jedna nie (co by oznaczało przypisanie wartości x do wartości literalnej 5?). Skąd więc kompilator wie, które wyrażenia mogą legalnie występować po obu stronach instrukcji przypisania?

Odpowiedź leży w drugiej właściwości wyrażeń: value category. Słowo kluczowe kategoria wartości wyrażenia (lub podwyrażenia) wskazuje, czy wyrażenie daje się rozwiązać na wartość, funkcję czy jakiś obiekt.

Przed C++11 istniały tylko dwie możliwe kategorie wartości: lvalue i rvalue.

W W języku C++11 dodano trzy dodatkowe kategorie wartości (glvalue, prvalue, I xvalue) w celu obsługi nowej funkcji o nazwie move semantics.

Nota autora

W tej lekcji pozostaniemy przy widoku kategorii wartości sprzed wersji C++11, ponieważ stanowi to łagodniejsze wprowadzenie do kategorii wartości (i to wszystko, czego w tej chwili potrzebujemy). Semantykę przenoszenia (oraz trzy dodatkowe kategorie wartości) omówimy w następnym rozdziale.

Wyrażenia l-wartości i rwartości

An wartość (wymawiane „wartość-ell”, skrót od „wartość lewa” lub „wartość lokalizatora”, czasami zapisywane jako „wartość-l”) to wyrażenie, którego wynikiem jest możliwy do zidentyfikowania obiekt lub funkcja (lub pole bitowe).

Termin „tożsamość” jest używany w standardzie C++, ale nie jest dobrze zdefiniowany. Obiekt (taki jak obiekt lub funkcja), który ma tożsamość, można odróżnić od innych podobnych obiektów (zwykle poprzez porównanie adresów obiektu).

Do obiektów z tożsamością można uzyskać dostęp za pośrednictwem identyfikatora, odwołania lub wskaźnika i zazwyczaj mają one dłuższy czas życia niż pojedyncze wyrażenie lub instrukcja.

int main()
{
    int x { 5 };
    int y { x }; // x is an lvalue expression

    return 0;
}

W powyższym programie wyrażenie x jest wyrażeniem lwartościowym, zgodnie z jego wartością zmienna x (która posiada identyfikator).

Od czasu wprowadzenia stałych do języka, lwartości występują w dwóch podtypach: a modyfikowalna lwartość to lwartość, której wartość można modyfikować. A niemodyfikowalna lwartość to lwartość, której wartości nie można modyfikować (ponieważ lwartość to const lub constexpr).

int main()
{
    int x{};
    const double d{};

    int y { x }; // x is a modifiable lvalue expression
    const double e { d }; // d is a non-modifiable lvalue expression

    return 0;
}

An wartość (wymawiane „arr-value”, skrót od „right value”, czasami zapisywane jako r-value) to wyrażenie, które nie jest lwartością. Wyrażenia Rvalue dają w wyniku wartość. Często spotykane wartości obejmują literały (z wyjątkiem literałów łańcuchowych w stylu C, które są lwartościami) oraz wartość zwracaną przez funkcje i operatory, które zwracają wartość. Wartości nie można zidentyfikować (co oznacza, że należy je natychmiast użyć) i istnieją jedynie w zakresie wyrażenia, w którym są użyte.

int return5()
{
    return 5;
}

int main()
{
    int x{ 5 }; // 5 is an rvalue expression
    const double d{ 1.2 }; // 1.2 is an rvalue expression

    int y { x }; // x is a modifiable lvalue expression
    const double e { d }; // d is a non-modifiable lvalue expression
    int z { return5() }; // return5() is an rvalue expression (since the result is returned by value)

    int w { x + 1 }; // x + 1 is an rvalue expression
    int q { static_cast<int>(d) }; // the result of static casting d to an int is an rvalue expression

    return 0;
}

Możesz się zastanawiać, dlaczego return5(), x + 1, I static_cast<int>(d) są wartościami: odpowiedź jest taka, że wyrażenia te generują wartości tymczasowe, które nie są możliwymi do zidentyfikowania obiektami.

Kluczowa informacja

Wyrażenia Lwartości oceniają się na możliwy do zidentyfikowania obiekt.
Wyrażenia R wartością są wartość.

Kategorie i operatory wartości

O ile nie określono inaczej, operatory oczekują, że ich operandy będą rwartościami. Na przykład binarny operator+ oczekuje, że jego operandy będą wartościami:

#include <iostream>

int main()
{
    std::cout << 1 + 2; // 1 and 2 are rvalues, operator+ returns an rvalue

    return 0;
}

Literały 1 i 2 są wyrażeniami wartości. operator+ z radością użyją ich do zwrócenia wyrażenia wartości 3.

Teraz możemy odpowiedzieć na pytanie, dlaczego x = 5 jest poprawne, ale 5 = x nie jest: operacja przypisania wymaga, aby jej lewy operand był modyfikowalnym wyrażeniem wartości. To ostatnie przypisanie (5 = x) kończy się niepowodzeniem, ponieważ lewy operand jest wyrażeniem 5 jest wartością, a nie modyfikowalną wartością.

int main()
{
    int x{};

    // Assignment requires the left operand to be a modifiable lvalue expression and the right operand to be an rvalue expression
    x = 5; // valid: x is a modifiable lvalue expression and 5 is an rvalue expression
    5 = x; // error: 5 is an rvalue expression and x is a modifiable lvalue expression

    return 0;
}

Konwersja lwartości na rwartość

Ponieważ operacje przypisania oczekują, że prawy operand będzie wyrażeniem rwartości, możesz zastanawiać się, dlaczego działa następujące działanie:

int main()
{
    int x{ 1 };
    int y{ 2 };

    x = y; // y is not an rvalue, but this is legal

    return 0;
}

W przypadkach, gdy oczekiwana jest wartość, ale jest ona pod warunkiem, że lwartość zostanie przekształcona z lwartości na rwartość, dzięki czemu będzie można jej używać w takich kontekstach. Zasadniczo oznacza to, że lwartość jest oceniana w celu wytworzenia jej wartości, która jest wartością.

W powyższym przykładzie wyrażenie lwartości y poddaje się konwersji lwartości na rwartość, która w wyniku y wytwarza rwartość (2), która jest następnie przypisana do x.

Kluczowa informacja

lwartość zostanie niejawnie przekonwertowana na rwartość. Oznacza to, że lwartości można użyć wszędzie tam, gdzie się jej oczekuje.
Z drugiej strony, wartość nie zostanie w sposób dorozumiany przekonwertowana na lwartość.

Rozważmy teraz następujący przykład:

int main()
{
    int x { 2 };

    x = x + 1;

    return 0;
}

W tej instrukcji zmienna x jest używana w dwóch różnych kontekstach. Po lewej stronie operatora przypisania (gdzie wymagane jest wyrażenie lwartości) x znajduje się wyrażenie lwartości, którego wynikiem jest zmienna x. Po prawej stronie operatora przypisania x przechodzi konwersję lwartości na rwartość, a następnie jest oceniany w taki sposób, że jego (2) może zostać użyty jako lewy operand operator+. operator+ zwraca wyrażenie rwartości 3, które jest następnie używane jako prawy operand przypisania.

Jak odróżnić lwartości od rwartości

Nadal możesz nie być zdezorientowany, jaki rodzaj wyrażeń kwalifikuje się jako lwartość i rwartość. Na przykład, czy wynik operator++ jest wartością czy wartością? Omówimy tutaj różne metody, których możesz użyć do określenia, która jest która.

Wskazówka

Praktyczna zasada identyfikowania wyrażeń l-wartość i rwartość:

  • Wyrażenia l-wartości to te, które wyznaczają funkcje lub możliwe do zidentyfikowania obiekty (w tym zmienne), które utrzymują się po zakończeniu wyrażenia.
  • Wyrażenia R-wartości to te, które oceniają wartości, w tym literały i obiekty tymczasowe, które nie pozostają poza końcem wyrażenia wyrażenie.

Pełniejszą listę wyrażeń lwartość i rwartość można znaleźć w dokumentacji technicznej.

Wskazówka

Pełną listę wyrażeń lwartość i rwartość można znaleźć tutaj. W C++11 wartości są podzielone na dwa podtypy: wartości prvalue i wartości x, więc wartości, o których tu mówimy, stanowią sumę obu tych kategorii.

W końcu możemy napisać program i poprosić kompilator o informację, jakiego rodzaju wyrażeniem jest coś. Poniższy kod ilustruje metodę określającą, czy wyrażenie jest wartością lvalue czy rvalue:

#include <iostream>
#include <string>

// T& is an lvalue reference, so this overload will be preferred for lvalues
template <typename T>
constexpr bool is_lvalue(T&)
{
    return true;
}

// T&& is an rvalue reference, so this overload will be preferred for rvalues
template <typename T>
constexpr bool is_lvalue(T&&)
{
    return false;
}

// A helper macro (#expr prints whatever is passed in for expr as text)
#define PRINTVCAT(expr) { std::cout << #expr << " is an " << (is_lvalue(expr) ? "lvalue\n" : "rvalue\n"); }

int getint() { return 5; }

int main()
{
    PRINTVCAT(5);        // rvalue
    PRINTVCAT(getint()); // rvalue
    int x { 5 };
    PRINTVCAT(x);        // lvalue
    PRINTVCAT(std::string {"Hello"}); // rvalue
    PRINTVCAT("Hello");  // lvalue
    PRINTVCAT(++x);      // lvalue
    PRINTVCAT(x++);      // rvalue
} 

Wypisuje:

5 is an rvalue
getint() is an rvalue
x is an lvalue
std::string {"Hello"} is an rvalue
"Hello" is an lvalue
++x is an lvalue
x++ is an rvalue

Ta metoda opiera się na dwóch przeciążonych funkcjach: jednej z parametrem referencyjnym lvalue i drugiej z parametrem referencyjnym rvalue. Wersja referencyjna lvalue będzie preferowana dla argumentów lvalue, a wersja referencyjna rvalue będzie preferowana dla argumentów rvalue. W ten sposób możemy określić, czy argument jest lwartością czy rwartością w oparciu o wybraną funkcję.

Jak zatem widać, to, czy operator++ w wyniku zostanie wygenerowana lwartość czy rwartość, zależy od tego, czy zostanie użyty jako operator przedrostkowy (który zwraca lwartość), czy operator postfiksowy (który zwraca rwartość)!

Dla zaawansowanych czytelników

W przeciwieństwie do innych literałów (które są rwartościami), literał łańcuchowy w stylu C jest wartością, ponieważ ciągi znaków w stylu C (które są tablicami w stylu C) zamieniają się w wskaźnik. Proces zanikania działa tylko wtedy, gdy tablica jest wartością (a zatem ma adres, który można zapisać we wskaźniku). C++ odziedziczył to w celu zapewnienia kompatybilności wstecznej.

Rozpad tablic omówimy na lekcji 17.8 -- Zanikanie tablicy w stylu C.

Teraz, gdy omówiliśmy już wartości, możemy przejść do naszego pierwszego typu złożonego: lvalue reference.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
155 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze