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.

